add message resource crud and gateway events (messages + message history)

This commit is contained in:
hippoz 2022-04-12 00:02:43 +03:00
parent ce9d331bc6
commit 6a6cc1aafd
Signed by: hippoz
GPG key ID: 7C52899193467641
7 changed files with 261 additions and 11 deletions

View file

@ -11,7 +11,15 @@ export default async function databaseInit() {
CREATE TABLE IF NOT EXISTS channels( CREATE TABLE IF NOT EXISTS channels(
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
name VARCHAR(32) UNIQUE NOT NULL, name VARCHAR(32) UNIQUE NOT NULL,
owner_id SERIAL REFERENCES users owner_id SERIAL REFERENCES users ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS messages(
id SERIAL PRIMARY KEY,
content VARCHAR(4000) NOT NULL,
channel_id SERIAL REFERENCES channels ON DELETE CASCADE,
author_id SERIAL REFERENCES users ON DELETE CASCADE,
created_at BIGINT
); );
`)); `));
} }

View file

@ -4,7 +4,11 @@ export enum GatewayPayloadType {
Ready, Ready,
Ping, Ping,
ChannelCreate = 1000, ChannelCreate = 110,
ChannelUpdate, ChannelUpdate,
ChannelDelete ChannelDelete,
MessageCreate = 120,
MessageUpdate,
MessageDelete
} }

View file

