add message
resource crud and gateway events (messages + message history)
This commit is contained in:
parent
ce9d331bc6
commit
6a6cc1aafd
7 changed files with 261 additions and 11 deletions
|
@ -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
|
||||
);
|
||||
`));
|
||||
}
|
|
@ -4,7 +4,11 @@ export enum GatewayPayloadType {
|
|||
Ready,
|
||||
Ping,
|
||||
|
||||
ChannelCreate = 1000,
|
||||
ChannelCreate = 110,
|
||||
ChannelUpdate,
|
||||
ChannelDelete
|
||||
ChannelDelete,
|
||||
|
||||
MessageCreate = 120,
|
||||
MessageUpdate,
|
||||
MessageDelete
|
||||
}
|
||||
|
|
|
@ -56,7 +56,9 @@ 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 }) {
|
||||
|
|
|
@ -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;
|
126
src/routes/api/v1/messages.ts
Normal file
126
src/routes/api/v1/messages.ts
Normal 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;
|
|
@ -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);
|
||||
};
|
||||
|
|
47
test.rest
47
test.rest
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue