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
No known key found for this signature in database
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(
id SERIAL PRIMARY KEY,
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,
Ping,
ChannelCreate = 1000,
ChannelCreate = 110,
ChannelUpdate,
ChannelDelete
ChannelDelete,
MessageCreate = 120,
MessageUpdate,
MessageDelete
}

View file

@ -55,8 +55,10 @@ function clientUnsubscribeAll(ws: WebSocket) {
export function dispatch(channel: string, message: GatewayPayload) {
const members = dispatchChannels.get(channel);
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 }) {

View file

@ -135,6 +135,11 @@ router.get(
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, name, owner_id FROM channels WHERE id = $1", [id]);
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;

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 usersRouter from "./routes/api/v1/users";
import channelsRouter from "./routes/api/v1/channels";
import messagesRouter from "./routes/api/v1/messages";
export default function(app: Application) {
app.get("/", (req, res) => res.send("hello!"));
@ -8,4 +9,5 @@ export default function(app: Application) {
app.use(json());
app.use("/api/v1/users", usersRouter);
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
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NzAwMTE0LCJleHAiOjE2NDk4NzI5MTR9.EOn8MBHZLCxfU5fHc0ZY2x9p3y-_RdD7X915L1B6Ftc
###
POST http://localhost:3000/api/v1/channels HTTP/1.1
content-type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NzAwMTE0LCJleHAiOjE2NDk4NzI5MTR9.EOn8MBHZLCxfU5fHc0ZY2x9p3y-_RdD7X915L1B6Ftc
{
"name": "yet another channel"
@ -35,7 +35,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6M
PUT http://localhost:3000/api/v1/channels/5 HTTP/1.1
content-type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NzAwMTE0LCJleHAiOjE2NDk4NzI5MTR9.EOn8MBHZLCxfU5fHc0ZY2x9p3y-_RdD7X915L1B6Ftc
#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
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NTI0NDA1LCJleHAiOjE2NDk2OTcyMDV9.4nIDs0K8MCT18GsdlKdicT_GK2KbEqi_P7cND3_aZvE
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5NzAwMTE0LCJleHAiOjE2NDk4NzI5MTR9.EOn8MBHZLCxfU5fHc0ZY2x9p3y-_RdD7X915L1B6Ftc
#Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwidHlwZSI6MSwiaWF0IjoxNjQ5MjU5NDUwLCJleHAiOjE2NDk0MzIyNTB9.JmF9NujFZnln7A-ynNpeyayGBqmR5poAyACYV6RnSQY
###
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
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