@ -55,8 +55,10 @@ function clientUnsubscribeAll(ws: WebSocket) {
export function dispatch(channel: string, message: GatewayPayload) { export function dispatch(channel: string, message: GatewayPayload) {
const members = dispatchChannels.get(channel); const members = dispatchChannels.get(channel);
if (!members) return; if (!members) return;
members.forEach(e => e.send(JSON.stringify(message))); members.forEach(e => {
e.send(JSON.stringify(message));
});
} }
function closeWithError(ws: WebSocket, { code, message }: { code: number, message: string }) { function closeWithError(ws: WebSocket, { code, message }: { code: number, message: string }) {

View file

@ -135,6 +135,11 @@ router.get(
authenticateRoute(), authenticateRoute(),
param("id").isNumeric(), param("id").isNumeric(),
async (req, res) => { async (req, res) => {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
}
const { id } = req.params; const { id } = req.params;
const result = await query("SELECT id, name, owner_id FROM channels WHERE id = $1", [id]); const result = await query("SELECT id, name, owner_id FROM channels WHERE id = $1", [id]);
if (result.rowCount < 1) { if (result.rowCount < 1) {
@ -157,4 +162,72 @@ router.get(
} }
); );
router.post(
"/:id/messages",
authenticateRoute(),
param("id").isNumeric(),
body("content").isLength({ min: 1, max: 4000 }),
async (req, res) => {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
}
const channelId = parseInt(req.params.id);
const { content } = req.body;
const authorId = req.user.id;
const createdAt = Date.now().toString();
const result = await query("INSERT INTO messages(content, channel_id, author_id, created_at) VALUES ($1, $2, $3, $4) RETURNING id", [content, channelId, authorId, createdAt]);
if (result.rowCount < 1) {
return res.status(500).json({
...errors.GOT_NO_DATABASE_DATA
});
}
const returnObject = {
id: result.rows[0].id,
content,
channel_id: channelId,
author_id: authorId,
created_at: createdAt
};
dispatch(`channel:${channelId}`, {
t: GatewayPayloadType.MessageCreate,
d: returnObject
});
return res.status(201).send(returnObject);
}
);
router.get(
"/:id/messages",
authenticateRoute(),
param("id").isNumeric(),
async (req, res) => {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
}
const { before } = req.query;
const channelId = parseInt(req.params.id);
let finalRows = [];
if (before) {
const result = await query("SELECT id, content, channel_id, author_id, created_at FROM messages WHERE id < $1 AND channel_id = $2 ORDER BY id DESC LIMIT 50", [before, channelId]);
finalRows = result.rows;
} else {
const result = await query("SELECT id, content, channel_id, author_id, created_at FROM messages WHERE channel_id = $1 ORDER BY id DESC LIMIT 50", [channelId]);
finalRows = result.rows;
}
return res.status(200).send(finalRows);
}
);
export default router; export default router;

View file

@ -0,0 +1,126 @@
import express from "express";
import { body, param, validationResult } from "express-validator";
import { authenticateRoute } from "../../../auth";
import { query } from "../../../database";
import { errors } from "../../../errors";
import { dispatch } from "../../../gateway";
import { GatewayPayloadType } from "../../../gateway/gatewaypayloadtype";
const router = express.Router();
router.delete(
"/:id",
authenticateRoute(),
param("id").isNumeric(),
async (req, res) => {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
}
const id = parseInt(req.params.id); // TODO: ??
const permissionCheckResult = await query("SELECT author_id, channel_id FROM messages WHERE id = $1", [id]);
if (permissionCheckResult.rowCount < 1) {
return res.status(404).json({
...errors.NOT_FOUND
});
}
if (permissionCheckResult.rows[0].author_id !== req.user.id) {
return res.status(403).json({
...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS
});
}
const result = await query("DELETE FROM messages WHERE id = $1", [id]);
if (result.rowCount < 1) {
return res.status(500).json({
...errors.GOT_NO_DATABASE_DATA
});
}
dispatch(`channel:${permissionCheckResult.rows[0].channel_id}`, {
t: GatewayPayloadType.MessageDelete,
d: {
id
}
});
return res.status(204).send("");
}
);
router.put(
"/:id",
authenticateRoute(),
body("content").isLength({ min: 1, max: 4000 }),
param("id").isNumeric(),
async (req, res) => {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
}
const { content } = req.body;
const id = parseInt(req.params.id); // TODO: ??
const permissionCheckResult = await query("SELECT author_id, channel_id, created_at FROM messages WHERE id = $1", [id]);
if (permissionCheckResult.rowCount < 1) {
return res.status(404).json({
...errors.NOT_FOUND
});
}
if (permissionCheckResult.rows[0].author_id !== req.user.id) {
return res.status(403).json({
...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS
});
}
const result = await query("UPDATE messages SET content = $1 WHERE id = $2", [content, id]);
if (result.rowCount < 1) {
return res.status(500).json({
...errors.GOT_NO_DATABASE_DATA
});
}
const returnObject = {
id,
content,
channel_id: permissionCheckResult.rows[0].channel_id,
author_id: permissionCheckResult.rows[0].author_id,
created_at: permissionCheckResult.rows[0].created_at
};
dispatch(`channel:${permissionCheckResult.rows[0].channel_id}`, {
t: GatewayPayloadType.MessageUpdate,
d: returnObject
});
return res.status(200).send(returnObject);
}
);
router.get(
"/:id",
authenticateRoute(),
param("id").isNumeric(),
async (req, res) => {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
}
const { id } = req.params;
const result = await query("SELECT id, content, channel_id, author_id, created_at FROM messages WHERE id = $1", [id]);
if (result.rowCount < 1) {
return res.status(404).json({
...errors.NOT_FOUND
});
}
return res.status(200).send(result.rows[0]);
}
);
export default router;

View file

@ -1,6 +1,7 @@
import { Application, json } from "express"; import { Application, json } from "express";
import usersRouter from "./routes/api/v1/users"; import usersRouter from "./routes/api/v1/users";
import channelsRouter from "./routes/api/v1/channels"; import channelsRouter from "./routes/api/v1/channels";
import messagesRouter from "./routes/api/v1/messages";
export default function(app: Application) { export default function(app: Application) {
app.get("/", (req, res) => res.send("hello!")); app.get("/", (req, res) => res.send("hello!"));
@ -8,4 +9,5 @@ export default function(app: Application) {
app.use(json()); app.use(json());
app.use("/api/v1/users", usersRouter); app.use("/api/v1/users", usersRouter);
app.use("/api/v1/channels", channelsRouter); app.use("/api/v1/channels", channelsRouter);
app.use("/api/v1/messages", messagesRouter);
}; };

View file

@ -19,13 +19,13 @@ content-type: application/json
### ###
GET http://localhost:3000/api/v1/users/self HTTP/1.1 GET http://localhost:3000/api/v1/users/self HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NzAwMTE0LCJleHAiOjE2NDk4NzI5MTR9.EOn8MBHZLCxfU5fHc0ZY2x9p3y-_RdD7X915L1B6Ftc
### ###
POST http://localhost:3000/api/v1/channels HTTP/1.1 POST http://localhost:3000/api/v1/channels HTTP/1.1
content-type: application/json content-type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NzAwMTE0LCJleHAiOjE2NDk4NzI5MTR9.EOn8MBHZLCxfU5fHc0ZY2x9p3y-_RdD7X915L1B6Ftc
{ {
"name": "yet another channel" "name": "yet another channel"
@ -35,7 +35,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6M
PUT http://localhost:3000/api/v1/channels/5 HTTP/1.1 PUT http://localhost:3000/api/v1/channels/5 HTTP/1.1
content-type: application/json content-type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NzAwMTE0LCJleHAiOjE2NDk4NzI5MTR9.EOn8MBHZLCxfU5fHc0ZY2x9p3y-_RdD7X915L1B6Ftc
#Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5MjU5NDUwLCJleHAiOjE2NDk0MzIyNTB9.JmF9NujFZnln7A-ynNpeyayGBqmR5poAyACYV6RnSQY #Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5MjU5NDUwLCJleHAiOjE2NDk0MzIyNTB9.JmF9NujFZnln7A-ynNpeyayGBqmR5poAyACYV6RnSQY
{ {
@ -45,15 +45,50 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6M
### ###
DELETE http://localhost:3000/api/v1/channels/1 HTTP/1.1 DELETE http://localhost:3000/api/v1/channels/1 HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NzAwMTE0LCJleHAiOjE2NDk4NzI5MTR9.EOn8MBHZLCxfU5fHc0ZY2x9p3y-_RdD7X915L1B6Ftc
#Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5MjU5NDUwLCJleHAiOjE2NDk0MzIyNTB9.JmF9NujFZnln7A-ynNpeyayGBqmR5poAyACYV6RnSQY #Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5MjU5NDUwLCJleHAiOjE2NDk0MzIyNTB9.JmF9NujFZnln7A-ynNpeyayGBqmR5poAyACYV6RnSQY
### ###
GET http://localhost:3000/api/v1/channels/1 HTTP/1.1 GET http://localhost:3000/api/v1/channels/1 HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NzAwMTE0LCJleHAiOjE2NDk4NzI5MTR9.EOn8MBHZLCxfU5fHc0ZY2x9p3y-_RdD7X915L1B6Ftc
### ###
GET http://localhost:3000/api/v1/channels HTTP/1.1 GET http://localhost:3000/api/v1/channels HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NzAwMTE0LCJleHAiOjE2NDk4NzI5MTR9.EOn8MBHZLCxfU5fHc0ZY2x9p3y-_RdD7X915L1B6Ftc
###
POST http://localhost:3000/api/v1/channels/5/messages HTTP/1.1
content-type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NzAwMTE0LCJleHAiOjE2NDk4NzI5MTR9.EOn8MBHZLCxfU5fHc0ZY2x9p3y-_RdD7X915L1B6Ftc
{
"content": "i hate cheese"
}
###
GET http://localhost:3000/api/v1/channels/5/messages HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NzAwMTE0LCJleHAiOjE2NDk4NzI5MTR9.EOn8MBHZLCxfU5fHc0ZY2x9p3y-_RdD7X915L1B6Ftc
###
PUT http://localhost:3000/api/v1/messages/2 HTTP/1.1
content-type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NzAwMTE0LCJleHAiOjE2NDk4NzI5MTR9.EOn8MBHZLCxfU5fHc0ZY2x9p3y-_RdD7X915L1B6Ftc
{
"content": "hello again!"
}
###
GET http://localhost:3000/api/v1/messages/2
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NzAwMTE0LCJleHAiOjE2NDk4NzI5MTR9.EOn8MBHZLCxfU5fHc0ZY2x9p3y-_RdD7X915L1B6Ftc
###
DELETE http://localhost:3000/api/v1/messages/2 HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NzAwMTE0LCJleHAiOjE2NDk4NzI5MTR9.EOn8MBHZLCxfU5fHc0ZY2x9p3y-_RdD7X915L1B6Ftc