From 50b0164c9b0753abb50ab6cbd180c6c33b6abf47 Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 02:44:50 -0400 Subject: [PATCH 01/26] First schema iteration --- concord-server/package-lock.json | 390 +++++++++++++++++++++++++++++++ concord-server/package.json | 3 +- concord-server/schema.prisma | 113 +++++++++ 3 files changed, 505 insertions(+), 1 deletion(-) create mode 100644 concord-server/package-lock.json create mode 100644 concord-server/schema.prisma diff --git a/concord-server/package-lock.json b/concord-server/package-lock.json new file mode 100644 index 0000000..cef0762 --- /dev/null +++ b/concord-server/package-lock.json @@ -0,0 +1,390 @@ +{ + "name": "concord-server", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "concord-server", + "dependencies": { + "@prisma/client": "^6.16.2", + "hono": "^4.9.9", + "prisma": "^6.16.2" + }, + "devDependencies": { + "@types/bun": "latest" + } + }, + "node_modules/@prisma/client": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.2.tgz", + "integrity": "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.16.2", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.16.12", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.16.2", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.16.2", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.2", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/fetch-engine": "6.16.2", + "@prisma/get-platform": "6.16.2" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.16.2", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.2", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/get-platform": "6.16.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.16.2", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.2" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/@types/bun": { + "version": "1.2.22", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.22.tgz", + "integrity": "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.2.22" + } + }, + "node_modules/@types/node": { + "version": "24.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.12.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.14", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/bun-types": { + "version": "1.2.22", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "peerDependencies": { + "@types/react": "^19" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/confbox": { + "version": "0.2.2", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "16.6.1", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/effect": { + "version": "3.16.12", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/hono": { + "version": "4.9.9", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/jiti": { + "version": "2.6.0", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "license": "MIT" + }, + "node_modules/nypm": { + "version": "0.6.2", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/prisma": { + "version": "6.16.2", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@prisma/config": "6.16.2", + "@prisma/engines": "6.16.2" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/rc9": { + "version": "2.1.2", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/tinyexec": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.12.0", + "dev": true, + "license": "MIT" + } + } +} diff --git a/concord-server/package.json b/concord-server/package.json index 77eee4a..42fc858 100644 --- a/concord-server/package.json +++ b/concord-server/package.json @@ -4,10 +4,11 @@ "dev": "bun run --hot src/index.ts" }, "dependencies": { + "@prisma/client": "^6.16.2", "hono": "^4.9.9", "prisma": "^6.16.2" }, "devDependencies": { "@types/bun": "latest" } -} \ No newline at end of file +} diff --git a/concord-server/schema.prisma b/concord-server/schema.prisma new file mode 100644 index 0000000..dc00572 --- /dev/null +++ b/concord-server/schema.prisma @@ -0,0 +1,113 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Hello { + id String @id @default(uuid(7)) + names Thing[] +} + +model Thing { + id BigInt @id @default(autoincrement()) + txt String + Hello Hello? @relation(fields: [helloId], references: [id]) + helloId String? +} + +model Instance { + id String @id @default(uuid(7)) + name String + icon String? + Role Role[] + Category Category[] +} + +model User { + id String @id @default(uuid(7)) + username String + nickname String? + bio String? + picture String? + banner String? + admin Boolean + status String // online/offline/dnd/idle/invis + Role Role[] + UserAuth UserAuth? + Message Message[] + MessagePing MessagePing[] +} + +model Role { + User User @relation(fields: [userId], references: [id]) + userId String + Instance Instance @relation(fields: [instanceId], references: [id]) + instanceId String + + @@unique([userId, instanceId]) +} + +model UserAuth { + User User @relation(fields: [userId], references: [id]) + userId String + password String // HASHED PASSWORD AS STRING USING SHA-256 + token String? // current user token + + @@unique([userId]) +} + +model Category { + id String @id @default(uuid(7)) + Instance Instance? @relation(fields: [instanceId], references: [id]) + instanceId String? + name String + position Int + Channel Channel[] + Message Message[] +} + +model Channel { + id String @id @default(uuid(7)) + type String + Category Category? @relation(fields: [categoryId], references: [id]) + categoryId String? + name String + description String + Message Message[] +} + +model Message { + id String @id @default(uuid(7)) + Channel Channel? @relation(fields: [channelId], references: [id]) + channelId String? + Category Category? @relation(fields: [categoryId], references: [id]) + categoryId String? + User User? @relation(fields: [userId], references: [id]) + userId String? + text String + replies Reply[] @relation("MessageToReply") + repliedTo Reply[] @relation("ReplyToMessage") + MessagePing MessagePing[] +} + +model Reply { + message Message @relation("MessageToReply", fields: [messageId], references: [id]) + messageId String + repliesTo Message @relation("ReplyToMessage", fields: [repliesToId], references: [id]) + repliesToId String + + @@unique([messageId, repliesToId]) +} + +model MessagePing { + Message Message @relation(fields: [messageId], references: [id]) + messageId String + PingsUser User @relation(fields: [pingsUserId], references: [id]) + pingsUserId String + + @@unique([messageId, pingsUserId]) +} From 58abd3c241b5e1eaf61fabf284931b16177459c7 Mon Sep 17 00:00:00 2001 From: PrimarchPaul Date: Sat, 27 Sep 2025 02:52:38 -0400 Subject: [PATCH 02/26] added actions, routes, and socketio bun engine --- concord-server/src/index.ts | 50 ++++++++++++++++++++++++-- concord-server/src/routes/actions.ts | 27 ++++++++++++++ concord-server/src/routes/index.ts | 11 ++++++ concord-server/src/services/actions.ts | 50 ++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 concord-server/src/routes/actions.ts create mode 100644 concord-server/src/routes/index.ts create mode 100644 concord-server/src/services/actions.ts diff --git a/concord-server/src/index.ts b/concord-server/src/index.ts index 3191383..da9f8a6 100644 --- a/concord-server/src/index.ts +++ b/concord-server/src/index.ts @@ -1,9 +1,53 @@ import { Hono } from 'hono' +import { cors } from 'hono/cors' +import { Server as Engine } from '@socket.io/bun-engine' +import { Server } from 'socket.io' +import routes from './routes/index' + +//initialize socket.io server +const io = new Server() + +//initialize bun engine +//then bind to socket.io server +const engine = new Engine() +io.bind(engine) + +io.on('connection', (socket) => { + + //get userId and clientId from query params + const userId = socket.handshake.query.userId; + const clientId = socket.handshake.query.clientId; + if(!userId || Array.isArray(userId)){ + socket.disconnect(); + throw new Error('Invalid user ID'); + } + + if(!clientId || Array.isArray(clientId)){ + socket.disconnect() + throw new Error('Invalid client ID') + } + + socket.join(userId) + console.log(`User ${userId} connected. Client ID ${clientId} on socket ${socket.id}`); + + socket.on('disconnect', () => { + console.log(`User ${userId} disconnected from socket ${socket.id}`); + }); +}); const app = new Hono() -app.get('/', (c) => { - return c.text('Hello Hono!') -}) +app.use('*', cors({ + origin: 'http://localhost:5173', + allowHeaders: ['Content-Type', 'Authorization'], + allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + credentials: true +})) +app.use(routes) + + + + + export default app diff --git a/concord-server/src/routes/actions.ts b/concord-server/src/routes/actions.ts new file mode 100644 index 0000000..68fa198 --- /dev/null +++ b/concord-server/src/routes/actions.ts @@ -0,0 +1,27 @@ +import { Hono } from "hono" +const actions = new Hono() + + +actions.post("actions/message/:id", (c) => { + //todo: pass service function to send a message to user id +}) + +actions.delete("actions/message/delete/:id", (c) => { + //todo: pass service function to delete a message by user id +}) + +actions.get("actions/message/all/:userId", (c) => { + //todo: pass service function to fetch all messages f a user +}) + +actions.get("actions/:id", (c) => { + //todo: pass service function to fetch user data by id +}) + +actions.get("actions/userChannels/:id", (c) => { + //todo: pass service function to fetch all channels the id is part of +}) + + + +export default actions \ No newline at end of file diff --git a/concord-server/src/routes/index.ts b/concord-server/src/routes/index.ts new file mode 100644 index 0000000..4ba151a --- /dev/null +++ b/concord-server/src/routes/index.ts @@ -0,0 +1,11 @@ +//place exported routes below this line +import { Hono } from 'hono'; +import actions from './actions'; + + +const routes = new Hono(); + +routes.route("/", actions) + + +export default routes; \ No newline at end of file diff --git a/concord-server/src/services/actions.ts b/concord-server/src/services/actions.ts new file mode 100644 index 0000000..879d60a --- /dev/null +++ b/concord-server/src/services/actions.ts @@ -0,0 +1,50 @@ +//send a ping to a user + +export async function getUserChannels(userId: string): Promise { + try{ + if(!userId){ + throw new Error('missing userId'); + } + + //TODO: add some prisma code here + //to fetch all the channels a user is part of + + + return ["test1","test2"]; + }catch(err){ + const errMessage = err as Error + + if(errMessage.message === 'missing userId'){ + console.log('services::actions::getUserChannels - missing userId'); + return null; + } + + console.log('services::actions::getUserChannels - unknown error', errMessage); + return null; + } +} + +export async function getUserInformation(userId: string): Promise<{id: string, name: string} | null> { + try{ + if(!userId){ + throw new Error('missing userId'); + } + + //TODO: add some prisma code here + //to fetch user information + + return {id: userId, name: "Test User"}; + }catch(err){ + const errMessage = err as Error + + if(errMessage.message === 'missing userId'){ + console.log('services::actions::getUserInformation - missing userId'); + return null; + } + + console.log('services::actions::getUserInformation - unknown error', errMessage); + return null; + } +} + +export async function getAllMessage(userId: string): Promise<{} From 77d71d7e084500537d5cec1dae59014c681add5c Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 03:13:33 -0400 Subject: [PATCH 03/26] Prisma type generation ; Add deleted flag to message --- concord-server/bun.lock | 3 +++ concord-server/schema.prisma | 1 + 2 files changed, 4 insertions(+) diff --git a/concord-server/bun.lock b/concord-server/bun.lock index 365d51b..0512dc8 100644 --- a/concord-server/bun.lock +++ b/concord-server/bun.lock @@ -4,6 +4,7 @@ "": { "name": "concord-server", "dependencies": { + "@prisma/client": "^6.16.2", "hono": "^4.9.9", "prisma": "^6.16.2", }, @@ -13,6 +14,8 @@ }, }, "packages": { + "@prisma/client": ["@prisma/client@6.16.2", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw=="], + "@prisma/config": ["@prisma/config@6.16.2", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.16.12", "empathic": "2.0.0" } }, "sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg=="], "@prisma/debug": ["@prisma/debug@6.16.2", "", {}, "sha512-bo4/gA/HVV6u8YK2uY6glhNsJ7r+k/i5iQ9ny/3q5bt9ijCj7WMPUwfTKPvtEgLP+/r26Z686ly11hhcLiQ8zA=="], diff --git a/concord-server/schema.prisma b/concord-server/schema.prisma index dc00572..e82aeca 100644 --- a/concord-server/schema.prisma +++ b/concord-server/schema.prisma @@ -88,6 +88,7 @@ model Message { categoryId String? User User? @relation(fields: [userId], references: [id]) userId String? + deleted Boolean text String replies Reply[] @relation("MessageToReply") repliedTo Reply[] @relation("ReplyToMessage") From f71e639d492bc67f92ae08b6aab8f5a4fb89dc4f Mon Sep 17 00:00:00 2001 From: PrimarchPaul Date: Sat, 27 Sep 2025 04:15:51 -0400 Subject: [PATCH 04/26] fix: app.route overload --- concord-server/src/controller/actions.ts | 6 ++ concord-server/src/index.ts | 11 +-- concord-server/src/routes/actions.ts | 17 ++++- concord-server/src/services/actions.ts | 96 ++++++++++++++++++++++-- concord-server/src/validators/actions.ts | 3 + 5 files changed, 117 insertions(+), 16 deletions(-) create mode 100644 concord-server/src/controller/actions.ts create mode 100644 concord-server/src/validators/actions.ts diff --git a/concord-server/src/controller/actions.ts b/concord-server/src/controller/actions.ts new file mode 100644 index 0000000..b1dc7fc --- /dev/null +++ b/concord-server/src/controller/actions.ts @@ -0,0 +1,6 @@ +import { getUserInformation } from "../services/actions"; + + +export async function fetchUserData(id: string) { + return await getUserInformation(id); +} \ No newline at end of file diff --git a/concord-server/src/index.ts b/concord-server/src/index.ts index da9f8a6..d47ea7e 100644 --- a/concord-server/src/index.ts +++ b/concord-server/src/index.ts @@ -42,12 +42,9 @@ app.use('*', cors({ allowHeaders: ['Content-Type', 'Authorization'], allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], credentials: true -})) -app.use(routes) +}) + +app.route('/api', routes) - - - - -export default app +export default app; diff --git a/concord-server/src/routes/actions.ts b/concord-server/src/routes/actions.ts index 68fa198..d2ca9de 100644 --- a/concord-server/src/routes/actions.ts +++ b/concord-server/src/routes/actions.ts @@ -1,7 +1,8 @@ import { Hono } from "hono" +import { fetchUserData } from "../controller/actions" const actions = new Hono() - +/* actions.post("actions/message/:id", (c) => { //todo: pass service function to send a message to user id }) @@ -14,14 +15,22 @@ actions.get("actions/message/all/:userId", (c) => { //todo: pass service function to fetch all messages f a user }) -actions.get("actions/:id", (c) => { - //todo: pass service function to fetch user data by id -}) + actions.get("actions/userChannels/:id", (c) => { //todo: pass service function to fetch all channels the id is part of }) +*/ +actions.get("actions/:id", async (c) => { + const id = c.req.param("id"); + const userData = await fetchUserData(id); + if (userData) { + return c.json(userData); + } else { + return c.json({ error: "User not found" }, 404); + } +}) export default actions \ No newline at end of file diff --git a/concord-server/src/services/actions.ts b/concord-server/src/services/actions.ts index 879d60a..797f85a 100644 --- a/concord-server/src/services/actions.ts +++ b/concord-server/src/services/actions.ts @@ -1,4 +1,4 @@ -//send a ping to a user +import { Message, MessagePing, Role, UserAuth } from "@prisma/client"; export async function getUserChannels(userId: string): Promise { try{ @@ -24,16 +24,42 @@ export async function getUserChannels(userId: string): Promise } } -export async function getUserInformation(userId: string): Promise<{id: string, name: string} | null> { +export async function getUserInformation(userId: string): + Promise<{ + id: string, + userName: string, + nickName?: string, + bio?: string, + picture?: string, + banner?: string, + admin: boolean, + status: 'online' | 'offline' | 'dnd' | 'idle' | 'invis', + role: Role[], + userAuth?: UserAuth, + messages: Message[], + MessagePing: MessagePing[] +} | null> { try{ if(!userId){ throw new Error('missing userId'); } - //TODO: add some prisma code here //to fetch user information - return {id: userId, name: "Test User"}; + return { + id: userId, + userName: "Test User", + bio: "this is a bio", + picture: "https://www.goodhousekeeping.com/life/pets/g61070837/cutest-cat-breeds/", + banner: "https://www.goodhousekeeping.com/life/pets/g61070837/cutest-cat-breeds/", + admin: false, + status: "offline", + role: [], + userAuth: undefined, + messages: [], + MessagePing: [] + }; + }catch(err){ const errMessage = err as Error @@ -47,4 +73,64 @@ export async function getUserInformation(userId: string): Promise<{id: string, n } } -export async function getAllMessage(userId: string): Promise<{} +export async function getAllMessage(userId: string): Promise<{id: string, content: string, senderId: string, receiverId: string}[] | null> { + try{ + if(!userId){ + throw new Error('missing userId'); + } + + //TODO: add some prisma code here + //to fetch all messages for a user + + return [ + {id: "1", content: "Hello", senderId: userId, receiverId: "2"}, + {id: "2", content: "Hi", senderId: "2", receiverId: userId} + ]; + }catch(err){ + const errMessage = err as Error + + if(errMessage.message === 'missing userId'){ + console.log('services::actions::getAllMessage - missing userId'); + return null; + } + + console.log('services::actions::getAllMessage - unknown error', errMessage); + return null; + } +} + + +export async function sendMessage(userId: string, content: string): Promise { + try{ + if(!userId){ + throw new Error('missing userId'); + } + + if(!content){ + throw new Error('missing content'); + } + + //TODO: add some prisma code here + //to save a message + + //todo: add some socketio code to + //emit the message to the receiver + + return true; + }catch(err){ + const errMessage = err as Error + + if(errMessage.message === 'missing userId'){ + console.log('services::actions::sendMessage - missing userId'); + return false; + } + + if(errMessage.message === 'missing content'){ + console.log('services::actions::sendMessage - missing content'); + return false; + } + + console.log('services::actions::sendMessage - unknown error', errMessage); + return false; + } +} diff --git a/concord-server/src/validators/actions.ts b/concord-server/src/validators/actions.ts new file mode 100644 index 0000000..55d73ea --- /dev/null +++ b/concord-server/src/validators/actions.ts @@ -0,0 +1,3 @@ +import { zValidator } from "@hono/zod-validator" + +export const \ No newline at end of file From f80c522bb8b16eb97201380f68c748c44e8754c9 Mon Sep 17 00:00:00 2001 From: PrimarchPaul Date: Sat, 27 Sep 2025 04:18:44 -0400 Subject: [PATCH 05/26] fixed: app.route overload --- concord-server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concord-server/src/index.ts b/concord-server/src/index.ts index d47ea7e..16f7ec9 100644 --- a/concord-server/src/index.ts +++ b/concord-server/src/index.ts @@ -42,7 +42,7 @@ app.use('*', cors({ allowHeaders: ['Content-Type', 'Authorization'], allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], credentials: true -}) +})) app.route('/api', routes) From 45f7e310f6e5e33a72c8d60a43d05d3a024e61ce Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 04:20:38 -0400 Subject: [PATCH 06/26] add the buns --- concord-server/bun.lock | 44 +++++++++++++++++++++++++++++++++++++ concord-server/package.json | 4 +++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/concord-server/bun.lock b/concord-server/bun.lock index 0512dc8..73939b3 100644 --- a/concord-server/bun.lock +++ b/concord-server/bun.lock @@ -5,8 +5,10 @@ "name": "concord-server", "dependencies": { "@prisma/client": "^6.16.2", + "@socket.io/bun-engine": "^0.0.3", "hono": "^4.9.9", "prisma": "^6.16.2", + "socket.io": "^4.8.1", }, "devDependencies": { "@types/bun": "latest", @@ -28,14 +30,24 @@ "@prisma/get-platform": ["@prisma/get-platform@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2" } }, "sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA=="], + "@socket.io/bun-engine": ["@socket.io/bun-engine@0.0.3", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-OK2ZObq9hKsxyAcV7xDTcWGubBmfEY3Lt4nb04K+HlYl9G5PDgrY9hxJm9uV+B0xo3MhKFrgdg9VQsVZ3pbT/g=="], + + "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], + "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], "@types/react": ["@types/react@19.1.14", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-ukd93VGzaNPMAUPy0gRDSC57UuQbnH9Kussp7HBjM06YFi9uZTFhOvMSO2OKqXm1rSgzOE+pVx1k1PYHGwlc8Q=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], + "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], @@ -48,8 +60,14 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], @@ -62,6 +80,10 @@ "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + "engine.io": ["engine.io@6.6.4", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1" } }, "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g=="], + + "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], + "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], @@ -72,10 +94,20 @@ "jiti": ["jiti@2.6.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -92,8 +124,20 @@ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "socket.io": ["socket.io@4.8.1", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg=="], + + "socket.io-adapter": ["socket.io-adapter@2.5.5", "", { "dependencies": { "debug": "~4.3.4", "ws": "~8.17.1" } }, "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg=="], + + "socket.io-parser": ["socket.io-parser@4.2.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" } }, "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew=="], + "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], } } diff --git a/concord-server/package.json b/concord-server/package.json index 42fc858..7b14759 100644 --- a/concord-server/package.json +++ b/concord-server/package.json @@ -5,8 +5,10 @@ }, "dependencies": { "@prisma/client": "^6.16.2", + "@socket.io/bun-engine": "^0.0.3", "hono": "^4.9.9", - "prisma": "^6.16.2" + "prisma": "^6.16.2", + "socket.io": "^4.8.1" }, "devDependencies": { "@types/bun": "latest" From 122cfdd8c329d610c034e776e479d758ceb29cd7 Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 04:52:32 -0400 Subject: [PATCH 07/26] Add zod deps + fix user get by id method --- concord-server/bun.lock | 68 ++++++++++++++++++++++++++ concord-server/package.json | 4 +- concord-server/schema.prisma | 14 +----- concord-server/src/services/actions.ts | 55 ++++++++++++++------- 4 files changed, 108 insertions(+), 33 deletions(-) diff --git a/concord-server/bun.lock b/concord-server/bun.lock index 73939b3..c36e117 100644 --- a/concord-server/bun.lock +++ b/concord-server/bun.lock @@ -4,6 +4,7 @@ "": { "name": "concord-server", "dependencies": { + "@hono/zod-validator": "^0.7.3", "@prisma/client": "^6.16.2", "@socket.io/bun-engine": "^0.0.3", "hono": "^4.9.9", @@ -12,24 +13,43 @@ }, "devDependencies": { "@types/bun": "latest", + "prisma-zod-generator": "^1.22.1", }, }, }, "packages": { + "@hono/zod-validator": ["@hono/zod-validator@0.7.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-uYGdgVib3RlGD698WR5dVM0zB3UuPY5vHKXffGUbUh7r4xY+mFIhF3/v4AcQVLrU5CQdBso8BJr4wuVoCrjTuQ=="], + "@prisma/client": ["@prisma/client@6.16.2", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw=="], "@prisma/config": ["@prisma/config@6.16.2", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.16.12", "empathic": "2.0.0" } }, "sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg=="], "@prisma/debug": ["@prisma/debug@6.16.2", "", {}, "sha512-bo4/gA/HVV6u8YK2uY6glhNsJ7r+k/i5iQ9ny/3q5bt9ijCj7WMPUwfTKPvtEgLP+/r26Z686ly11hhcLiQ8zA=="], + "@prisma/dmmf": ["@prisma/dmmf@6.16.2", "", {}, "sha512-o9ztgdbj2KZXl6DL+oP56TTC0poTLPns9/MeU761b49E1IQ/fd0jwdov1bidlNOiwio8Nsou23xNrYE/db10aA=="], + + "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2" } }, "sha512-DMgfafnG0zPd+QoAQOC0Trn1xlb0fVAfQi2MpkpzSf641KiVkVPkJRXDSbcTbxGxO2HRdd0vI9U6LlesWad4XA=="], + "@prisma/engines": ["@prisma/engines@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2", "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/fetch-engine": "6.16.2", "@prisma/get-platform": "6.16.2" } }, "sha512-7yf3AjfPUgsg/l7JSu1iEhsmZZ/YE00yURPjTikqm2z4btM0bCl2coFtTGfeSOWbQMmq45Jab+53yGUIAT1sjA=="], "@prisma/engines-version": ["@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "", {}, "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA=="], "@prisma/fetch-engine": ["@prisma/fetch-engine@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2", "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/get-platform": "6.16.2" } }, "sha512-wPnZ8DMRqpgzye758ZvfAMiNJRuYpz+rhgEBZi60ZqDIgOU2694oJxiuu3GKFeYeR/hXxso4/2oBC243t/whxQ=="], + "@prisma/generator": ["@prisma/generator@6.16.2", "", {}, "sha512-7bwRmtMIgfe1rUynh1p9VlmYvEiidbRO6aBphPBS6YGEGSvNe8+QExbRpsqFlFBvIX76BhZCxuEj7ZwALMYRKQ=="], + + "@prisma/generator-helper": ["@prisma/generator-helper@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2", "@prisma/dmmf": "6.16.2", "@prisma/generator": "6.16.2" } }, "sha512-8tVnWM8ETJNrvI5CT9eKCW23+aPLNkidC+g9NJn7ghXm60Q7GGlLX5tmvn5dE8tXvs/FSX3MN7KNmNJpOr89Hw=="], + "@prisma/get-platform": ["@prisma/get-platform@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2" } }, "sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA=="], + "@prisma/internals": ["@prisma/internals@6.16.2", "", { "dependencies": { "@prisma/config": "6.16.2", "@prisma/debug": "6.16.2", "@prisma/dmmf": "6.16.2", "@prisma/driver-adapter-utils": "6.16.2", "@prisma/engines": "6.16.2", "@prisma/fetch-engine": "6.16.2", "@prisma/generator": "6.16.2", "@prisma/generator-helper": "6.16.2", "@prisma/get-platform": "6.16.2", "@prisma/prisma-schema-wasm": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/schema-engine-wasm": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/schema-files-loader": "6.16.2", "arg": "5.0.2", "prompts": "2.4.2" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-gwmWl7H8iTbi+58RXua5Lsus5LDbIZGO2wQ4RoSX9YtEbKWHwRP8TUzTVLwRNeJ2DHwfnzhTLrUnybwotqiACg=="], + + "@prisma/prisma-schema-wasm": ["@prisma/prisma-schema-wasm@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "", {}, "sha512-DvYi0zKqzPd49Z5japS3FawyMylscaoUmlXNhnRAXb8HZryG4Q7TM1FLX8OIAfCgLmoWS1c/Zf4UZznBXkvWSg=="], + + "@prisma/schema-engine-wasm": ["@prisma/schema-engine-wasm@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "", {}, "sha512-HDgFE0um5OHkk2pkQbAgARR284i2VpoM+7GYRAT0zxoTagsdaZ6yquJF2LEZuAKfibib0Ct7JZxRCB8eN/Ru6g=="], + + "@prisma/schema-files-loader": ["@prisma/schema-files-loader@6.16.2", "", { "dependencies": { "@prisma/prisma-schema-wasm": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "fs-extra": "11.3.0" } }, "sha512-TN77DUFgOxT/WL6wuxVhn7qVDvwVRl0TEzhFfRh5vKQsuZ5itLzA7Ki4TgOs4Pk18wwZnti6ZKdzR3Y7cu2KsA=="], + "@socket.io/bun-engine": ["@socket.io/bun-engine@0.0.3", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-OK2ZObq9hKsxyAcV7xDTcWGubBmfEY3Lt4nb04K+HlYl9G5PDgrY9hxJm9uV+B0xo3MhKFrgdg9VQsVZ3pbT/g=="], "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], @@ -46,6 +66,12 @@ "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], @@ -66,6 +92,8 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], @@ -88,12 +116,30 @@ "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "hono": ["hono@4.9.9", "", {}, "sha512-Hxw4wT6zjJGZJdkJzAx9PyBdf7ZpxaTSA0NfxqjLghwMrLBX8p33hJBzoETRakF3UJu6OdNQBZAlNSkGqKFukw=="], "jiti": ["jiti@2.6.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -102,6 +148,10 @@ "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="], @@ -116,14 +166,24 @@ "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + "prisma": ["prisma@6.16.2", "", { "dependencies": { "@prisma/config": "6.16.2", "@prisma/engines": "6.16.2" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA=="], + "prisma-zod-generator": ["prisma-zod-generator@1.22.1", "", { "dependencies": { "@prisma/client": "^6.16.2", "@prisma/generator-helper": "^6.16.2", "@prisma/internals": "^6.16.2", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "node-fetch": "^3.3.2", "prettier": "^3.6.2", "tslib": "^2.8.1" }, "peerDependencies": { "zod": ">=3.25.0 <5" }, "bin": { "prisma-zod-generator": "lib/generator.js" } }, "sha512-nBr00sfR8onGCD5eIDLHoFrpeJTSuZxSeaO61Zg6CAEyXPR51gpkO1ev9huG7+tsV+mm8me8VNl8hMcVtWl8FA=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "socket.io": ["socket.io@4.8.1", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg=="], "socket.io-adapter": ["socket.io-adapter@2.5.5", "", { "dependencies": { "debug": "~4.3.4", "ws": "~8.17.1" } }, "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg=="], @@ -132,12 +192,20 @@ "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], + + "zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], } } diff --git a/concord-server/package.json b/concord-server/package.json index 7b14759..61d3f41 100644 --- a/concord-server/package.json +++ b/concord-server/package.json @@ -4,6 +4,7 @@ "dev": "bun run --hot src/index.ts" }, "dependencies": { + "@hono/zod-validator": "^0.7.3", "@prisma/client": "^6.16.2", "@socket.io/bun-engine": "^0.0.3", "hono": "^4.9.9", @@ -11,6 +12,7 @@ "socket.io": "^4.8.1" }, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "prisma-zod-generator": "^1.22.1" } } diff --git a/concord-server/schema.prisma b/concord-server/schema.prisma index e82aeca..9c2688f 100644 --- a/concord-server/schema.prisma +++ b/concord-server/schema.prisma @@ -1,5 +1,5 @@ generator client { - provider = "prisma-client-js" + provider = "prisma-zod-generator" } datasource db { @@ -7,18 +7,6 @@ datasource db { url = env("DATABASE_URL") } -model Hello { - id String @id @default(uuid(7)) - names Thing[] -} - -model Thing { - id BigInt @id @default(autoincrement()) - txt String - Hello Hello? @relation(fields: [helloId], references: [id]) - helloId String? -} - model Instance { id String @id @default(uuid(7)) name String diff --git a/concord-server/src/services/actions.ts b/concord-server/src/services/actions.ts index 797f85a..1d88458 100644 --- a/concord-server/src/services/actions.ts +++ b/concord-server/src/services/actions.ts @@ -1,4 +1,6 @@ -import { Message, MessagePing, Role, UserAuth } from "@prisma/client"; +import { Message, MessagePing, PrismaClient, Role, UserAuth } from "@prisma/client"; + +const prisma = new PrismaClient(); export async function getUserChannels(userId: string): Promise { try{ @@ -28,36 +30,46 @@ export async function getUserInformation(userId: string): Promise<{ id: string, userName: string, - nickName?: string, - bio?: string, - picture?: string, - banner?: string, + nickName: string | null, + bio: string | null, + picture: string | null, + banner: string | null, admin: boolean, status: 'online' | 'offline' | 'dnd' | 'idle' | 'invis', role: Role[], userAuth?: UserAuth, - messages: Message[], - MessagePing: MessagePing[] } | null> { try{ if(!userId){ throw new Error('missing userId'); } - //TODO: add some prisma code here - //to fetch user information + + const user = await prisma.user.findUnique({ + where: { + id: userId + } + }) + + if (!user) { + throw new Error('could not find user'); + } + + const userRoles = await prisma.role.findMany({ + where: { + userId: userId + } + }) return { id: userId, - userName: "Test User", - bio: "this is a bio", - picture: "https://www.goodhousekeeping.com/life/pets/g61070837/cutest-cat-breeds/", - banner: "https://www.goodhousekeeping.com/life/pets/g61070837/cutest-cat-breeds/", - admin: false, - status: "offline", - role: [], - userAuth: undefined, - messages: [], - MessagePing: [] + userName: user.username, + nickName: user.nickname, + bio: user.bio, + picture: user.picture, + banner: user.banner, + admin: user.admin, + status: (["online", "offline", "dnd", "idle", "invis"] as const).includes(user.status as any) ? user.status as 'online' | 'offline' | 'dnd' | 'idle' | 'invis' : 'offline', + role: userRoles, }; }catch(err){ @@ -68,6 +80,11 @@ export async function getUserInformation(userId: string): return null; } + if(errMessage.message === 'could not find user'){ + console.log('services::actions::getUserInformation - unable to find user'); + return null; + } + console.log('services::actions::getUserInformation - unknown error', errMessage); return null; } From b22734831efccca4c6ad6a2cbed04cb93f59e15f Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 11:13:37 -0400 Subject: [PATCH 08/26] Created some user endpoints ; Pretty code --- concord-server/.editorconfig | 10 ++ concord-server/README.md | 2 + concord-server/src/controller/actions.ts | 6 - .../src/controller/userController.ts | 9 ++ concord-server/src/index.ts | 73 +++++---- concord-server/src/routes/actions.ts | 36 ----- concord-server/src/routes/index.ts | 10 +- concord-server/src/routes/userRoutes.ts | 25 +++ concord-server/src/services/actions.ts | 153 ------------------ concord-server/src/services/userService.ts | 151 +++++++++++++++++ concord-server/src/validators/actions.ts | 3 - concord-server/tsconfig.json | 2 +- 12 files changed, 240 insertions(+), 240 deletions(-) create mode 100644 concord-server/.editorconfig delete mode 100644 concord-server/src/controller/actions.ts create mode 100644 concord-server/src/controller/userController.ts delete mode 100644 concord-server/src/routes/actions.ts create mode 100644 concord-server/src/routes/userRoutes.ts delete mode 100644 concord-server/src/services/actions.ts create mode 100644 concord-server/src/services/userService.ts delete mode 100644 concord-server/src/validators/actions.ts diff --git a/concord-server/.editorconfig b/concord-server/.editorconfig new file mode 100644 index 0000000..1e09672 --- /dev/null +++ b/concord-server/.editorconfig @@ -0,0 +1,10 @@ +# EditorConfig helps maintain consistent coding styles +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/concord-server/README.md b/concord-server/README.md index 6dd13e7..d950ba6 100644 --- a/concord-server/README.md +++ b/concord-server/README.md @@ -1,9 +1,11 @@ To install dependencies: + ```sh bun install ``` To run: + ```sh bun run dev ``` diff --git a/concord-server/src/controller/actions.ts b/concord-server/src/controller/actions.ts deleted file mode 100644 index b1dc7fc..0000000 --- a/concord-server/src/controller/actions.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { getUserInformation } from "../services/actions"; - - -export async function fetchUserData(id: string) { - return await getUserInformation(id); -} \ No newline at end of file diff --git a/concord-server/src/controller/userController.ts b/concord-server/src/controller/userController.ts new file mode 100644 index 0000000..f6fdd63 --- /dev/null +++ b/concord-server/src/controller/userController.ts @@ -0,0 +1,9 @@ +import { getAllUsersFrom, getUserInformation } from "../services/userService"; + +export async function fetchUserData(id: string) { + return await getUserInformation(id); +} + +export async function fetchAllUsers(instanceId: string) { + return await getAllUsersFrom(instanceId); +} diff --git a/concord-server/src/index.ts b/concord-server/src/index.ts index 16f7ec9..c26549d 100644 --- a/concord-server/src/index.ts +++ b/concord-server/src/index.ts @@ -1,50 +1,53 @@ -import { Hono } from 'hono' -import { cors } from 'hono/cors' -import { Server as Engine } from '@socket.io/bun-engine' -import { Server } from 'socket.io' -import routes from './routes/index' +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { Server as Engine } from "@socket.io/bun-engine"; +import { Server } from "socket.io"; +import routes from "./routes/index"; //initialize socket.io server -const io = new Server() +const io = new Server(); //initialize bun engine //then bind to socket.io server -const engine = new Engine() -io.bind(engine) +const engine = new Engine(); +io.bind(engine); -io.on('connection', (socket) => { +io.on("connection", (socket) => { + //get userId and clientId from query params + const userId = socket.handshake.query.userId; + const clientId = socket.handshake.query.clientId; + if (!userId || Array.isArray(userId)) { + socket.disconnect(); + throw new Error("Invalid user ID"); + } - //get userId and clientId from query params - const userId = socket.handshake.query.userId; - const clientId = socket.handshake.query.clientId; - if(!userId || Array.isArray(userId)){ - socket.disconnect(); - throw new Error('Invalid user ID'); - } + if (!clientId || Array.isArray(clientId)) { + socket.disconnect(); + throw new Error("Invalid client ID"); + } - if(!clientId || Array.isArray(clientId)){ - socket.disconnect() - throw new Error('Invalid client ID') - } + socket.join(userId); + console.log( + `User ${userId} connected. Client ID ${clientId} on socket ${socket.id}`, + ); - socket.join(userId) - console.log(`User ${userId} connected. Client ID ${clientId} on socket ${socket.id}`); - - socket.on('disconnect', () => { - console.log(`User ${userId} disconnected from socket ${socket.id}`); - }); + socket.on("disconnect", () => { + console.log(`User ${userId} disconnected from socket ${socket.id}`); + }); }); -const app = new Hono() +const app = new Hono(); -app.use('*', cors({ - origin: 'http://localhost:5173', - allowHeaders: ['Content-Type', 'Authorization'], - allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - credentials: true -})) - -app.route('/api', routes) +app.use( + "*", + cors({ + origin: "http://localhost:5173", + allowHeaders: ["Content-Type", "Authorization"], + allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + credentials: true, + }), +); +app.route("/api", routes); export default app; diff --git a/concord-server/src/routes/actions.ts b/concord-server/src/routes/actions.ts deleted file mode 100644 index d2ca9de..0000000 --- a/concord-server/src/routes/actions.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Hono } from "hono" -import { fetchUserData } from "../controller/actions" -const actions = new Hono() - -/* -actions.post("actions/message/:id", (c) => { - //todo: pass service function to send a message to user id -}) - -actions.delete("actions/message/delete/:id", (c) => { - //todo: pass service function to delete a message by user id -}) - -actions.get("actions/message/all/:userId", (c) => { - //todo: pass service function to fetch all messages f a user -}) - - - -actions.get("actions/userChannels/:id", (c) => { - //todo: pass service function to fetch all channels the id is part of -}) -*/ - -actions.get("actions/:id", async (c) => { - const id = c.req.param("id"); - const userData = await fetchUserData(id); - if (userData) { - return c.json(userData); - } else { - return c.json({ error: "User not found" }, 404); - } -}) - - -export default actions \ No newline at end of file diff --git a/concord-server/src/routes/index.ts b/concord-server/src/routes/index.ts index 4ba151a..550c3b3 100644 --- a/concord-server/src/routes/index.ts +++ b/concord-server/src/routes/index.ts @@ -1,11 +1,9 @@ //place exported routes below this line -import { Hono } from 'hono'; -import actions from './actions'; - +import { Hono } from "hono"; +import actions from "./userRoutes"; const routes = new Hono(); -routes.route("/", actions) +routes.route("/", actions); - -export default routes; \ No newline at end of file +export default routes; diff --git a/concord-server/src/routes/userRoutes.ts b/concord-server/src/routes/userRoutes.ts new file mode 100644 index 0000000..c7a46fa --- /dev/null +++ b/concord-server/src/routes/userRoutes.ts @@ -0,0 +1,25 @@ +import { Hono } from "hono"; +import { fetchAllUsers, fetchUserData } from "../controller/userController"; +const actions = new Hono(); + +actions.get("user/:id", async (c) => { + const id = c.req.param("id"); + const userData = await fetchUserData(id); + if (userData) { + return c.json(userData); + } else { + return c.json({ error: "User not found" }, 404); + } +}); + +actions.get("instance/:id/users", async (c) => { + const instanceId = c.req.param("id"); + const userData = await fetchAllUsers(instanceId); + if (userData) { + return c.json(userData); + } else { + return c.json({ error: "Error getting all users from instance" }, 500); + } +}); + +export default actions; diff --git a/concord-server/src/services/actions.ts b/concord-server/src/services/actions.ts deleted file mode 100644 index 1d88458..0000000 --- a/concord-server/src/services/actions.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Message, MessagePing, PrismaClient, Role, UserAuth } from "@prisma/client"; - -const prisma = new PrismaClient(); - -export async function getUserChannels(userId: string): Promise { - try{ - if(!userId){ - throw new Error('missing userId'); - } - - //TODO: add some prisma code here - //to fetch all the channels a user is part of - - - return ["test1","test2"]; - }catch(err){ - const errMessage = err as Error - - if(errMessage.message === 'missing userId'){ - console.log('services::actions::getUserChannels - missing userId'); - return null; - } - - console.log('services::actions::getUserChannels - unknown error', errMessage); - return null; - } -} - -export async function getUserInformation(userId: string): - Promise<{ - id: string, - userName: string, - nickName: string | null, - bio: string | null, - picture: string | null, - banner: string | null, - admin: boolean, - status: 'online' | 'offline' | 'dnd' | 'idle' | 'invis', - role: Role[], - userAuth?: UserAuth, -} | null> { - try{ - if(!userId){ - throw new Error('missing userId'); - } - - const user = await prisma.user.findUnique({ - where: { - id: userId - } - }) - - if (!user) { - throw new Error('could not find user'); - } - - const userRoles = await prisma.role.findMany({ - where: { - userId: userId - } - }) - - return { - id: userId, - userName: user.username, - nickName: user.nickname, - bio: user.bio, - picture: user.picture, - banner: user.banner, - admin: user.admin, - status: (["online", "offline", "dnd", "idle", "invis"] as const).includes(user.status as any) ? user.status as 'online' | 'offline' | 'dnd' | 'idle' | 'invis' : 'offline', - role: userRoles, - }; - - }catch(err){ - const errMessage = err as Error - - if(errMessage.message === 'missing userId'){ - console.log('services::actions::getUserInformation - missing userId'); - return null; - } - - if(errMessage.message === 'could not find user'){ - console.log('services::actions::getUserInformation - unable to find user'); - return null; - } - - console.log('services::actions::getUserInformation - unknown error', errMessage); - return null; - } -} - -export async function getAllMessage(userId: string): Promise<{id: string, content: string, senderId: string, receiverId: string}[] | null> { - try{ - if(!userId){ - throw new Error('missing userId'); - } - - //TODO: add some prisma code here - //to fetch all messages for a user - - return [ - {id: "1", content: "Hello", senderId: userId, receiverId: "2"}, - {id: "2", content: "Hi", senderId: "2", receiverId: userId} - ]; - }catch(err){ - const errMessage = err as Error - - if(errMessage.message === 'missing userId'){ - console.log('services::actions::getAllMessage - missing userId'); - return null; - } - - console.log('services::actions::getAllMessage - unknown error', errMessage); - return null; - } -} - - -export async function sendMessage(userId: string, content: string): Promise { - try{ - if(!userId){ - throw new Error('missing userId'); - } - - if(!content){ - throw new Error('missing content'); - } - - //TODO: add some prisma code here - //to save a message - - //todo: add some socketio code to - //emit the message to the receiver - - return true; - }catch(err){ - const errMessage = err as Error - - if(errMessage.message === 'missing userId'){ - console.log('services::actions::sendMessage - missing userId'); - return false; - } - - if(errMessage.message === 'missing content'){ - console.log('services::actions::sendMessage - missing content'); - return false; - } - - console.log('services::actions::sendMessage - unknown error', errMessage); - return false; - } -} diff --git a/concord-server/src/services/userService.ts b/concord-server/src/services/userService.ts new file mode 100644 index 0000000..82722b2 --- /dev/null +++ b/concord-server/src/services/userService.ts @@ -0,0 +1,151 @@ +import { + Message, + MessagePing, + PrismaClient, + Role, + UserAuth, +} from "@prisma/client"; + +const prisma = new PrismaClient(); + +export async function getUserInformation(userId: string): Promise<{ + id: string; + userName: string; + nickName: string | null; + bio: string | null; + picture: string | null; + banner: string | null; + admin: boolean; + status: "online" | "offline" | "dnd" | "idle" | "invis"; + role: Role[]; +} | null> { + try { + if (!userId) { + throw new Error("missing userId"); + } + + const user = await prisma.user.findUnique({ + where: { + id: userId, + }, + }); + + if (!user) { + throw new Error("could not find user"); + } + + const userRoles = await prisma.role.findMany({ + where: { + userId: userId, + }, + }); + + return { + id: userId, + userName: user.username, + nickName: user.nickname, + bio: user.bio, + picture: user.picture, + banner: user.banner, + admin: user.admin, + status: (["online", "offline", "dnd", "idle", "invis"] as const).includes( + user.status as any, + ) + ? (user.status as "online" | "offline" | "dnd" | "idle" | "invis") + : "offline", + role: userRoles, + }; + } catch (err) { + const errMessage = err as Error; + + if (errMessage.message === "missing userId") { + console.log("services::actions::getUserInformation - missing userId"); + return null; + } + + if (errMessage.message === "could not find user") { + console.log( + "services::actions::getUserInformation - unable to find user", + ); + return null; + } + + console.log( + "services::actions::getUserInformation - unknown error", + errMessage, + ); + return null; + } +} + +export async function getAllUsersFrom(instanceId: string): Promise< + | { + id: string; + userName: string; + nickName: string | null; + bio: string | null; + picture: string | null; + banner: string | null; + admin: boolean; + status: "online" | "offline" | "dnd" | "idle" | "invis"; + role: { + userId: string; + instanceId: string; + }[]; + }[] + | null +> { + try { + const instances = await prisma.instance.findMany(); + if (!instances) { + throw new Error("could not get all instances"); + } + + const users = await prisma.user.findMany({ + where: { + Role: { + some: { + instanceId: instanceId, + }, + }, + }, + }); + if (!users) { + throw new Error("could not get all users in instance"); + } + + const userData = await Promise.all( + users.map(async (u) => { + const userRoles = await prisma.role.findMany({ + where: { + userId: u.id, + }, + }); + if (!userRoles) { + throw new Error("could not get user roles for user " + u.id); + } + + return { + id: u.id, + userName: u.username, + nickName: u.nickname, + bio: u.bio, + picture: u.picture, + banner: u.banner, + admin: u.admin, + status: ( + ["online", "offline", "dnd", "idle", "invis"] as const + ).includes(u.status as any) + ? (u.status as "online" | "offline" | "dnd" | "idle" | "invis") + : "offline", + role: userRoles, + }; + }), + ); + + return userData; + } catch (err) { + console.log(err); + return null; + } +} diff --git a/concord-server/src/validators/actions.ts b/concord-server/src/validators/actions.ts deleted file mode 100644 index 55d73ea..0000000 --- a/concord-server/src/validators/actions.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { zValidator } from "@hono/zod-validator" - -export const \ No newline at end of file diff --git a/concord-server/tsconfig.json b/concord-server/tsconfig.json index c442b33..9136c14 100644 --- a/concord-server/tsconfig.json +++ b/concord-server/tsconfig.json @@ -4,4 +4,4 @@ "jsx": "react-jsx", "jsxImportSource": "hono/jsx" } -} \ No newline at end of file +} From c39ab15694a680440ec1d4ac04883414a154217a Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 12:26:24 -0400 Subject: [PATCH 09/26] Add role type ; include admins in instance user list --- concord-server/schema.prisma | 9 +++-- concord-server/src/routes/userRoutes.ts | 8 +++- concord-server/src/services/userService.ts | 43 +++++++++++++++++++++- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/concord-server/schema.prisma b/concord-server/schema.prisma index 9c2688f..fc43e65 100644 --- a/concord-server/schema.prisma +++ b/concord-server/schema.prisma @@ -31,10 +31,11 @@ model User { } model Role { - User User @relation(fields: [userId], references: [id]) - userId String - Instance Instance @relation(fields: [instanceId], references: [id]) - instanceId String + User User @relation(fields: [userId], references: [id]) + userId String + Instance Instance @relation(fields: [instanceId], references: [id]) + instanceId String + type String @@unique([userId, instanceId]) } diff --git a/concord-server/src/routes/userRoutes.ts b/concord-server/src/routes/userRoutes.ts index c7a46fa..ec25a10 100644 --- a/concord-server/src/routes/userRoutes.ts +++ b/concord-server/src/routes/userRoutes.ts @@ -12,8 +12,12 @@ actions.get("user/:id", async (c) => { } }); -actions.get("instance/:id/users", async (c) => { - const instanceId = c.req.param("id"); +actions.get("user", async (c) => { + const instanceId = c.req.query("instance_id"); + if (!instanceId) { + return c.json({ error: "No instance id provided" }, 400); + } + const userData = await fetchAllUsers(instanceId); if (userData) { return c.json(userData); diff --git a/concord-server/src/services/userService.ts b/concord-server/src/services/userService.ts index 82722b2..d9d7434 100644 --- a/concord-server/src/services/userService.ts +++ b/concord-server/src/services/userService.ts @@ -114,6 +114,47 @@ export async function getAllUsersFrom(instanceId: string): Promise< throw new Error("could not get all users in instance"); } + const admins = await prisma.user.findMany({ + where: { + admin: true + } + }); + if (!admins) { + throw new Error("could not get all admins"); + } + const adminData = await Promise.all( + admins.map(async (u) => { + const adminRoles = await prisma.role.findMany({ + where: { + userId: u.id, + }, + }); + + if (!adminRoles) { + throw new Error("could not get admin roles for admin " + u.id); + } + + return { + id: u.id, + userName: u.username, + nickName: u.nickname, + bio: u.bio, + picture: u.picture, + banner: u.banner, + admin: u.admin, + status: ( + ["online", "offline", "dnd", "idle", "invis"] as const + ).includes(u.status as any) + ? (u.status as "online" | "offline" | "dnd" | "idle" | "invis") + : "offline", + role: adminRoles.map(r => ({ + userId: r.userId, + instanceId: r.instanceId, + })), + } + }) + ) + const userData = await Promise.all( users.map(async (u) => { const userRoles = await prisma.role.findMany({ @@ -143,7 +184,7 @@ export async function getAllUsersFrom(instanceId: string): Promise< }), ); - return userData; + return userData.concat(adminData); } catch (err) { console.log(err); return null; From 3d79c9cf4f05883283e4f6016583ae197c599731 Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:04:35 -0400 Subject: [PATCH 10/26] Add user POST + validation --- concord-server/bun.lock | 28 +++++++++++++++++++ concord-server/package.json | 2 ++ .../src/controller/userController.ts | 7 ++++- concord-server/src/routes/userRoutes.ts | 18 +++++++++++- concord-server/src/services/userService.ts | 15 ++++++++++ .../src/validators/userValidator.ts | 13 +++++++++ 6 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 concord-server/src/validators/userValidator.ts diff --git a/concord-server/bun.lock b/concord-server/bun.lock index c36e117..dfe5a87 100644 --- a/concord-server/bun.lock +++ b/concord-server/bun.lock @@ -6,8 +6,10 @@ "dependencies": { "@hono/zod-validator": "^0.7.3", "@prisma/client": "^6.16.2", + "@scalar/hono-api-reference": "^0.9.19", "@socket.io/bun-engine": "^0.0.3", "hono": "^4.9.9", + "hono-openapi": "^1.0.8", "prisma": "^6.16.2", "socket.io": "^4.8.1", }, @@ -50,16 +52,30 @@ "@prisma/schema-files-loader": ["@prisma/schema-files-loader@6.16.2", "", { "dependencies": { "@prisma/prisma-schema-wasm": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "fs-extra": "11.3.0" } }, "sha512-TN77DUFgOxT/WL6wuxVhn7qVDvwVRl0TEzhFfRh5vKQsuZ5itLzA7Ki4TgOs4Pk18wwZnti6ZKdzR3Y7cu2KsA=="], + "@scalar/core": ["@scalar/core@0.3.17", "", { "dependencies": { "@scalar/types": "0.2.16" } }, "sha512-G6tP+2oorFA90szI8DGiEQ23SmobiuhN93GfTJNpFMhz/kdEtC4lcYawAxL1tWkZhlK/QYRcaCZcVzmpTUgBCA=="], + + "@scalar/hono-api-reference": ["@scalar/hono-api-reference@0.9.19", "", { "dependencies": { "@scalar/core": "0.3.17" }, "peerDependencies": { "hono": "^4.0.0" } }, "sha512-MeBdIwuAAhwvsUt1f9174wASu0C8b4WrXuVdC/6FjrsTxLHSbJVx+XFzZAKF5ex+1kaXjvYrm3fiovRFHLyfJA=="], + + "@scalar/openapi-types": ["@scalar/openapi-types@0.3.7", "", { "dependencies": { "zod": "3.24.1" } }, "sha512-QHSvHBVDze3+dUwAhIGq6l1iOev4jdoqdBK7QpfeN1Q4h+6qpVEw3EEqBiH0AXUSh/iWwObBv4uMgfIx0aNZ5g=="], + + "@scalar/types": ["@scalar/types@0.2.16", "", { "dependencies": { "@scalar/openapi-types": "0.3.7", "nanoid": "5.1.5", "zod": "3.24.1" } }, "sha512-XWff9jWfYaj6q3ww94x66S6Q58u/3kA1sDOUhLAwb9va7r58bzk3NRwLOkEEdJmyEns1MEJAM53mY8KRWX6elA=="], + "@socket.io/bun-engine": ["@socket.io/bun-engine@0.0.3", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-OK2ZObq9hKsxyAcV7xDTcWGubBmfEY3Lt4nb04K+HlYl9G5PDgrY9hxJm9uV+B0xo3MhKFrgdg9VQsVZ3pbT/g=="], "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + "@standard-community/standard-json": ["@standard-community/standard-json@0.3.5", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@types/json-schema": "^7.0.15", "@valibot/to-json-schema": "^1.3.0", "arktype": "^2.1.20", "effect": "^3.16.8", "quansync": "^0.2.11", "sury": "^10.0.0", "typebox": "^1.0.17", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.5" }, "optionalPeers": ["@valibot/to-json-schema", "arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-to-json-schema"] }, "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA=="], + + "@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.8", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-80ap74p5oy/SU4al5HkPwO5+NbN2wH/FBr6kwaE5ROq7AvcDFaxzUfTazewroNaCotbvdGcvzXb9oEoOIyfC/Q=="], + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], "@types/react": ["@types/react@19.1.14", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-ukd93VGzaNPMAUPy0gRDSC57UuQbnH9Kussp7HBjM06YFi9uZTFhOvMSO2OKqXm1rSgzOE+pVx1k1PYHGwlc8Q=="], @@ -132,6 +148,8 @@ "hono": ["hono@4.9.9", "", {}, "sha512-Hxw4wT6zjJGZJdkJzAx9PyBdf7ZpxaTSA0NfxqjLghwMrLBX8p33hJBzoETRakF3UJu6OdNQBZAlNSkGqKFukw=="], + "hono-openapi": ["hono-openapi@1.0.8", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.1", "@standard-community/standard-openapi": "^0.2.4", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-JjSdT4sNUgxQGgwO90boRLfnrVYp3ge+Y/vHqPMJrAZuaIhKekAVipoeJ8AgpTyK+ZaxPzqdcmDBA9L+Ce3X9Q=="], + "jiti": ["jiti@2.6.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -146,6 +164,8 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], @@ -160,6 +180,8 @@ "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], @@ -176,6 +198,8 @@ "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], @@ -207,5 +231,9 @@ "ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], "zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], + + "@scalar/openapi-types/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="], + + "@scalar/types/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="], } } diff --git a/concord-server/package.json b/concord-server/package.json index 61d3f41..caea3cb 100644 --- a/concord-server/package.json +++ b/concord-server/package.json @@ -6,8 +6,10 @@ "dependencies": { "@hono/zod-validator": "^0.7.3", "@prisma/client": "^6.16.2", + "@scalar/hono-api-reference": "^0.9.19", "@socket.io/bun-engine": "^0.0.3", "hono": "^4.9.9", + "hono-openapi": "^1.0.8", "prisma": "^6.16.2", "socket.io": "^4.8.1" }, diff --git a/concord-server/src/controller/userController.ts b/concord-server/src/controller/userController.ts index f6fdd63..2e5af93 100644 --- a/concord-server/src/controller/userController.ts +++ b/concord-server/src/controller/userController.ts @@ -1,4 +1,5 @@ -import { getAllUsersFrom, getUserInformation } from "../services/userService"; +import { getAllUsersFrom, getUserInformation, createUser } from "../services/userService"; +import { CreateUserInput } from "../validators/userValidator"; export async function fetchUserData(id: string) { return await getUserInformation(id); @@ -7,3 +8,7 @@ export async function fetchUserData(id: string) { export async function fetchAllUsers(instanceId: string) { return await getAllUsersFrom(instanceId); } + +export async function createNewUser(data: CreateUserInput) { + return await createUser(data); +} diff --git a/concord-server/src/routes/userRoutes.ts b/concord-server/src/routes/userRoutes.ts index ec25a10..f709e53 100644 --- a/concord-server/src/routes/userRoutes.ts +++ b/concord-server/src/routes/userRoutes.ts @@ -1,5 +1,7 @@ import { Hono } from "hono"; -import { fetchAllUsers, fetchUserData } from "../controller/userController"; +import { fetchAllUsers, fetchUserData, createNewUser } from "../controller/userController"; +import { createUserSchema } from "../validators/userValidator"; +import { zValidator } from "@hono/zod-validator"; const actions = new Hono(); actions.get("user/:id", async (c) => { @@ -26,4 +28,18 @@ actions.get("user", async (c) => { } }); +actions.post( + "user", + zValidator('json', createUserSchema), + async (c) => { + try { + const data = await c.req.json(); + const newUser = await createNewUser(data); + return c.json(newUser, 201); + } catch (error) { + return c.json({ error: "Error creating user" }, 500); + } + } +); + export default actions; diff --git a/concord-server/src/services/userService.ts b/concord-server/src/services/userService.ts index d9d7434..23f19dd 100644 --- a/concord-server/src/services/userService.ts +++ b/concord-server/src/services/userService.ts @@ -5,9 +5,24 @@ import { Role, UserAuth, } from "@prisma/client"; +import { CreateUserInput } from '../validators/userValidator'; const prisma = new PrismaClient(); +export async function createUser(data: CreateUserInput) { + return await prisma.user.create({ + data: { + username: data.username, + nickname: data.nickname, + bio: data.bio, + picture: data.picture, + banner: data.banner, + status: data.status, + admin: data.admin, + }, + }); +} + export async function getUserInformation(userId: string): Promise<{ id: string; userName: string; diff --git a/concord-server/src/validators/userValidator.ts b/concord-server/src/validators/userValidator.ts new file mode 100644 index 0000000..0f7eae6 --- /dev/null +++ b/concord-server/src/validators/userValidator.ts @@ -0,0 +1,13 @@ +import { z } from 'zod' + +export const createUserSchema = z.object({ + username: z.string().min(3).max(30), + nickname: z.string().min(1).max(30).optional(), + bio: z.string().max(500).optional(), + picture: z.url().optional(), + banner: z.url().optional(), + status: z.enum(['online', 'offline', 'dnd', 'idle', 'invis']).default('online'), + admin: z.boolean().default(false), +}) + +export type CreateUserInput = z.infer \ No newline at end of file From b9982b626f8a20e001a5df4416473cf30bdceff3 Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:20:15 -0400 Subject: [PATCH 11/26] Enable Scalar + OpenAPI spec for user posting --- concord-server/bun.lock | 3 +++ concord-server/package.json | 1 + concord-server/src/index.ts | 20 ++++++++++++++++++++ concord-server/src/routes/userRoutes.ts | 12 ++++++++++++ 4 files changed, 36 insertions(+) diff --git a/concord-server/bun.lock b/concord-server/bun.lock index dfe5a87..4a5362e 100644 --- a/concord-server/bun.lock +++ b/concord-server/bun.lock @@ -4,6 +4,7 @@ "": { "name": "concord-server", "dependencies": { + "@hono/standard-validator": "^0.1.5", "@hono/zod-validator": "^0.7.3", "@prisma/client": "^6.16.2", "@scalar/hono-api-reference": "^0.9.19", @@ -20,6 +21,8 @@ }, }, "packages": { + "@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="], + "@hono/zod-validator": ["@hono/zod-validator@0.7.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-uYGdgVib3RlGD698WR5dVM0zB3UuPY5vHKXffGUbUh7r4xY+mFIhF3/v4AcQVLrU5CQdBso8BJr4wuVoCrjTuQ=="], "@prisma/client": ["@prisma/client@6.16.2", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw=="], diff --git a/concord-server/package.json b/concord-server/package.json index caea3cb..82af71b 100644 --- a/concord-server/package.json +++ b/concord-server/package.json @@ -4,6 +4,7 @@ "dev": "bun run --hot src/index.ts" }, "dependencies": { + "@hono/standard-validator": "^0.1.5", "@hono/zod-validator": "^0.7.3", "@prisma/client": "^6.16.2", "@scalar/hono-api-reference": "^0.9.19", diff --git a/concord-server/src/index.ts b/concord-server/src/index.ts index c26549d..92d2d99 100644 --- a/concord-server/src/index.ts +++ b/concord-server/src/index.ts @@ -3,6 +3,8 @@ import { cors } from "hono/cors"; import { Server as Engine } from "@socket.io/bun-engine"; import { Server } from "socket.io"; import routes from "./routes/index"; +import { Scalar } from "@scalar/hono-api-reference"; +import { openAPIRouteHandler } from "hono-openapi"; //initialize socket.io server const io = new Server(); @@ -50,4 +52,22 @@ app.use( app.route("/api", routes); +app.get( + '/openapi', + openAPIRouteHandler(app, { + documentation: { + info: { + title: 'Hono API', + version: '1.0.0', + description: 'Greeting API', + }, + servers: [ + { url: 'http://localhost:3000', description: 'Local Server' }, + ], + }, + }) +) + +app.get('/scalar', Scalar({ url: '/openapi' })) + export default app; diff --git a/concord-server/src/routes/userRoutes.ts b/concord-server/src/routes/userRoutes.ts index f709e53..3d507a1 100644 --- a/concord-server/src/routes/userRoutes.ts +++ b/concord-server/src/routes/userRoutes.ts @@ -2,6 +2,7 @@ import { Hono } from "hono"; import { fetchAllUsers, fetchUserData, createNewUser } from "../controller/userController"; import { createUserSchema } from "../validators/userValidator"; import { zValidator } from "@hono/zod-validator"; +import { describeRoute, resolver } from "hono-openapi"; const actions = new Hono(); actions.get("user/:id", async (c) => { @@ -30,6 +31,17 @@ actions.get("user", async (c) => { actions.post( "user", + describeRoute({ + description: "Create a new user", + responses: { + 201: { + description: "Success", + content: { + 'application/json': { schema: resolver(createUserSchema) }, + }, + } + } + }), zValidator('json', createUserSchema), async (c) => { try { From 27807357ace2c7fcabf5c10a2f1271a0a5997655 Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 14:17:15 -0400 Subject: [PATCH 12/26] Add good validation to all existing endpoints --- concord-server/src/routes/userRoutes.ts | 83 ++++++++++++++----- concord-server/src/services/userService.ts | 14 +++- .../src/validators/userValidator.ts | 8 ++ 3 files changed, 82 insertions(+), 23 deletions(-) diff --git a/concord-server/src/routes/userRoutes.ts b/concord-server/src/routes/userRoutes.ts index 3d507a1..4fc91f3 100644 --- a/concord-server/src/routes/userRoutes.ts +++ b/concord-server/src/routes/userRoutes.ts @@ -1,33 +1,67 @@ import { Hono } from "hono"; import { fetchAllUsers, fetchUserData, createNewUser } from "../controller/userController"; -import { createUserSchema } from "../validators/userValidator"; +import { createUserSchema, queryAllUsersByInstanceId, queryUserByIdSchema } from "../validators/userValidator"; import { zValidator } from "@hono/zod-validator"; import { describeRoute, resolver } from "hono-openapi"; const actions = new Hono(); -actions.get("user/:id", async (c) => { - const id = c.req.param("id"); - const userData = await fetchUserData(id); - if (userData) { - return c.json(userData); - } else { - return c.json({ error: "User not found" }, 404); +actions.get("user/:id", + describeRoute({ + description: "Get user by id", + responses: { + 200: { + description: "Success getting user", + content: { + "application/json": { schema: resolver(queryUserByIdSchema) } + } + }, + 404: { + description: "User id not found", + content: { + "application/json": { schema: resolver(queryUserByIdSchema) } + } + } + } + }), + zValidator('param', queryUserByIdSchema), + async (c) => { + const id = c.req.param("id"); + const userData = await fetchUserData(id); + if (userData) { + return c.json(userData); + } else { + return c.json({ error: "User not found" }, 404); + } } -}); +); -actions.get("user", async (c) => { - const instanceId = c.req.query("instance_id"); - if (!instanceId) { - return c.json({ error: "No instance id provided" }, 400); - } +actions.get("user", + describeRoute({ + description: "Get all users by instance id", + responses: { + 200: { + description: "Success getting all users in instance", + content: { + "application/json": { schema: resolver(queryAllUsersByInstanceId) } + } + } + } + }), + zValidator('query', queryAllUsersByInstanceId), + async (c) => { + const instanceId = c.req.query("instanceId"); + if (!instanceId) { + return c.json({ error: "No instance id provided" }, 400); + } - const userData = await fetchAllUsers(instanceId); - if (userData) { - return c.json(userData); - } else { - return c.json({ error: "Error getting all users from instance" }, 500); + const userData = await fetchAllUsers(instanceId); + if (userData) { + return c.json(userData); + } else { + return c.json({ error: "Error getting all users from instance" }, 500); + } } -}); +); actions.post( "user", @@ -39,6 +73,12 @@ actions.post( content: { 'application/json': { schema: resolver(createUserSchema) }, }, + }, + 400: { + description: "Bad request (user exists)", + content: { + 'application/json': { schema: resolver(createUserSchema) } + } } } }), @@ -47,6 +87,9 @@ actions.post( try { const data = await c.req.json(); const newUser = await createNewUser(data); + if (!newUser) { + return c.json({ error: "User already exists" }, 400); + } return c.json(newUser, 201); } catch (error) { return c.json({ error: "Error creating user" }, 500); diff --git a/concord-server/src/services/userService.ts b/concord-server/src/services/userService.ts index 23f19dd..4228b66 100644 --- a/concord-server/src/services/userService.ts +++ b/concord-server/src/services/userService.ts @@ -10,6 +10,10 @@ import { CreateUserInput } from '../validators/userValidator'; const prisma = new PrismaClient(); export async function createUser(data: CreateUserInput) { + if (await prisma.user.count({ where: { username: data.username }}) >= 1) { + return null; + } + return await prisma.user.create({ data: { username: data.username, @@ -111,9 +115,13 @@ export async function getAllUsersFrom(instanceId: string): Promise< | null > { try { - const instances = await prisma.instance.findMany(); - if (!instances) { - throw new Error("could not get all instances"); + const instances = await prisma.instance.count({ + where: { + id: instanceId + } + }) + if (instances < 1) { + throw new Error("could not find given instance id"); } const users = await prisma.user.findMany({ diff --git a/concord-server/src/validators/userValidator.ts b/concord-server/src/validators/userValidator.ts index 0f7eae6..a4aaa60 100644 --- a/concord-server/src/validators/userValidator.ts +++ b/concord-server/src/validators/userValidator.ts @@ -1,5 +1,13 @@ import { z } from 'zod' +export const queryUserByIdSchema = z.object({ + id: z.uuidv7() +}) + +export const queryAllUsersByInstanceId = z.object({ + instanceId: z.uuidv7() +}) + export const createUserSchema = z.object({ username: z.string().min(3).max(30), nickname: z.string().min(1).max(30).optional(), From 072bd7f0d7ad2ac6c894b296a9ad352c5730cdf6 Mon Sep 17 00:00:00 2001 From: PrimarchPaul Date: Sat, 27 Sep 2025 14:23:19 -0400 Subject: [PATCH 13/26] added realtime post/deleteFromChannel serv+controller --- concord-server/src/controller/realtime.ts | 128 ++++++++++++++++++++++ concord-server/src/services/realtime.ts | 88 +++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 concord-server/src/controller/realtime.ts create mode 100644 concord-server/src/services/realtime.ts diff --git a/concord-server/src/controller/realtime.ts b/concord-server/src/controller/realtime.ts new file mode 100644 index 0000000..0151393 --- /dev/null +++ b/concord-server/src/controller/realtime.ts @@ -0,0 +1,128 @@ +import { Context } from "hono"; +import { sendMessageToChannel, removeMessageFromChannel } from "../services/realtime.js" +import { success } from "zod"; + + + +export async function postMessageToChannel( + io: any, + c: Context +) { + try { + io = c.get("io"); + + const instanceId = c.req.param("instanceId"); + const categoryId = c.req.param("categoryId"); + const channelId = c.req.param("channelId"); + const message = await c.req.json(); + + const result = await sendMessageToChannel( + instanceId, + categoryId, + channelId, + message, + "new_channel_message", + io + ) + + if(result === "Event not implemented"){ + console.log("controller::realtime::postMessageToChannel - Failed to send message") + return c.json({ + success: false, + message: "Event not implemented or recognized", + status: 400 + }) + } + + if(result === "no acknowledgment"){ + console.log("controller::realtime::postMessageToChannel - No acknowledgment received from client") + return c.json({ + success: false, + message: "No acknowledgment received from client", + status: 500 + }) + } + + if(!result){ + throw new Error("failed to send message"); + } + + return c.json({ + success: true, + message: "Message sent successfully", + status: 200 + }) + + + } catch (err) { + const errMessage = err as Error; + console.log("controller::realtime::postMessageToChannel - ", errMessage); + return c.json({ + success: false, + message: errMessage.message, + status: 500 + }); + } +} + +export async function deleteMessageFromChannel( + io: any, + c: Context +){ + try { + + io = c.get("io"); + + const instanceId = c.req.param("instanceId"); + const categoryId = c.req.param("categoryId"); + const channelId = c.req.param("channelId"); + const messageId = c.req.param("messageId"); + + const result = await removeMessageFromChannel( + instanceId, + categoryId, + channelId, + messageId, + "delete_channel_message", + io + ) + + if(result === "event not implemented"){ + console.log("controller::realtime::deleteMessageFromChannel - Event not implemented") + return c.json({ + success: false, + message: "Event not implemented or recognized", + status: 400 + }); + } + + if(result === "no acknowledgment"){ + console.log("controller::realtime::deleteMessageFromChannel - No acknowledgment received from client") + return c.json({ + success: false, + message: "No acknowledgment received from client", + status: 500 + }); + } + + if(!result){ + throw new Error("failed to delete message"); + } + + c.json({ + success: true, + message: "Message deleted successfully", + status: 200 + }) + + } catch (err) { + + const errMessage = err as Error; + console.log("services::realtime::deleteMessageFromChannel - ", errMessage); + return c.json({ + success: false, + message: errMessage.message, + status: 500 + }); + } +} \ No newline at end of file diff --git a/concord-server/src/services/realtime.ts b/concord-server/src/services/realtime.ts new file mode 100644 index 0000000..b99c5cd --- /dev/null +++ b/concord-server/src/services/realtime.ts @@ -0,0 +1,88 @@ +import { readonly } from "zod"; + +const EVENTS = { + NEW_CHANNEL_MESSAGE: "new_channel_message", + DELETE_CHANNEL_MESSAGE: "delete_channel_message", +} + + + +export async function sendMessageToChannel( + instanceId: string, + categoryId: string, + channelId: string, + message: any, + event: string, + io: any, +): Promise { + try { + + //TODO: implement middleware to replace this + if(EVENTS.NEW_CHANNEL_MESSAGE === event){ + throw new Error("Event not implemented"); + } + + //TODO: add prisma to save channel message to DB + + return new Promise((resolve) => { + io.to(instanceId).emit(event, message, (ack: any) => { + if (ack && ack.status === 'received') { + console.log(`Message ${ack.messageId} acknowledged by client.`); + resolve(true); + } else { + console.log('services::realtime::sendMessageToChannel No acknowledgment received from client.'); + resolve("no acknowledgment"); + } + }); + }); + } catch (err) { + const errMessage = err as Error; + if (errMessage.message === "Event not implemented") { + console.log(`services::realtime::sendMessageToChannel - Event not implemented. Attempted event: ${event}`) + return "event not implemented" + } + console.log("services::realtime::sendMessageToChannel - ", errMessage); + return false; + + } +} + +export async function removeMessageFromChannel( + instanceId: string, + categoryId: string, + channelId: string, + messageId: string, + event: string, + io: any +): Promise{ + try { + + //TODO: implement middleware to replace this + if(EVENTS.DELETE_CHANNEL_MESSAGE === event){ + throw new Error("event not implemented"); + } + + //TODO: add prisma to flag a channel message as deleted + + return new Promise((resolve) => { + io.to(instanceId).emit(event, { messageId }, (ack: any) => { + if (ack && ack.status === 'received') { + console.log(`Message ${ack.messageId} acknowledged by client.`); + resolve(true); + } else { + console.log('services::realtime::deleteMessageFromChannel No acknowledgment received from client.'); + resolve("no acknowledgment"); + } + }); + }); + } catch (err) { + const errMessage = err as Error; + if (errMessage.message === "Event not implemented") { + console.log(`services::realtime::deleteMessageFromChannel - Event not implemented. Attempted event: ${event}`) + return false; + } + console.log("services::realtime::deleteMessageFromChannel - ", errMessage); + return false; + + } +} \ No newline at end of file From 0818d9828a138809bc0dd056c5e3f2e942e9a0b0 Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 14:53:15 -0400 Subject: [PATCH 14/26] Update db schema + add hasher func --- concord-server/schema.prisma | 11 ++++------- concord-server/src/helper/hashing.ts | 5 +++++ concord-server/src/validators/userValidator.ts | 2 ++ 3 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 concord-server/src/helper/hashing.ts diff --git a/concord-server/schema.prisma b/concord-server/schema.prisma index fc43e65..0e23b2a 100644 --- a/concord-server/schema.prisma +++ b/concord-server/schema.prisma @@ -56,7 +56,6 @@ model Category { name String position Int Channel Channel[] - Message Message[] } model Channel { @@ -71,12 +70,10 @@ model Channel { model Message { id String @id @default(uuid(7)) - Channel Channel? @relation(fields: [channelId], references: [id]) - channelId String? - Category Category? @relation(fields: [categoryId], references: [id]) - categoryId String? - User User? @relation(fields: [userId], references: [id]) - userId String? + Channel Channel @relation(fields: [channelId], references: [id]) + channelId String + User User @relation(fields: [userId], references: [id]) + userId String deleted Boolean text String replies Reply[] @relation("MessageToReply") diff --git a/concord-server/src/helper/hashing.ts b/concord-server/src/helper/hashing.ts new file mode 100644 index 0000000..cd4cb4f --- /dev/null +++ b/concord-server/src/helper/hashing.ts @@ -0,0 +1,5 @@ +import * as crypto from 'crypto'; + +export default function shaHash(data:string, salt:string) : string { + return crypto.createHmac('sha256', salt).update(data).digest('hex'); +} diff --git a/concord-server/src/validators/userValidator.ts b/concord-server/src/validators/userValidator.ts index a4aaa60..6837dd2 100644 --- a/concord-server/src/validators/userValidator.ts +++ b/concord-server/src/validators/userValidator.ts @@ -18,4 +18,6 @@ export const createUserSchema = z.object({ admin: z.boolean().default(false), }) +export type QueryUserByIdInput = z.infer +export type QueryAllUsersByInstanceIdInput = z.infer export type CreateUserInput = z.infer \ No newline at end of file From 8eb8adec67283dfaa782684a4e43270407ceb06e Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 15:01:10 -0400 Subject: [PATCH 15/26] aefnuosbgtdhjkyh --- concord-server/schema.prisma | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/concord-server/schema.prisma b/concord-server/schema.prisma index 0e23b2a..69e738c 100644 --- a/concord-server/schema.prisma +++ b/concord-server/schema.prisma @@ -76,16 +76,16 @@ model Message { userId String deleted Boolean text String - replies Reply[] @relation("MessageToReply") - repliedTo Reply[] @relation("ReplyToMessage") + replies Reply? @relation("MessageToReply") + repliedTo Reply? @relation("ReplyToMessage") MessagePing MessagePing[] } model Reply { message Message @relation("MessageToReply", fields: [messageId], references: [id]) - messageId String + messageId String @unique repliesTo Message @relation("ReplyToMessage", fields: [repliesToId], references: [id]) - repliesToId String + repliesToId String @unique @@unique([messageId, repliesToId]) } From 5634518cd377cac6e95850a1e54ba243ffa9cde2 Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 15:31:49 -0400 Subject: [PATCH 16/26] Add admin auth verification to user creation ; Force credentials for a new user --- .../migration.sql | 131 ++++++++++++++++++ concord-server/migrations/migration_lock.toml | 3 + concord-server/src/services/userService.ts | 83 ++++++++++- .../src/validators/userValidator.ts | 3 + 4 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 concord-server/migrations/20250927190915_init2finalfinal/migration.sql create mode 100644 concord-server/migrations/migration_lock.toml diff --git a/concord-server/migrations/20250927190915_init2finalfinal/migration.sql b/concord-server/migrations/20250927190915_init2finalfinal/migration.sql new file mode 100644 index 0000000..6defc25 --- /dev/null +++ b/concord-server/migrations/20250927190915_init2finalfinal/migration.sql @@ -0,0 +1,131 @@ +-- CreateTable +CREATE TABLE "public"."Instance" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "icon" TEXT, + + CONSTRAINT "Instance_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."User" ( + "id" TEXT NOT NULL, + "username" TEXT NOT NULL, + "nickname" TEXT, + "bio" TEXT, + "picture" TEXT, + "banner" TEXT, + "admin" BOOLEAN NOT NULL, + "status" TEXT NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Role" ( + "userId" TEXT NOT NULL, + "instanceId" TEXT NOT NULL, + "type" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "public"."UserAuth" ( + "userId" TEXT NOT NULL, + "password" TEXT NOT NULL, + "token" TEXT +); + +-- CreateTable +CREATE TABLE "public"."Category" ( + "id" TEXT NOT NULL, + "instanceId" TEXT, + "name" TEXT NOT NULL, + "position" INTEGER NOT NULL, + + CONSTRAINT "Category_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Channel" ( + "id" TEXT NOT NULL, + "type" TEXT NOT NULL, + "categoryId" TEXT, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + + CONSTRAINT "Channel_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Message" ( + "id" TEXT NOT NULL, + "channelId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "deleted" BOOLEAN NOT NULL, + "text" TEXT NOT NULL, + + CONSTRAINT "Message_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Reply" ( + "messageId" TEXT NOT NULL, + "repliesToId" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "public"."MessagePing" ( + "messageId" TEXT NOT NULL, + "pingsUserId" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "Role_userId_instanceId_key" ON "public"."Role"("userId", "instanceId"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserAuth_userId_key" ON "public"."UserAuth"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Reply_messageId_key" ON "public"."Reply"("messageId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Reply_repliesToId_key" ON "public"."Reply"("repliesToId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Reply_messageId_repliesToId_key" ON "public"."Reply"("messageId", "repliesToId"); + +-- CreateIndex +CREATE UNIQUE INDEX "MessagePing_messageId_pingsUserId_key" ON "public"."MessagePing"("messageId", "pingsUserId"); + +-- AddForeignKey +ALTER TABLE "public"."Role" ADD CONSTRAINT "Role_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Role" ADD CONSTRAINT "Role_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "public"."Instance"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."UserAuth" ADD CONSTRAINT "UserAuth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Category" ADD CONSTRAINT "Category_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "public"."Instance"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Channel" ADD CONSTRAINT "Channel_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "public"."Category"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Message" ADD CONSTRAINT "Message_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "public"."Channel"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Message" ADD CONSTRAINT "Message_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Reply" ADD CONSTRAINT "Reply_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "public"."Message"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Reply" ADD CONSTRAINT "Reply_repliesToId_fkey" FOREIGN KEY ("repliesToId") REFERENCES "public"."Message"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."MessagePing" ADD CONSTRAINT "MessagePing_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "public"."Message"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."MessagePing" ADD CONSTRAINT "MessagePing_pingsUserId_fkey" FOREIGN KEY ("pingsUserId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/concord-server/migrations/migration_lock.toml b/concord-server/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/concord-server/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/concord-server/src/services/userService.ts b/concord-server/src/services/userService.ts index 4228b66..13e96d9 100644 --- a/concord-server/src/services/userService.ts +++ b/concord-server/src/services/userService.ts @@ -6,15 +6,34 @@ import { UserAuth, } from "@prisma/client"; import { CreateUserInput } from '../validators/userValidator'; +import shaHash from "../helper/hashing"; const prisma = new PrismaClient(); -export async function createUser(data: CreateUserInput) { +export async function createUser(data: CreateUserInput): Promise<{ + username: string, + nickname: string | null, + bio: string | null, + picture: string | null, + banner: string | null, + status: string, + admin: boolean +} | null> { + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials(data.requestingUserId) + if (!requestingUser + || !requestingUserCredentials + || !requestingUser.admin + || requestingUserCredentials.token == null + || data.requestingUserToken != requestingUserCredentials.token) { + return null; + } + if (await prisma.user.count({ where: { username: data.username }}) >= 1) { return null; } - return await prisma.user.create({ + const userData = await prisma.user.create({ data: { username: data.username, nickname: data.nickname, @@ -25,8 +44,68 @@ export async function createUser(data: CreateUserInput) { admin: data.admin, }, }); + + if (!(await prisma.userAuth.create({ + data: { + userId: userData.id, + password: shaHash(data.passwordhash, userData.id), + token: null, + } + }))) { + return null; + } + + return userData; } +export async function getUserCredentials(userId: string): Promise<{ + userId: string, + password: string, + token: string | null + } | null> { + try { + if (!userId) { + throw new Error("missing userId"); + } + + const userAuth = await prisma.userAuth.findUnique({ + where: { + userId: userId, + }, + }); + + if (!userAuth) { + throw new Error("could not find user credentials"); + } + + return { + userId: userAuth.userId, + password: userAuth.password, + token: userAuth.token, + }; + } catch (err) { + const errMessage = err as Error; + + if (errMessage.message === "missing userId") { + console.log("services::actions::getUserCredentials - missing userId"); + return null; + } + + if (errMessage.message === "could not find user credentials") { + console.log( + "services::actions::getUserCredentials - unable to find user credentials", + ); + return null; + } + + console.log( + "services::actions::getUserCredentials - unknown error", + errMessage, + ); + return null; + } + } + export async function getUserInformation(userId: string): Promise<{ id: string; userName: string; diff --git a/concord-server/src/validators/userValidator.ts b/concord-server/src/validators/userValidator.ts index 6837dd2..463957a 100644 --- a/concord-server/src/validators/userValidator.ts +++ b/concord-server/src/validators/userValidator.ts @@ -16,6 +16,9 @@ export const createUserSchema = z.object({ banner: z.url().optional(), status: z.enum(['online', 'offline', 'dnd', 'idle', 'invis']).default('online'), admin: z.boolean().default(false), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), + passwordhash: z.string(), }) export type QueryUserByIdInput = z.infer From 74f4e076ceaeeab2fb173d9ba60db7efa56bd97d Mon Sep 17 00:00:00 2001 From: PrimarchPaul Date: Sat, 27 Sep 2025 15:51:33 -0400 Subject: [PATCH 17/26] feat: message.ts service --- concord-server/schema.prisma | 8 +- concord-server/src/controller/realtime.ts | 5 +- concord-server/src/routes/realtime.ts | 28 +++++++ concord-server/src/services/message.ts | 77 +++++++++++++++++++ .../src/validators/messageValidator.ts | 0 5 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 concord-server/src/routes/realtime.ts create mode 100644 concord-server/src/services/message.ts create mode 100644 concord-server/src/validators/messageValidator.ts diff --git a/concord-server/schema.prisma b/concord-server/schema.prisma index 69e738c..fdfebd7 100644 --- a/concord-server/schema.prisma +++ b/concord-server/schema.prisma @@ -82,10 +82,10 @@ model Message { } model Reply { - message Message @relation("MessageToReply", fields: [messageId], references: [id]) - messageId String @unique - repliesTo Message @relation("ReplyToMessage", fields: [repliesToId], references: [id]) - repliesToId String @unique + message Message @relation("MessageToReply", fields: [messageId], references: [id]) //message text + messageId String @unique //message id of the reply + repliesTo Message @relation("ReplyToMessage", fields: [repliesToId], references: [id]) //message id that this message replies to + repliesToId String @unique //replies to this message id @@unique([messageId, repliesToId]) } diff --git a/concord-server/src/controller/realtime.ts b/concord-server/src/controller/realtime.ts index 0151393..7cacb9d 100644 --- a/concord-server/src/controller/realtime.ts +++ b/concord-server/src/controller/realtime.ts @@ -3,7 +3,6 @@ import { sendMessageToChannel, removeMessageFromChannel } from "../services/real import { success } from "zod"; - export async function postMessageToChannel( io: any, c: Context @@ -114,7 +113,7 @@ export async function deleteMessageFromChannel( message: "Message deleted successfully", status: 200 }) - + } catch (err) { const errMessage = err as Error; @@ -125,4 +124,4 @@ export async function deleteMessageFromChannel( status: 500 }); } -} \ No newline at end of file +} diff --git a/concord-server/src/routes/realtime.ts b/concord-server/src/routes/realtime.ts new file mode 100644 index 0000000..27fb941 --- /dev/null +++ b/concord-server/src/routes/realtime.ts @@ -0,0 +1,28 @@ +import { Hono } from "hono"; +import { zValidator } from "@hono/zod-validator"; +import { describeRoute, resolver } from "hono-openapi"; +import { postMessageToChannel, + deleteMessageFromChannel +} from "../controller/realtime"; + +const app = new Hono(); + +app.post( + "message/", + zValidator({ + body: z.object({ + content: z.string().min(1).max(500) + }) + }), + async (c) => { + const { instanceId, categoryId, channelId } = c.req.params; + const { content } = c.req.body; + + return postMessageToChannel(c.get("io"), { + instanceId, + categoryId, + channelId, + content + }); + } +); \ No newline at end of file diff --git a/concord-server/src/services/message.ts b/concord-server/src/services/message.ts new file mode 100644 index 0000000..4e666ee --- /dev/null +++ b/concord-server/src/services/message.ts @@ -0,0 +1,77 @@ +import { + Message, + MessagePing, + PrismaClient, + Role, + Reply + +} from "@prisma/client"; +import { CreateUserInput } from '../validators/userValidator'; + +const prisma = new PrismaClient(); + +class MessageService { + public async function sendMessageToChannel( + channelId: string, + userId: string, + content: string, + repliedMessageId: string | null + ): Promise<{ + id: string, + channelId: string, + userId: string, + text: string, + deleted: boolean, + replies: null | { + messageId: string, + repliesToId: string, + repliesToText: string + } + } | null> { + try { + + + const newMessage = await prisma.message.create({ + data: { + channelId: channelId, + userId: userId, + text: content, + deleted: false, + + } + }) + + let origMessage; + if(repliedMessageId){ + origMessage = await prisma.message.findUnique({ + where: { + id: repliedMessageId + } + }) + + if(!origMessage){ + throw new Error("could not find original message to reply to"); + } + + await prisma.reply.create({ + data: { + messageId: newMessage.id, + repliesToId: origMessage.id + } + }) + } + + return { + ...newMessage, + replies: repliedMessageId ? { + messageId: newMessage.id, + repliesToId: origMessage?.id, + repliesToText: origMessage?.text + } : null + } + + } catch (error) { + + + } +} \ No newline at end of file diff --git a/concord-server/src/validators/messageValidator.ts b/concord-server/src/validators/messageValidator.ts new file mode 100644 index 0000000..e69de29 From da1310b9c8302ea808716d557ad50a1d248f03da Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 15:55:20 -0400 Subject: [PATCH 18/26] made pretty and fixed message posting --- concord-server/src/controller/realtime.ts | 225 +++++++++--------- .../src/controller/userController.ts | 6 +- concord-server/src/helper/hashing.ts | 6 +- concord-server/src/index.ts | 18 +- concord-server/src/routes/realtime.ts | 37 +-- concord-server/src/routes/userRoutes.ts | 60 +++-- concord-server/src/services/message.ts | 133 ++++++----- concord-server/src/services/realtime.ts | 156 ++++++------ concord-server/src/services/userService.ts | 140 +++++------ .../src/validators/userValidator.ts | 24 +- 10 files changed, 415 insertions(+), 390 deletions(-) diff --git a/concord-server/src/controller/realtime.ts b/concord-server/src/controller/realtime.ts index 7cacb9d..5f9b961 100644 --- a/concord-server/src/controller/realtime.ts +++ b/concord-server/src/controller/realtime.ts @@ -1,127 +1,126 @@ import { Context } from "hono"; -import { sendMessageToChannel, removeMessageFromChannel } from "../services/realtime.js" +import { + sendMessageToChannel, + removeMessageFromChannel, +} from "../services/realtime.js"; import { success } from "zod"; +export async function postMessageToChannel(io: any, c: Context) { + try { + io = c.get("io"); -export async function postMessageToChannel( - io: any, - c: Context -) { - try { - io = c.get("io"); + const instanceId = c.req.param("instanceId"); + const categoryId = c.req.param("categoryId"); + const channelId = c.req.param("channelId"); + const message = await c.req.json(); - const instanceId = c.req.param("instanceId"); - const categoryId = c.req.param("categoryId"); - const channelId = c.req.param("channelId"); - const message = await c.req.json(); - - const result = await sendMessageToChannel( - instanceId, - categoryId, - channelId, - message, - "new_channel_message", - io - ) + const result = await sendMessageToChannel( + instanceId, + categoryId, + channelId, + message, + "new_channel_message", + io, + ); - if(result === "Event not implemented"){ - console.log("controller::realtime::postMessageToChannel - Failed to send message") - return c.json({ - success: false, - message: "Event not implemented or recognized", - status: 400 - }) - } - - if(result === "no acknowledgment"){ - console.log("controller::realtime::postMessageToChannel - No acknowledgment received from client") - return c.json({ - success: false, - message: "No acknowledgment received from client", - status: 500 - }) - } - - if(!result){ - throw new Error("failed to send message"); - } - - return c.json({ - success: true, - message: "Message sent successfully", - status: 200 - }) - - - } catch (err) { - const errMessage = err as Error; - console.log("controller::realtime::postMessageToChannel - ", errMessage); - return c.json({ - success: false, - message: errMessage.message, - status: 500 - }); + if (result === "Event not implemented") { + console.log( + "controller::realtime::postMessageToChannel - Failed to send message", + ); + return c.json({ + success: false, + message: "Event not implemented or recognized", + status: 400, + }); } + + if (result === "no acknowledgment") { + console.log( + "controller::realtime::postMessageToChannel - No acknowledgment received from client", + ); + return c.json({ + success: false, + message: "No acknowledgment received from client", + status: 500, + }); + } + + if (!result) { + throw new Error("failed to send message"); + } + + return c.json({ + success: true, + message: "Message sent successfully", + status: 200, + }); + } catch (err) { + const errMessage = err as Error; + console.log("controller::realtime::postMessageToChannel - ", errMessage); + return c.json({ + success: false, + message: errMessage.message, + status: 500, + }); + } } -export async function deleteMessageFromChannel( - io: any, - c: Context -){ - try { +export async function deleteMessageFromChannel(io: any, c: Context) { + try { + io = c.get("io"); - io = c.get("io"); + const instanceId = c.req.param("instanceId"); + const categoryId = c.req.param("categoryId"); + const channelId = c.req.param("channelId"); + const messageId = c.req.param("messageId"); - const instanceId = c.req.param("instanceId"); - const categoryId = c.req.param("categoryId"); - const channelId = c.req.param("channelId"); - const messageId = c.req.param("messageId"); + const result = await removeMessageFromChannel( + instanceId, + categoryId, + channelId, + messageId, + "delete_channel_message", + io, + ); - const result = await removeMessageFromChannel( - instanceId, - categoryId, - channelId, - messageId, - "delete_channel_message", - io - ) - - if(result === "event not implemented"){ - console.log("controller::realtime::deleteMessageFromChannel - Event not implemented") - return c.json({ - success: false, - message: "Event not implemented or recognized", - status: 400 - }); - } - - if(result === "no acknowledgment"){ - console.log("controller::realtime::deleteMessageFromChannel - No acknowledgment received from client") - return c.json({ - success: false, - message: "No acknowledgment received from client", - status: 500 - }); - } - - if(!result){ - throw new Error("failed to delete message"); - } - - c.json({ - success: true, - message: "Message deleted successfully", - status: 200 - }) - - } catch (err) { - - const errMessage = err as Error; - console.log("services::realtime::deleteMessageFromChannel - ", errMessage); - return c.json({ - success: false, - message: errMessage.message, - status: 500 - }); + if (result === "event not implemented") { + console.log( + "controller::realtime::deleteMessageFromChannel - Event not implemented", + ); + return c.json({ + success: false, + message: "Event not implemented or recognized", + status: 400, + }); } + + if (result === "no acknowledgment") { + console.log( + "controller::realtime::deleteMessageFromChannel - No acknowledgment received from client", + ); + return c.json({ + success: false, + message: "No acknowledgment received from client", + status: 500, + }); + } + + if (!result) { + throw new Error("failed to delete message"); + } + + c.json({ + success: true, + message: "Message deleted successfully", + status: 200, + }); + } catch (err) { + const errMessage = err as Error; + console.log("services::realtime::deleteMessageFromChannel - ", errMessage); + return c.json({ + success: false, + message: errMessage.message, + status: 500, + }); + } } diff --git a/concord-server/src/controller/userController.ts b/concord-server/src/controller/userController.ts index 2e5af93..0ba558d 100644 --- a/concord-server/src/controller/userController.ts +++ b/concord-server/src/controller/userController.ts @@ -1,4 +1,8 @@ -import { getAllUsersFrom, getUserInformation, createUser } from "../services/userService"; +import { + getAllUsersFrom, + getUserInformation, + createUser, +} from "../services/userService"; import { CreateUserInput } from "../validators/userValidator"; export async function fetchUserData(id: string) { diff --git a/concord-server/src/helper/hashing.ts b/concord-server/src/helper/hashing.ts index cd4cb4f..d987684 100644 --- a/concord-server/src/helper/hashing.ts +++ b/concord-server/src/helper/hashing.ts @@ -1,5 +1,5 @@ -import * as crypto from 'crypto'; +import * as crypto from "crypto"; -export default function shaHash(data:string, salt:string) : string { - return crypto.createHmac('sha256', salt).update(data).digest('hex'); +export default function shaHash(data: string, salt: string): string { + return crypto.createHmac("sha256", salt).update(data).digest("hex"); } diff --git a/concord-server/src/index.ts b/concord-server/src/index.ts index 92d2d99..1348a03 100644 --- a/concord-server/src/index.ts +++ b/concord-server/src/index.ts @@ -53,21 +53,19 @@ app.use( app.route("/api", routes); app.get( - '/openapi', + "/openapi", openAPIRouteHandler(app, { documentation: { info: { - title: 'Hono API', - version: '1.0.0', - description: 'Greeting API', + title: "Hono API", + version: "1.0.0", + description: "Greeting API", }, - servers: [ - { url: 'http://localhost:3000', description: 'Local Server' }, - ], + servers: [{ url: "http://localhost:3000", description: "Local Server" }], }, - }) -) + }), +); -app.get('/scalar', Scalar({ url: '/openapi' })) +app.get("/scalar", Scalar({ url: "/openapi" })); export default app; diff --git a/concord-server/src/routes/realtime.ts b/concord-server/src/routes/realtime.ts index 27fb941..933b839 100644 --- a/concord-server/src/routes/realtime.ts +++ b/concord-server/src/routes/realtime.ts @@ -1,28 +1,29 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; import { describeRoute, resolver } from "hono-openapi"; -import { postMessageToChannel, - deleteMessageFromChannel +import { + postMessageToChannel, + deleteMessageFromChannel, } from "../controller/realtime"; const app = new Hono(); app.post( - "message/", - zValidator({ - body: z.object({ - content: z.string().min(1).max(500) - }) + "message/", + zValidator({ + body: z.object({ + content: z.string().min(1).max(500), }), - async (c) => { - const { instanceId, categoryId, channelId } = c.req.params; - const { content } = c.req.body; + }), + async (c) => { + const { instanceId, categoryId, channelId } = c.req.params; + const { content } = c.req.body; - return postMessageToChannel(c.get("io"), { - instanceId, - categoryId, - channelId, - content - }); - } -); \ No newline at end of file + return postMessageToChannel(c.get("io"), { + instanceId, + categoryId, + channelId, + content, + }); + }, +); diff --git a/concord-server/src/routes/userRoutes.ts b/concord-server/src/routes/userRoutes.ts index 4fc91f3..fd3fbf4 100644 --- a/concord-server/src/routes/userRoutes.ts +++ b/concord-server/src/routes/userRoutes.ts @@ -1,29 +1,38 @@ import { Hono } from "hono"; -import { fetchAllUsers, fetchUserData, createNewUser } from "../controller/userController"; -import { createUserSchema, queryAllUsersByInstanceId, queryUserByIdSchema } from "../validators/userValidator"; +import { + fetchAllUsers, + fetchUserData, + createNewUser, +} from "../controller/userController"; +import { + createUserSchema, + queryAllUsersByInstanceId, + queryUserByIdSchema, +} from "../validators/userValidator"; import { zValidator } from "@hono/zod-validator"; import { describeRoute, resolver } from "hono-openapi"; const actions = new Hono(); -actions.get("user/:id", +actions.get( + "user/:id", describeRoute({ description: "Get user by id", responses: { 200: { description: "Success getting user", content: { - "application/json": { schema: resolver(queryUserByIdSchema) } - } + "application/json": { schema: resolver(queryUserByIdSchema) }, + }, }, 404: { description: "User id not found", content: { - "application/json": { schema: resolver(queryUserByIdSchema) } - } - } - } + "application/json": { schema: resolver(queryUserByIdSchema) }, + }, + }, + }, }), - zValidator('param', queryUserByIdSchema), + zValidator("param", queryUserByIdSchema), async (c) => { const id = c.req.param("id"); const userData = await fetchUserData(id); @@ -32,22 +41,23 @@ actions.get("user/:id", } else { return c.json({ error: "User not found" }, 404); } - } + }, ); -actions.get("user", +actions.get( + "user", describeRoute({ description: "Get all users by instance id", responses: { 200: { description: "Success getting all users in instance", content: { - "application/json": { schema: resolver(queryAllUsersByInstanceId) } - } - } - } + "application/json": { schema: resolver(queryAllUsersByInstanceId) }, + }, + }, + }, }), - zValidator('query', queryAllUsersByInstanceId), + zValidator("query", queryAllUsersByInstanceId), async (c) => { const instanceId = c.req.query("instanceId"); if (!instanceId) { @@ -60,7 +70,7 @@ actions.get("user", } else { return c.json({ error: "Error getting all users from instance" }, 500); } - } + }, ); actions.post( @@ -71,18 +81,18 @@ actions.post( 201: { description: "Success", content: { - 'application/json': { schema: resolver(createUserSchema) }, + "application/json": { schema: resolver(createUserSchema) }, }, }, 400: { description: "Bad request (user exists)", content: { - 'application/json': { schema: resolver(createUserSchema) } - } - } - } + "application/json": { schema: resolver(createUserSchema) }, + }, + }, + }, }), - zValidator('json', createUserSchema), + zValidator("json", createUserSchema), async (c) => { try { const data = await c.req.json(); @@ -94,7 +104,7 @@ actions.post( } catch (error) { return c.json({ error: "Error creating user" }, 500); } - } + }, ); export default actions; diff --git a/concord-server/src/services/message.ts b/concord-server/src/services/message.ts index 4e666ee..f90550e 100644 --- a/concord-server/src/services/message.ts +++ b/concord-server/src/services/message.ts @@ -3,75 +3,76 @@ import { MessagePing, PrismaClient, Role, - Reply - + Reply, } from "@prisma/client"; -import { CreateUserInput } from '../validators/userValidator'; +import { CreateUserInput } from "../validators/userValidator"; const prisma = new PrismaClient(); -class MessageService { - public async function sendMessageToChannel( - channelId: string, - userId: string, - content: string, - repliedMessageId: string | null - ): Promise<{ - id: string, - channelId: string, - userId: string, - text: string, - deleted: boolean, - replies: null | { - messageId: string, - repliesToId: string, - repliesToText: string - } - } | null> { - try { - - - const newMessage = await prisma.message.create({ - data: { - channelId: channelId, - userId: userId, - text: content, - deleted: false, - - } - }) - - let origMessage; - if(repliedMessageId){ - origMessage = await prisma.message.findUnique({ - where: { - id: repliedMessageId - } - }) - - if(!origMessage){ - throw new Error("could not find original message to reply to"); - } - - await prisma.reply.create({ - data: { - messageId: newMessage.id, - repliesToId: origMessage.id - } - }) - } - - return { - ...newMessage, - replies: repliedMessageId ? { - messageId: newMessage.id, - repliesToId: origMessage?.id, - repliesToText: origMessage?.text - } : null - } - - } catch (error) { - +export async function sendMessageToChannel( + channelId: string, + userId: string, + content: string, + repliedMessageId: string | null, +): Promise<{ + id: string; + channelId: string; + userId: string; + text: string; + deleted: boolean; + replies: null | { + messageId: string; + repliesToId: string; + repliesToText: string; + }; +} | null> { + try { + const newMessage = await prisma.message.create({ + data: { + channelId: channelId, + userId: userId, + text: content, + deleted: false, + }, + }); + if (!newMessage) { + return null; } -} \ No newline at end of file + + let origMessage; + if (repliedMessageId) { + origMessage = await prisma.message.findUnique({ + where: { + id: repliedMessageId, + }, + }); + + if (!origMessage) { + throw new Error("could not find original message to reply to"); + } + + await prisma.reply.create({ + data: { + messageId: newMessage.id, + repliesToId: origMessage.id, + }, + }); + } + + return { + ...newMessage, + channelId: newMessage.channelId!, + userId: newMessage.userId!, + replies: origMessage + ? { + messageId: newMessage.id, + repliesToId: origMessage?.id, + repliesToText: origMessage?.text, + } + : null, + }; + } catch (error) { + return null; + } +} diff --git a/concord-server/src/services/realtime.ts b/concord-server/src/services/realtime.ts index b99c5cd..434de72 100644 --- a/concord-server/src/services/realtime.ts +++ b/concord-server/src/services/realtime.ts @@ -1,88 +1,90 @@ import { readonly } from "zod"; const EVENTS = { - NEW_CHANNEL_MESSAGE: "new_channel_message", - DELETE_CHANNEL_MESSAGE: "delete_channel_message", -} - - + NEW_CHANNEL_MESSAGE: "new_channel_message", + DELETE_CHANNEL_MESSAGE: "delete_channel_message", +}; export async function sendMessageToChannel( - instanceId: string, - categoryId: string, - channelId: string, - message: any, - event: string, - io: any, + instanceId: string, + categoryId: string, + channelId: string, + message: any, + event: string, + io: any, ): Promise { - try { - - //TODO: implement middleware to replace this - if(EVENTS.NEW_CHANNEL_MESSAGE === event){ - throw new Error("Event not implemented"); - } - - //TODO: add prisma to save channel message to DB - - return new Promise((resolve) => { - io.to(instanceId).emit(event, message, (ack: any) => { - if (ack && ack.status === 'received') { - console.log(`Message ${ack.messageId} acknowledged by client.`); - resolve(true); - } else { - console.log('services::realtime::sendMessageToChannel No acknowledgment received from client.'); - resolve("no acknowledgment"); - } - }); - }); - } catch (err) { - const errMessage = err as Error; - if (errMessage.message === "Event not implemented") { - console.log(`services::realtime::sendMessageToChannel - Event not implemented. Attempted event: ${event}`) - return "event not implemented" - } - console.log("services::realtime::sendMessageToChannel - ", errMessage); - return false; - + try { + //TODO: implement middleware to replace this + if (EVENTS.NEW_CHANNEL_MESSAGE === event) { + throw new Error("Event not implemented"); } + + //TODO: add prisma to save channel message to DB + + return new Promise((resolve) => { + io.to(instanceId).emit(event, message, (ack: any) => { + if (ack && ack.status === "received") { + console.log(`Message ${ack.messageId} acknowledged by client.`); + resolve(true); + } else { + console.log( + "services::realtime::sendMessageToChannel No acknowledgment received from client.", + ); + resolve("no acknowledgment"); + } + }); + }); + } catch (err) { + const errMessage = err as Error; + if (errMessage.message === "Event not implemented") { + console.log( + `services::realtime::sendMessageToChannel - Event not implemented. Attempted event: ${event}`, + ); + return "event not implemented"; + } + console.log("services::realtime::sendMessageToChannel - ", errMessage); + return false; + } } export async function removeMessageFromChannel( - instanceId: string, - categoryId: string, - channelId: string, - messageId: string, - event: string, - io: any -): Promise{ - try { - - //TODO: implement middleware to replace this - if(EVENTS.DELETE_CHANNEL_MESSAGE === event){ - throw new Error("event not implemented"); - } - - //TODO: add prisma to flag a channel message as deleted - - return new Promise((resolve) => { - io.to(instanceId).emit(event, { messageId }, (ack: any) => { - if (ack && ack.status === 'received') { - console.log(`Message ${ack.messageId} acknowledged by client.`); - resolve(true); - } else { - console.log('services::realtime::deleteMessageFromChannel No acknowledgment received from client.'); - resolve("no acknowledgment"); - } - }); - }); - } catch (err) { - const errMessage = err as Error; - if (errMessage.message === "Event not implemented") { - console.log(`services::realtime::deleteMessageFromChannel - Event not implemented. Attempted event: ${event}`) - return false; - } - console.log("services::realtime::deleteMessageFromChannel - ", errMessage); - return false; - + instanceId: string, + categoryId: string, + channelId: string, + messageId: string, + event: string, + io: any, +): Promise { + try { + //TODO: implement middleware to replace this + if (EVENTS.DELETE_CHANNEL_MESSAGE === event) { + throw new Error("event not implemented"); } -} \ No newline at end of file + + //TODO: add prisma to flag a channel message as deleted + + return new Promise((resolve) => { + io.to(instanceId).emit(event, { messageId }, (ack: any) => { + if (ack && ack.status === "received") { + console.log(`Message ${ack.messageId} acknowledged by client.`); + resolve(true); + } else { + console.log( + "services::realtime::deleteMessageFromChannel No acknowledgment received from client.", + ); + resolve("no acknowledgment"); + } + }); + }); + } catch (err) { + const errMessage = err as Error; + if (errMessage.message === "Event not implemented") { + console.log( + `services::realtime::deleteMessageFromChannel - Event not implemented. Attempted event: ${event}`, + ); + return false; + } + console.log("services::realtime::deleteMessageFromChannel - ", errMessage); + return false; + } +} diff --git a/concord-server/src/services/userService.ts b/concord-server/src/services/userService.ts index 13e96d9..eec7a47 100644 --- a/concord-server/src/services/userService.ts +++ b/concord-server/src/services/userService.ts @@ -5,31 +5,35 @@ import { Role, UserAuth, } from "@prisma/client"; -import { CreateUserInput } from '../validators/userValidator'; +import { CreateUserInput } from "../validators/userValidator"; import shaHash from "../helper/hashing"; const prisma = new PrismaClient(); export async function createUser(data: CreateUserInput): Promise<{ - username: string, - nickname: string | null, - bio: string | null, - picture: string | null, - banner: string | null, - status: string, - admin: boolean + username: string; + nickname: string | null; + bio: string | null; + picture: string | null; + banner: string | null; + status: string; + admin: boolean; } | null> { const requestingUser = await getUserInformation(data.requestingUserId); - const requestingUserCredentials = await getUserCredentials(data.requestingUserId) - if (!requestingUser - || !requestingUserCredentials - || !requestingUser.admin - || requestingUserCredentials.token == null - || data.requestingUserToken != requestingUserCredentials.token) { + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { return null; } - if (await prisma.user.count({ where: { username: data.username }}) >= 1) { + if ((await prisma.user.count({ where: { username: data.username } })) >= 1) { return null; } @@ -45,13 +49,15 @@ export async function createUser(data: CreateUserInput): Promise<{ }, }); - if (!(await prisma.userAuth.create({ - data: { - userId: userData.id, - password: shaHash(data.passwordhash, userData.id), - token: null, - } - }))) { + if ( + !(await prisma.userAuth.create({ + data: { + userId: userData.id, + password: shaHash(data.passwordhash, userData.id), + token: null, + }, + })) + ) { return null; } @@ -59,52 +65,52 @@ export async function createUser(data: CreateUserInput): Promise<{ } export async function getUserCredentials(userId: string): Promise<{ - userId: string, - password: string, - token: string | null - } | null> { - try { - if (!userId) { - throw new Error("missing userId"); - } + userId: string; + password: string; + token: string | null; +} | null> { + try { + if (!userId) { + throw new Error("missing userId"); + } - const userAuth = await prisma.userAuth.findUnique({ - where: { - userId: userId, - }, - }); + const userAuth = await prisma.userAuth.findUnique({ + where: { + userId: userId, + }, + }); - if (!userAuth) { - throw new Error("could not find user credentials"); - } + if (!userAuth) { + throw new Error("could not find user credentials"); + } - return { - userId: userAuth.userId, - password: userAuth.password, - token: userAuth.token, - }; - } catch (err) { - const errMessage = err as Error; + return { + userId: userAuth.userId, + password: userAuth.password, + token: userAuth.token, + }; + } catch (err) { + const errMessage = err as Error; - if (errMessage.message === "missing userId") { - console.log("services::actions::getUserCredentials - missing userId"); - return null; - } - - if (errMessage.message === "could not find user credentials") { - console.log( - "services::actions::getUserCredentials - unable to find user credentials", - ); - return null; - } + if (errMessage.message === "missing userId") { + console.log("services::actions::getUserCredentials - missing userId"); + return null; + } + if (errMessage.message === "could not find user credentials") { console.log( - "services::actions::getUserCredentials - unknown error", - errMessage, + "services::actions::getUserCredentials - unable to find user credentials", ); return null; } + + console.log( + "services::actions::getUserCredentials - unknown error", + errMessage, + ); + return null; } +} export async function getUserInformation(userId: string): Promise<{ id: string; @@ -196,9 +202,9 @@ export async function getAllUsersFrom(instanceId: string): Promise< try { const instances = await prisma.instance.count({ where: { - id: instanceId - } - }) + id: instanceId, + }, + }); if (instances < 1) { throw new Error("could not find given instance id"); } @@ -218,8 +224,8 @@ export async function getAllUsersFrom(instanceId: string): Promise< const admins = await prisma.user.findMany({ where: { - admin: true - } + admin: true, + }, }); if (!admins) { throw new Error("could not get all admins"); @@ -249,13 +255,13 @@ export async function getAllUsersFrom(instanceId: string): Promise< ).includes(u.status as any) ? (u.status as "online" | "offline" | "dnd" | "idle" | "invis") : "offline", - role: adminRoles.map(r => ({ + role: adminRoles.map((r) => ({ userId: r.userId, instanceId: r.instanceId, })), - } - }) - ) + }; + }), + ); const userData = await Promise.all( users.map(async (u) => { diff --git a/concord-server/src/validators/userValidator.ts b/concord-server/src/validators/userValidator.ts index 463957a..9b9bfb6 100644 --- a/concord-server/src/validators/userValidator.ts +++ b/concord-server/src/validators/userValidator.ts @@ -1,12 +1,12 @@ -import { z } from 'zod' +import { z } from "zod"; export const queryUserByIdSchema = z.object({ - id: z.uuidv7() -}) + id: z.uuidv7(), +}); export const queryAllUsersByInstanceId = z.object({ - instanceId: z.uuidv7() -}) + instanceId: z.uuidv7(), +}); export const createUserSchema = z.object({ username: z.string().min(3).max(30), @@ -14,13 +14,17 @@ export const createUserSchema = z.object({ bio: z.string().max(500).optional(), picture: z.url().optional(), banner: z.url().optional(), - status: z.enum(['online', 'offline', 'dnd', 'idle', 'invis']).default('online'), + status: z + .enum(["online", "offline", "dnd", "idle", "invis"]) + .default("online"), admin: z.boolean().default(false), requestingUserId: z.uuidv7(), requestingUserToken: z.uuidv4(), passwordhash: z.string(), -}) +}); -export type QueryUserByIdInput = z.infer -export type QueryAllUsersByInstanceIdInput = z.infer -export type CreateUserInput = z.infer \ No newline at end of file +export type QueryUserByIdInput = z.infer; +export type QueryAllUsersByInstanceIdInput = z.infer< + typeof queryAllUsersByInstanceId +>; +export type CreateUserInput = z.infer; From 0ee3984262e14ab55a6ceb4b175ee2bdccd25349 Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 18:47:34 -0400 Subject: [PATCH 19/26] Message post and get by id + Deps fixed --- concord-server/.gitignore | 1 + concord-server/bun.lock | 85 +++++----- concord-server/package.json | 5 +- concord-server/schema.prisma | 4 + .../src/controller/messageController.ts | 21 +++ concord-server/src/routes/index.ts | 6 +- concord-server/src/routes/messageRoutes.ts | 86 ++++++++++ concord-server/src/routes/userRoutes.ts | 16 +- concord-server/src/services/message.ts | 78 --------- concord-server/src/services/messageService.ts | 160 ++++++++++++++++++ .../src/validators/messageValidator.ts | 13 ++ 11 files changed, 343 insertions(+), 132 deletions(-) create mode 100644 concord-server/src/controller/messageController.ts create mode 100644 concord-server/src/routes/messageRoutes.ts delete mode 100644 concord-server/src/services/message.ts create mode 100644 concord-server/src/services/messageService.ts diff --git a/concord-server/.gitignore b/concord-server/.gitignore index 506e4c3..9371620 100644 --- a/concord-server/.gitignore +++ b/concord-server/.gitignore @@ -1,2 +1,3 @@ # deps node_modules/ +generated/ \ No newline at end of file diff --git a/concord-server/bun.lock b/concord-server/bun.lock index 4a5362e..d3d7309 100644 --- a/concord-server/bun.lock +++ b/concord-server/bun.lock @@ -10,9 +10,10 @@ "@scalar/hono-api-reference": "^0.9.19", "@socket.io/bun-engine": "^0.0.3", "hono": "^4.9.9", - "hono-openapi": "^1.0.8", + "hono-openapi": "^1.1.0", "prisma": "^6.16.2", "socket.io": "^4.8.1", + "zod": "^4.1.11", }, "devDependencies": { "@types/bun": "latest", @@ -25,27 +26,27 @@ "@hono/zod-validator": ["@hono/zod-validator@0.7.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-uYGdgVib3RlGD698WR5dVM0zB3UuPY5vHKXffGUbUh7r4xY+mFIhF3/v4AcQVLrU5CQdBso8BJr4wuVoCrjTuQ=="], - "@prisma/client": ["@prisma/client@6.16.2", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw=="], + "@prisma/client": ["@prisma/client@6.16.2", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw=="], - "@prisma/config": ["@prisma/config@6.16.2", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.16.12", "empathic": "2.0.0" } }, "sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg=="], + "@prisma/config": ["@prisma/config@6.16.2", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.16.12", "empathic": "2.0.0" } }, ""], - "@prisma/debug": ["@prisma/debug@6.16.2", "", {}, "sha512-bo4/gA/HVV6u8YK2uY6glhNsJ7r+k/i5iQ9ny/3q5bt9ijCj7WMPUwfTKPvtEgLP+/r26Z686ly11hhcLiQ8zA=="], + "@prisma/debug": ["@prisma/debug@6.16.2", "", {}, ""], "@prisma/dmmf": ["@prisma/dmmf@6.16.2", "", {}, "sha512-o9ztgdbj2KZXl6DL+oP56TTC0poTLPns9/MeU761b49E1IQ/fd0jwdov1bidlNOiwio8Nsou23xNrYE/db10aA=="], "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2" } }, "sha512-DMgfafnG0zPd+QoAQOC0Trn1xlb0fVAfQi2MpkpzSf641KiVkVPkJRXDSbcTbxGxO2HRdd0vI9U6LlesWad4XA=="], - "@prisma/engines": ["@prisma/engines@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2", "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/fetch-engine": "6.16.2", "@prisma/get-platform": "6.16.2" } }, "sha512-7yf3AjfPUgsg/l7JSu1iEhsmZZ/YE00yURPjTikqm2z4btM0bCl2coFtTGfeSOWbQMmq45Jab+53yGUIAT1sjA=="], + "@prisma/engines": ["@prisma/engines@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2", "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/fetch-engine": "6.16.2", "@prisma/get-platform": "6.16.2" } }, ""], - "@prisma/engines-version": ["@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "", {}, "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA=="], + "@prisma/engines-version": ["@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "", {}, ""], - "@prisma/fetch-engine": ["@prisma/fetch-engine@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2", "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/get-platform": "6.16.2" } }, "sha512-wPnZ8DMRqpgzye758ZvfAMiNJRuYpz+rhgEBZi60ZqDIgOU2694oJxiuu3GKFeYeR/hXxso4/2oBC243t/whxQ=="], + "@prisma/fetch-engine": ["@prisma/fetch-engine@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2", "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/get-platform": "6.16.2" } }, ""], "@prisma/generator": ["@prisma/generator@6.16.2", "", {}, "sha512-7bwRmtMIgfe1rUynh1p9VlmYvEiidbRO6aBphPBS6YGEGSvNe8+QExbRpsqFlFBvIX76BhZCxuEj7ZwALMYRKQ=="], "@prisma/generator-helper": ["@prisma/generator-helper@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2", "@prisma/dmmf": "6.16.2", "@prisma/generator": "6.16.2" } }, "sha512-8tVnWM8ETJNrvI5CT9eKCW23+aPLNkidC+g9NJn7ghXm60Q7GGlLX5tmvn5dE8tXvs/FSX3MN7KNmNJpOr89Hw=="], - "@prisma/get-platform": ["@prisma/get-platform@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2" } }, "sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA=="], + "@prisma/get-platform": ["@prisma/get-platform@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2" } }, ""], "@prisma/internals": ["@prisma/internals@6.16.2", "", { "dependencies": { "@prisma/config": "6.16.2", "@prisma/debug": "6.16.2", "@prisma/dmmf": "6.16.2", "@prisma/driver-adapter-utils": "6.16.2", "@prisma/engines": "6.16.2", "@prisma/fetch-engine": "6.16.2", "@prisma/generator": "6.16.2", "@prisma/generator-helper": "6.16.2", "@prisma/get-platform": "6.16.2", "@prisma/prisma-schema-wasm": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/schema-engine-wasm": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/schema-files-loader": "6.16.2", "arg": "5.0.2", "prompts": "2.4.2" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-gwmWl7H8iTbi+58RXua5Lsus5LDbIZGO2wQ4RoSX9YtEbKWHwRP8TUzTVLwRNeJ2DHwfnzhTLrUnybwotqiACg=="], @@ -71,7 +72,7 @@ "@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.8", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-80ap74p5oy/SU4al5HkPwO5+NbN2wH/FBr6kwaE5ROq7AvcDFaxzUfTazewroNaCotbvdGcvzXb9oEoOIyfC/Q=="], - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, ""], "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], @@ -79,9 +80,9 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, ""], - "@types/react": ["@types/react@19.1.14", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-ukd93VGzaNPMAUPy0gRDSC57UuQbnH9Kussp7HBjM06YFi9uZTFhOvMSO2OKqXm1rSgzOE+pVx1k1PYHGwlc8Q=="], + "@types/react": ["@types/react@19.1.14", "", { "dependencies": { "csstype": "^3.0.2" } }, ""], "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], @@ -93,47 +94,47 @@ "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], - "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], + "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, ""], - "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], + "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, ""], - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, ""], - "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, ""], - "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], + "confbox": ["confbox@0.2.2", "", {}, ""], - "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + "consola": ["consola@3.4.2", "", {}, ""], "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "csstype": ["csstype@3.1.3", "", {}, ""], "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], + "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, ""], - "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "defu": ["defu@6.1.4", "", {}, ""], - "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + "destr": ["destr@2.0.5", "", {}, ""], - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "dotenv": ["dotenv@16.6.1", "", {}, ""], - "effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="], + "effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, ""], - "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + "empathic": ["empathic@2.0.0", "", {}, ""], "engine.io": ["engine.io@6.6.4", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1" } }, "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g=="], "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], - "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], + "exsolve": ["exsolve@1.0.7", "", {}, ""], - "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, ""], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -145,15 +146,15 @@ "fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], - "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": "dist/cli.mjs" }, ""], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "hono": ["hono@4.9.9", "", {}, "sha512-Hxw4wT6zjJGZJdkJzAx9PyBdf7ZpxaTSA0NfxqjLghwMrLBX8p33hJBzoETRakF3UJu6OdNQBZAlNSkGqKFukw=="], + "hono": ["hono@4.9.9", "", {}, ""], - "hono-openapi": ["hono-openapi@1.0.8", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.1", "@standard-community/standard-openapi": "^0.2.4", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-JjSdT4sNUgxQGgwO90boRLfnrVYp3ge+Y/vHqPMJrAZuaIhKekAVipoeJ8AgpTyK+ZaxPzqdcmDBA9L+Ce3X9Q=="], + "hono-openapi": ["hono-openapi@1.1.0", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.8", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-eA5hN8D2O30EkPPUxWFilcZcThAe81TShbH38Y183ZZp8WkgMh4BrPEDeZ/EFN2tyDi3cmTgKTa3+oStyJX0UA=="], - "jiti": ["jiti@2.6.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="], + "jiti": ["jiti@2.6.0", "", { "bin": "lib/jiti-cli.mjs" }, ""], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -175,37 +176,37 @@ "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, ""], - "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="], + "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": "dist/cli.mjs" }, ""], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + "ohash": ["ohash@2.0.11", "", {}, ""], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pathe": ["pathe@2.0.3", "", {}, ""], - "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, ""], - "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, ""], "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], - "prisma": ["prisma@6.16.2", "", { "dependencies": { "@prisma/config": "6.16.2", "@prisma/engines": "6.16.2" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA=="], + "prisma": ["prisma@6.16.2", "", { "dependencies": { "@prisma/config": "6.16.2", "@prisma/engines": "6.16.2" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": "build/index.js" }, ""], "prisma-zod-generator": ["prisma-zod-generator@1.22.1", "", { "dependencies": { "@prisma/client": "^6.16.2", "@prisma/generator-helper": "^6.16.2", "@prisma/internals": "^6.16.2", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "node-fetch": "^3.3.2", "prettier": "^3.6.2", "tslib": "^2.8.1" }, "peerDependencies": { "zod": ">=3.25.0 <5" }, "bin": { "prisma-zod-generator": "lib/generator.js" } }, "sha512-nBr00sfR8onGCD5eIDLHoFrpeJTSuZxSeaO61Zg6CAEyXPR51gpkO1ev9huG7+tsV+mm8me8VNl8hMcVtWl8FA=="], "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], - "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "pure-rand": ["pure-rand@6.1.0", "", {}, ""], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], - "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, ""], - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "readdirp": ["readdirp@4.1.2", "", {}, ""], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -217,13 +218,13 @@ "socket.io-parser": ["socket.io-parser@4.2.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" } }, "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew=="], - "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], + "tinyexec": ["tinyexec@1.0.1", "", {}, ""], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], - "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], + "undici-types": ["undici-types@7.12.0", "", {}, ""], "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], diff --git a/concord-server/package.json b/concord-server/package.json index 82af71b..fe84cc1 100644 --- a/concord-server/package.json +++ b/concord-server/package.json @@ -10,9 +10,10 @@ "@scalar/hono-api-reference": "^0.9.19", "@socket.io/bun-engine": "^0.0.3", "hono": "^4.9.9", - "hono-openapi": "^1.0.8", + "hono-openapi": "^1.1.0", "prisma": "^6.16.2", - "socket.io": "^4.8.1" + "socket.io": "^4.8.1", + "zod": "^4.1.11" }, "devDependencies": { "@types/bun": "latest", diff --git a/concord-server/schema.prisma b/concord-server/schema.prisma index fdfebd7..ab40935 100644 --- a/concord-server/schema.prisma +++ b/concord-server/schema.prisma @@ -1,4 +1,8 @@ generator client { + provider = "prisma-client-js" +} + +generator zod { provider = "prisma-zod-generator" } diff --git a/concord-server/src/controller/messageController.ts b/concord-server/src/controller/messageController.ts new file mode 100644 index 0000000..d8c9a27 --- /dev/null +++ b/concord-server/src/controller/messageController.ts @@ -0,0 +1,21 @@ +import { getMessageInformation, sendMessageToChannel } from "../services/messageService"; + +export async function fetchMessageData(id:string) { + return await getMessageInformation(id); +} + +export async function sendMessage( + channelId: string, + userId: string, + content: string, + token: string, + repliedMessageId: string | null +) { + return await sendMessageToChannel( + channelId, + userId, + content, + token, + repliedMessageId + ); +} \ No newline at end of file diff --git a/concord-server/src/routes/index.ts b/concord-server/src/routes/index.ts index 550c3b3..0a8b476 100644 --- a/concord-server/src/routes/index.ts +++ b/concord-server/src/routes/index.ts @@ -1,9 +1,11 @@ //place exported routes below this line import { Hono } from "hono"; -import actions from "./userRoutes"; +import userRoutes from "./userRoutes"; +import messageRoutes from "./messageRoutes"; const routes = new Hono(); -routes.route("/", actions); +routes.route("/user", userRoutes); +routes.route("/message", messageRoutes); export default routes; diff --git a/concord-server/src/routes/messageRoutes.ts b/concord-server/src/routes/messageRoutes.ts new file mode 100644 index 0000000..3b7085c --- /dev/null +++ b/concord-server/src/routes/messageRoutes.ts @@ -0,0 +1,86 @@ +import { Hono } from "hono"; +import { describeResponse, describeRoute, resolver } from "hono-openapi"; +import { getMessageByIdSchema, sendMessageSchema } from "../validators/messageValidator"; +import { zValidator } from "@hono/zod-validator"; +import { fetchMessageData, sendMessage } from "../controller/messageController"; + +const messageRoutes = new Hono(); + +messageRoutes.get( + "/:id", + describeRoute({ + description: "Get message by id", + responses: { + 200: { + description: "Success getting message", + content: { + "application/json": { schema: resolver(getMessageByIdSchema) } + } + }, + 404: { + description: "Message id not found", + content: { + "application/json": { schema: resolver(getMessageByIdSchema) } + } + } + } + }), + zValidator("param", getMessageByIdSchema), + async (c) => { + const id = c.req.param("id"); + const messageData = await fetchMessageData(id); + + if (messageData) { + return c.json(messageData, 200); + } else { + return c.json({ error: "Message not found" }, 404); + } + } +) + +messageRoutes.post( + "", + describeRoute({ + description: "Send a message to a channel", + responses: { + 201: { + description: "Message sent successfully", + content: { + "application/json": { schema: resolver(sendMessageSchema) } + } + }, + 401: { + description: "Unauthorized - invalid token or user credentials", + content: { + "application/json": { schema: { type: "object", properties: { error: { type: "string" } } } } + } + }, + 500: { + description: "Server error", + content: { + "application/json": { schema: { type: "object", properties: { error: { type: "string" } } } } + } + } + } + }), + zValidator("json", sendMessageSchema), + async (c) => { + const { channelId, userId, content, token, repliedMessageId } = await c.req.json(); + + const result = await sendMessage( + channelId, + userId, + content, + token, + repliedMessageId || null + ); + + if (result) { + return c.json(result, 201); + } else { + return c.json({ error: "Failed to send message. Check your credentials and try again." }, 401); + } + } +) + +export default messageRoutes; \ No newline at end of file diff --git a/concord-server/src/routes/userRoutes.ts b/concord-server/src/routes/userRoutes.ts index fd3fbf4..a2c1fd9 100644 --- a/concord-server/src/routes/userRoutes.ts +++ b/concord-server/src/routes/userRoutes.ts @@ -11,10 +11,10 @@ import { } from "../validators/userValidator"; import { zValidator } from "@hono/zod-validator"; import { describeRoute, resolver } from "hono-openapi"; -const actions = new Hono(); +const userRoutes = new Hono(); -actions.get( - "user/:id", +userRoutes.get( + "/:id", describeRoute({ description: "Get user by id", responses: { @@ -44,8 +44,8 @@ actions.get( }, ); -actions.get( - "user", +userRoutes.get( + "", describeRoute({ description: "Get all users by instance id", responses: { @@ -73,8 +73,8 @@ actions.get( }, ); -actions.post( - "user", +userRoutes.post( + "", describeRoute({ description: "Create a new user", responses: { @@ -107,4 +107,4 @@ actions.post( }, ); -export default actions; +export default userRoutes; diff --git a/concord-server/src/services/message.ts b/concord-server/src/services/message.ts deleted file mode 100644 index f90550e..0000000 --- a/concord-server/src/services/message.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - Message, - MessagePing, - PrismaClient, - Role, - Reply, -} from "@prisma/client"; -import { CreateUserInput } from "../validators/userValidator"; - -const prisma = new PrismaClient(); - -export async function sendMessageToChannel( - channelId: string, - userId: string, - content: string, - repliedMessageId: string | null, -): Promise<{ - id: string; - channelId: string; - userId: string; - text: string; - deleted: boolean; - replies: null | { - messageId: string; - repliesToId: string; - repliesToText: string; - }; -} | null> { - try { - const newMessage = await prisma.message.create({ - data: { - channelId: channelId, - userId: userId, - text: content, - deleted: false, - }, - }); - - if (!newMessage) { - return null; - } - - let origMessage; - if (repliedMessageId) { - origMessage = await prisma.message.findUnique({ - where: { - id: repliedMessageId, - }, - }); - - if (!origMessage) { - throw new Error("could not find original message to reply to"); - } - - await prisma.reply.create({ - data: { - messageId: newMessage.id, - repliesToId: origMessage.id, - }, - }); - } - - return { - ...newMessage, - channelId: newMessage.channelId!, - userId: newMessage.userId!, - replies: origMessage - ? { - messageId: newMessage.id, - repliesToId: origMessage?.id, - repliesToText: origMessage?.text, - } - : null, - }; - } catch (error) { - return null; - } -} diff --git a/concord-server/src/services/messageService.ts b/concord-server/src/services/messageService.ts new file mode 100644 index 0000000..f75eead --- /dev/null +++ b/concord-server/src/services/messageService.ts @@ -0,0 +1,160 @@ +import { + PrismaClient, +} from "@prisma/client"; +import { getUserCredentials } from "./userService"; + +const prisma = new PrismaClient(); + +export async function getMessageInformation(id:string): Promise<{ + id: string, + channelId: string, + userId: string, + text: string, + deleted: boolean, + replies: null | { + messageId: string; + repliesToId: string; + repliesToText: string; + }; +} | null> { + try { + if (!id) { + throw new Error("missing messageId"); + } + + const message = await prisma.message.findUnique({ + where: { + id: id, + }, + }); + + if (!message) { + throw new Error("could not find message"); + } + + // Check if this message is a reply to another message + const replyData = await prisma.reply.findFirst({ + where: { + messageId: id, + }, + }); + + let originalMessage = null; + if (replyData) { + originalMessage = await prisma.message.findUnique({ + where: { + id: replyData.repliesToId, + }, + }); + } + + return { + id: message.id, + channelId: message.channelId!, + userId: message.userId!, + text: message.text, + deleted: message.deleted, + replies: originalMessage + ? { + messageId: message.id, + repliesToId: originalMessage.id, + repliesToText: originalMessage.text, + } + : null, + }; + } catch (err) { + const errMessage = err as Error; + + if (errMessage.message === "missing messageId") { + console.log("services::actions::getMessageInformation - missing messageId"); + return null; + } + + if (errMessage.message === "could not find message") { + console.log( + "services::actions::getMessageInformation - unable to find message" + ); + return null; + } + + console.log( + "services::actions::getMessageInformation - unknown error", + errMessage + ); + return null; + } +} + +export async function sendMessageToChannel( + channelId: string, + userId: string, + content: string, + token: string, + repliedMessageId: string | null, +): Promise<{ + id: string; + channelId: string; + userId: string; + text: string; + deleted: boolean; + replies: null | { + messageId: string; + repliesToId: string; + repliesToText: string; + }; +} | null> { + try { + const userCreds = await getUserCredentials(userId); + if (!userCreds || userCreds.token != token) { + return null; + } + + const newMessage = await prisma.message.create({ + data: { + channelId: channelId, + userId: userId, + text: content, + deleted: false, + }, + }); + + if (!newMessage) { + return null; + } + + let origMessage; + if (repliedMessageId) { + origMessage = await prisma.message.findUnique({ + where: { + id: repliedMessageId, + }, + }); + + if (!origMessage) { + throw new Error("could not find original message to reply to"); + } + + await prisma.reply.create({ + data: { + messageId: newMessage.id, + repliesToId: origMessage.id, + }, + }); + } + + return { + ...newMessage, + channelId: newMessage.channelId!, + userId: newMessage.userId!, + replies: origMessage + ? { + messageId: newMessage.id, + repliesToId: origMessage?.id, + repliesToText: origMessage?.text, + } + : null, + }; + } catch (error) { + return null; + } +} diff --git a/concord-server/src/validators/messageValidator.ts b/concord-server/src/validators/messageValidator.ts index e69de29..96774db 100644 --- a/concord-server/src/validators/messageValidator.ts +++ b/concord-server/src/validators/messageValidator.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +export const getMessageByIdSchema = z.object({ + id: z.uuidv7() +}) + +export const sendMessageSchema = z.object({ + channelId: z.uuidv7(), + userId: z.uuidv7(), + content: z.string(), + token: z.string(), + repliedMessageId: z.uuidv7().nullable().optional() +}) \ No newline at end of file From 74d0c502f36c498bf4083607b82ae3c64c11dbf4 Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 19:13:22 -0400 Subject: [PATCH 20/26] le super migration timestamp style on it --- .../20250927230554_timestampify/migration.sql | 71 ++++++++++++++++++ .../migration.sql | 26 +++++++ concord-server/schema.prisma | 75 +++++++++++++------ 3 files changed, 150 insertions(+), 22 deletions(-) create mode 100644 concord-server/migrations/20250927230554_timestampify/migration.sql create mode 100644 concord-server/migrations/20250927231139_pinnedidnullable/migration.sql diff --git a/concord-server/migrations/20250927230554_timestampify/migration.sql b/concord-server/migrations/20250927230554_timestampify/migration.sql new file mode 100644 index 0000000..7285123 --- /dev/null +++ b/concord-server/migrations/20250927230554_timestampify/migration.sql @@ -0,0 +1,71 @@ +/* + Warnings: + + - Added the required column `updatedAt` to the `Category` table without a default value. This is not possible if the table is not empty. + - Added the required column `pinnedId` to the `Channel` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `Channel` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `Instance` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `Message` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `Reply` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `Role` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `User` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `UserAuth` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "public"."Category" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."Channel" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "pinnedId" TEXT, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."Instance" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."Message" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."MessagePing" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."Reply" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."Role" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."User" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "public"."UserAuth" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- CreateTable +CREATE TABLE "public"."ChannelPin" ( + "messageId" TEXT NOT NULL, + "channelId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateIndex +CREATE UNIQUE INDEX "ChannelPin_messageId_key" ON "public"."ChannelPin"("messageId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ChannelPin_channelId_key" ON "public"."ChannelPin"("channelId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ChannelPin_messageId_channelId_key" ON "public"."ChannelPin"("messageId", "channelId"); + +-- AddForeignKey +ALTER TABLE "public"."ChannelPin" ADD CONSTRAINT "ChannelPin_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "public"."Message"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."ChannelPin" ADD CONSTRAINT "ChannelPin_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "public"."Channel"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/concord-server/migrations/20250927231139_pinnedidnullable/migration.sql b/concord-server/migrations/20250927231139_pinnedidnullable/migration.sql new file mode 100644 index 0000000..4614fa0 --- /dev/null +++ b/concord-server/migrations/20250927231139_pinnedidnullable/migration.sql @@ -0,0 +1,26 @@ +-- AlterTable +ALTER TABLE "public"."Category" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "public"."Channel" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "public"."Channel" ALTER COLUMN "pinnedId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "public"."Instance" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "public"."Message" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "public"."Reply" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "public"."Role" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "public"."User" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "public"."UserAuth" ALTER COLUMN "updatedAt" DROP DEFAULT; diff --git a/concord-server/schema.prisma b/concord-server/schema.prisma index ab40935..a4fbfa8 100644 --- a/concord-server/schema.prisma +++ b/concord-server/schema.prisma @@ -12,11 +12,13 @@ datasource db { } model Instance { - id String @id @default(uuid(7)) - name String - icon String? - Role Role[] - Category Category[] + id String @id @default(uuid(7)) + name String + icon String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + Role Role[] + Category Category[] } model User { @@ -28,6 +30,8 @@ model User { banner String? admin Boolean status String // online/offline/dnd/idle/invis + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt Role Role[] UserAuth UserAuth? Message Message[] @@ -35,20 +39,24 @@ model User { } model Role { - User User @relation(fields: [userId], references: [id]) - userId String - Instance Instance @relation(fields: [instanceId], references: [id]) - instanceId String - type String + User User @relation(fields: [userId], references: [id]) + userId String + Instance Instance @relation(fields: [instanceId], references: [id]) + instanceId String + type String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@unique([userId, instanceId]) } model UserAuth { - User User @relation(fields: [userId], references: [id]) - userId String - password String // HASHED PASSWORD AS STRING USING SHA-256 - token String? // current user token + User User @relation(fields: [userId], references: [id]) + userId String + password String // HASHED PASSWORD AS STRING USING SHA-256 + token String? // current user token + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@unique([userId]) } @@ -59,17 +67,34 @@ model Category { instanceId String? name String position Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt Channel Channel[] } model Channel { - id String @id @default(uuid(7)) + id String @id @default(uuid(7)) type String - Category Category? @relation(fields: [categoryId], references: [id]) + Category Category? @relation(fields: [categoryId], references: [id]) categoryId String? name String description String + pinnedId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt Message Message[] + ChannelPin ChannelPin? +} + +model ChannelPin { + messageId String @unique + channelId String @unique + + Message Message @relation(fields: [messageId], references: [id]) + Channel Channel @relation(fields: [channelId], references: [id]) + createdAt DateTime @default(now()) + + @@unique([messageId, channelId]) } model Message { @@ -80,25 +105,31 @@ model Message { userId String deleted Boolean text String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt replies Reply? @relation("MessageToReply") repliedTo Reply? @relation("ReplyToMessage") MessagePing MessagePing[] + ChannelPin ChannelPin? } model Reply { - message Message @relation("MessageToReply", fields: [messageId], references: [id]) //message text - messageId String @unique //message id of the reply - repliesTo Message @relation("ReplyToMessage", fields: [repliesToId], references: [id]) //message id that this message replies to - repliesToId String @unique //replies to this message id + message Message @relation("MessageToReply", fields: [messageId], references: [id]) //message text + messageId String @unique //message id of the reply + repliesTo Message @relation("ReplyToMessage", fields: [repliesToId], references: [id]) //message id that this message replies to + repliesToId String @unique //replies to this message id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@unique([messageId, repliesToId]) } model MessagePing { - Message Message @relation(fields: [messageId], references: [id]) + Message Message @relation(fields: [messageId], references: [id]) messageId String - PingsUser User @relation(fields: [pingsUserId], references: [id]) + PingsUser User @relation(fields: [pingsUserId], references: [id]) pingsUserId String + createdAt DateTime @default(now()) @@unique([messageId, pingsUserId]) } From 9b46852e20c081c490589e83a38233a9711f95d1 Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 19:37:56 -0400 Subject: [PATCH 21/26] Get up to 50 messages before ISO 8601 timestamp --- .../src/controller/messageController.ts | 6 +- concord-server/src/routes/messageRoutes.ts | 39 ++++++++++- concord-server/src/services/messageService.ts | 68 +++++++++++++++++++ .../src/validators/messageValidator.ts | 7 ++ 4 files changed, 117 insertions(+), 3 deletions(-) diff --git a/concord-server/src/controller/messageController.ts b/concord-server/src/controller/messageController.ts index d8c9a27..e96b404 100644 --- a/concord-server/src/controller/messageController.ts +++ b/concord-server/src/controller/messageController.ts @@ -1,9 +1,13 @@ -import { getMessageInformation, sendMessageToChannel } from "../services/messageService"; +import { getMessageInformation, getMessagesBefore, sendMessageToChannel } from "../services/messageService"; export async function fetchMessageData(id:string) { return await getMessageInformation(id); } +export async function fetchMessagesBefore(date:string, channelId:string) { + return getMessagesBefore(date, channelId); +} + export async function sendMessage( channelId: string, userId: string, diff --git a/concord-server/src/routes/messageRoutes.ts b/concord-server/src/routes/messageRoutes.ts index 3b7085c..2733231 100644 --- a/concord-server/src/routes/messageRoutes.ts +++ b/concord-server/src/routes/messageRoutes.ts @@ -1,8 +1,8 @@ import { Hono } from "hono"; import { describeResponse, describeRoute, resolver } from "hono-openapi"; -import { getMessageByIdSchema, sendMessageSchema } from "../validators/messageValidator"; +import { getMessageByIdSchema, getMessagesBeforeDate, sendMessageSchema } from "../validators/messageValidator"; import { zValidator } from "@hono/zod-validator"; -import { fetchMessageData, sendMessage } from "../controller/messageController"; +import { fetchMessageData, fetchMessagesBefore, sendMessage } from "../controller/messageController"; const messageRoutes = new Hono(); @@ -38,6 +38,41 @@ messageRoutes.get( } ) +messageRoutes.get( + "", + describeRoute({ + description: "Get up to 50 messages prior to given datetime", + responses: { + 200: { + description: "Success getting up to 50 messages", + content: { + "application/json": { schema: resolver(getMessagesBeforeDate) } + } + } + } + }), + zValidator("query", getMessagesBeforeDate), + async (c) => { + const date = c.req.query("date"); + if (!date) { + return c.json({ error: "date not provided" }, 400); + } + + const channelId = c.req.query("channelId"); + if (!channelId) { + return c.json({ error: "channelId not provided" }, 400); + } + + const messagesArr = await fetchMessagesBefore(date, channelId); + + if (messagesArr) { + return c.json(messagesArr, 200); + } else { + return c.json({ error: "Failed to fetch messages" }, 500); + } + } +) + messageRoutes.post( "", describeRoute({ diff --git a/concord-server/src/services/messageService.ts b/concord-server/src/services/messageService.ts index f75eead..33c3ca1 100644 --- a/concord-server/src/services/messageService.ts +++ b/concord-server/src/services/messageService.ts @@ -85,6 +85,74 @@ export async function getMessageInformation(id:string): Promise<{ } } +export async function getMessagesBefore(date:string, channelId:string) { + try { + if (!date || !channelId) { + throw new Error("missing date or channelId"); + } + + const messages = await prisma.message.findMany({ + where: { + channelId: channelId, + createdAt: { + lt: new Date(date), + }, + }, + orderBy: { + createdAt: "desc", + }, + take: 50, + }); + + const messageInformationPromises = messages.map(async (message) => { + const replyData = await prisma.reply.findFirst({ + where: { + messageId: message.id, + }, + }); + + let originalMessage = null; + if (replyData) { + originalMessage = await prisma.message.findUnique({ + where: { + id: replyData.repliesToId, + }, + }); + } + + return { + id: message.id, + channelId: message.channelId!, + userId: message.userId!, + text: message.text, + deleted: message.deleted, + replies: originalMessage + ? { + messageId: message.id, + repliesToId: originalMessage.id, + repliesToText: originalMessage.text, + } + : null, + }; + }); + + return Promise.all(messageInformationPromises); + } catch (err) { + const errMessage = err as Error; + + if (errMessage.message === "missing date or channelId") { + console.log("services::actions::getMessagesBefore - missing date or channelId"); + return null; + } + + console.log( + "services::actions::getMessagesBefore - unknown error", + errMessage + ); + return null; + } +} + export async function sendMessageToChannel( channelId: string, userId: string, diff --git a/concord-server/src/validators/messageValidator.ts b/concord-server/src/validators/messageValidator.ts index 96774db..83e4920 100644 --- a/concord-server/src/validators/messageValidator.ts +++ b/concord-server/src/validators/messageValidator.ts @@ -4,6 +4,13 @@ export const getMessageByIdSchema = z.object({ id: z.uuidv7() }) +export const getMessagesBeforeDate = z.object({ + date: z.string().refine((val) => !isNaN(Date.parse(val)), { + message: "Invalid date string format" + }), + channelId: z.uuidv7() +}) + export const sendMessageSchema = z.object({ channelId: z.uuidv7(), userId: z.uuidv7(), From d1273ca4ec61711d4847c8ad761db77c95d2eb89 Mon Sep 17 00:00:00 2001 From: PrimarchPaul Date: Sat, 27 Sep 2025 20:45:36 -0400 Subject: [PATCH 22/26] feat: category and channel endpoints --- .../src/controller/categoryController.ts | 39 ++ .../src/controller/channelController.ts | 38 ++ concord-server/src/routes/categoryRoutes.ts | 254 ++++++++++ concord-server/src/routes/channelRoutes.ts | 264 ++++++++++ concord-server/src/routes/index.ts | 2 + concord-server/src/services/channelService.ts | 450 ++++++++++++++++++ concord-server/src/services/userService.ts | 1 + .../src/validators/categoryValidator.ts | 55 +++ .../src/validators/channelValidator.ts | 52 ++ .../src/validators/userValidator.ts | 2 +- 10 files changed, 1156 insertions(+), 1 deletion(-) create mode 100644 concord-server/src/controller/categoryController.ts create mode 100644 concord-server/src/controller/channelController.ts create mode 100644 concord-server/src/routes/categoryRoutes.ts create mode 100644 concord-server/src/routes/channelRoutes.ts create mode 100644 concord-server/src/services/channelService.ts create mode 100644 concord-server/src/validators/categoryValidator.ts create mode 100644 concord-server/src/validators/channelValidator.ts diff --git a/concord-server/src/controller/categoryController.ts b/concord-server/src/controller/categoryController.ts new file mode 100644 index 0000000..7177e5f --- /dev/null +++ b/concord-server/src/controller/categoryController.ts @@ -0,0 +1,39 @@ +import{ + createCategory, + getCategory, + getCategoriesByInstance, + updateCategory, + deleteCategory, + deleteAllCategoriesFromInstance, +} from "../services/channelService"; +import{ + CreateCategoryInput, + UpdateCategoryInput, + DeleteCategoryInput, + DeleteCategoriesByInstanceIdInput +} from "../validators/categoryValidator"; + +export async function createNewCategory(data: CreateCategoryInput) { + return await createCategory(data); +} + +export async function fetchCategoryData(id: string) { + return await getCategory(id); +} + +export async function fetchCategoriesByInstance(instanceId: string) { + return await getCategoriesByInstance(instanceId); +} + +export async function updateExistingCategory(data: UpdateCategoryInput) { + return await updateCategory(data); +} + +export async function deleteExistingCategory(data: DeleteCategoryInput) { + return await deleteCategory(data); +} + +export async function deleteAllCategoriesByInstance(data: DeleteCategoriesByInstanceIdInput) { + return await deleteAllCategoriesFromInstance(data); +} + diff --git a/concord-server/src/controller/channelController.ts b/concord-server/src/controller/channelController.ts new file mode 100644 index 0000000..b1c6cce --- /dev/null +++ b/concord-server/src/controller/channelController.ts @@ -0,0 +1,38 @@ +import{ + createChannel, + getChannel, + getChannelsByCategory, + updateChannel, + deleteChannel, + deleteAllChannelsFromCategory +} from "../services/channelService"; +import { + CreateChannelInput, + UpdateChannelInput, + DeleteChannelInput, + DeleteChannelsByCategoryIdInput +} from "../validators/channelValidator"; + +export async function createNewChannel(data: CreateChannelInput) { + return await createChannel(data); +} + +export async function fetchChannelData(id: string) { + return await getChannel(id); +} + +export async function fetchChannelsByCategory(categoryId: string) { + return await getChannelsByCategory(categoryId); +} + +export async function updateExistingChannel(data: UpdateChannelInput) { + return await updateChannel(data); +} + +export async function deleteExistingChannel(data: DeleteChannelInput) { + return await deleteChannel(data); +} + +export async function deleteAllChannelsByCategory(data: DeleteChannelsByCategoryIdInput) { + return await deleteAllChannelsFromCategory(data); +} \ No newline at end of file diff --git a/concord-server/src/routes/categoryRoutes.ts b/concord-server/src/routes/categoryRoutes.ts new file mode 100644 index 0000000..5db776f --- /dev/null +++ b/concord-server/src/routes/categoryRoutes.ts @@ -0,0 +1,254 @@ +import { + createNewCategory, + fetchCategoryData, + fetchCategoriesByInstance, + updateExistingCategory, + deleteExistingCategory, + deleteAllCategoriesByInstance, +} from "../controller/categoryController"; + +import { + createCategorySchema, + CreateCategoryInput, + UpdateCategoryInput, + DeleteCategoriesByInstanceIdInput, +} from "../validators/categoryValidator"; +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { describeRoute, resolver } from "hono-openapi"; +const categoryRoutes = new Hono() + +categoryRoutes.post( + "/category/create", + describeRoute({ + description: "Create a new category", + responses: { + 200: { + description: "Success creating category", + content: { + "application/json": { schema: resolver(createCategorySchema) }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(createCategorySchema)}, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(createCategorySchema)}, + }, + }, + 404: { + description: "User Id not found", + content: { + "application/json": { schema: resolver(createCategorySchema)}, + }, + }, + }, + }), + zValidator("json", createCategorySchema), + async (c) => { + const data = c.req.valid("json") as CreateCategoryInput; + const categoryData = await createNewCategory(data); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Failed to create category" }, 400); + } + } +) + +categoryRoutes.get( + "/:id", + describeRoute({ + description: "Get category by id", + responses: { + 200: { + description: "Success getting category", + content: { + "application/json": { schema: resolver(createCategorySchema) }, + }, + }, + 404: { + description: "Category id not found", + content: { + "application/json": { schema: resolver(createCategorySchema) }, + }, + }, + }, + }), + async (c) => { + const id = c.req.param("id"); + const categoryData = await fetchCategoryData(id); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Category not found" }, 404); + } + } +); + +categoryRoutes.get( + "", + describeRoute({ + description: "Get all categories by instance id", + responses: { + 200: { + description: "Success getting all categories in instance", + content: { + "application/json": { schema: resolver(createCategorySchema) }, + }, + }, + }, + }), + zValidator("query", createCategorySchema), + async (c) => { + const instanceId = c.req.query("instanceId"); + if (!instanceId) { + return c.json({ error: "No instance id provided" }, 400); + } + + const categoryData = await fetchCategoriesByInstance(instanceId); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Error getting all categories from instance" }, 500); + } + } +); + +categoryRoutes.put( + "/category/update", + describeRoute({ + description: "Update an existing category", + responses: { + 200: { + description: "Success updating category", + content: { + "application/json": { schema: resolver(createCategorySchema) }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(createCategorySchema)}, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(createCategorySchema)}, + }, + }, + 404: { + description: "Category id or User Id not found", + content: { + "application/json": { schema: resolver(createCategorySchema)}, + }, + }, + }, + }), + zValidator("json", createCategorySchema), + async (c) => { + const data = c.req.valid("json") as UpdateCategoryInput; + const categoryData = await updateExistingCategory(data); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Failed to update category" }, 400); + } + } +); + +categoryRoutes.delete( + "/category/delete", + describeRoute({ + description: "Delete an existing category", + responses: { + 200: { + description: "Success deleting category", + content: { + "application/json": { schema: resolver(createCategorySchema) }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(createCategorySchema)}, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(createCategorySchema)}, + }, + }, + 404: { + description: "Category id or User Id not found", + content: { + "application/json": { schema: resolver(createCategorySchema)}, + }, + }, + }, + }), + zValidator("json", createCategorySchema), + async (c) => { + const data = c.req.valid("json") as UpdateCategoryInput; + const categoryData = await deleteExistingCategory(data); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Failed to delete category" }, 400); + } + } +) + +categoryRoutes.delete( + "/categories/delete/:id", + describeRoute({ + description: "Delete all categories by instance id", + responses: { + 200: { + description: "Success deleting all categories in instance", + content: { + "application/json": { schema: resolver(createCategorySchema) }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(createCategorySchema)}, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(createCategorySchema)}, + }, + }, + 404: { + description: "Instance id or User Id not found", + content: { + "application/json": { schema: resolver(createCategorySchema)}, + }, + }, + }, + }), + zValidator("json", createCategorySchema), + async (c) => { + const data = c.req.valid("json") as DeleteCategoriesByInstanceIdInput; + const categoryData = await deleteAllCategoriesByInstance(data); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Failed to delete categories" }, 400); + } + } +) + + + +export { categoryRoutes }; \ No newline at end of file diff --git a/concord-server/src/routes/channelRoutes.ts b/concord-server/src/routes/channelRoutes.ts new file mode 100644 index 0000000..b8a3dd6 --- /dev/null +++ b/concord-server/src/routes/channelRoutes.ts @@ -0,0 +1,264 @@ +import { + createNewChannel, + fetchChannelData, + fetchChannelsByCategory, + updateExistingChannel, + deleteExistingChannel, + deleteAllChannelsByCategory, +} from "../controller/channelController"; + +import { + createChannelSchema, + getChannelSchema, + getChannelsByCategoryIdSchema, + updateChannelSchema, + deleteChannelSchema, + deleteChannelsByCategoryIdSchema, + + CreateChannelInput, + GetChannelInput, + GetChannelsByCategoryIdInput, + UpdateChannelInput, + DeleteChannelInput, + DeleteChannelsByCategoryIdInput, +} from "../validators/channelValidator"; +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { describeRoute, resolver } from "hono-openapi"; + +const channelRoutes = new Hono() + +channelRoutes.post( + "/channel/create", + describeRoute({ + description: "Create a new channel", + responses: { + 200: { + description: "Success creating channel", + content: { + "application/json": { schema: resolver(createChannelSchema) }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(createChannelSchema)}, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(createChannelSchema)}, + }, + }, + 404: { + description: "User Id not found", + content: { + "application/json": { schema: resolver(createChannelSchema)}, + }, + }, + }, + }), + zValidator("json", createChannelSchema), + async (c) => { + const data = c.req.valid("json") as CreateChannelInput; + const channelData = await createNewChannel(data); + if (channelData) { + return c.json(channelData); + } else { + return c.json({ error: "Failed to create channel" }, 400); + } + } +) + +channelRoutes.get( + "/:id", + describeRoute({ + description: "Get channel by id", + responses: { + 200: { + description: "Success getting channel", + content: { + "application/json": { schema: resolver(getChannelSchema) }, + }, + }, + 404: { + description: "Channel id not found", + content: { + "application/json": { schema: resolver(getChannelSchema) }, + }, + }, + }, + }), + async (c) => { + const id = c.req.param("id"); + const channelData = await fetchChannelData(id); + if (channelData) { + return c.json(channelData); + } else { + return c.json({ error: "Channel not found" }, 404); + } + } +); + +channelRoutes.get( + "", + describeRoute({ + description: "Get all channels by category id", + responses: { + 200: { + description: "Success getting all channels in category", + content: { + "application/json": { schema: resolver(getChannelsByCategoryIdSchema) }, + }, + }, + }, + }), + zValidator("query", getChannelsByCategoryIdSchema), + async (c) => { + const categoryId = c.req.query("categoryId"); + if (!categoryId) { + return c.json({ error: "No category id provided" }, 400); + } + + const channels = await fetchChannelsByCategory(categoryId); + if (channels) { + return c.json(channels); + } else { + return c.json({ error: "Error getting channels from category" }, 500); + } + } +); + +channelRoutes.put( + "/channel/update", + describeRoute({ + description: "Update an existing channel", + responses: { + 200: { + description: "Success updating channel", + content: { + "application/json": { schema: resolver(updateChannelSchema) }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(updateChannelSchema)}, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(updateChannelSchema)}, + }, + }, + 404: { + description: "Channel id or User Id not found", + content: { + "application/json": { schema: resolver(updateChannelSchema)}, + }, + }, + }, + }), + zValidator("json", updateChannelSchema), + async (c) => { + const data = c.req.valid("json") as UpdateChannelInput; + const result = await updateExistingChannel(data); + if (result) { + return c.json(result); + } else { + return c.json({ error: "Failed to update channel" }, 400); + } + } +); + +channelRoutes.delete( + "/channel/delete", + describeRoute({ + description: "Delete an existing channel", + responses: { + 200: { + description: "Success deleting channel", + content: { + "application/json": { schema: resolver(deleteChannelSchema) }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(deleteChannelSchema)}, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(deleteChannelSchema)}, + }, + }, + 404: { + description: "Channel id or User Id not found", + content: { + "application/json": { schema: resolver(deleteChannelSchema)}, + }, + }, + }, + }), + zValidator("json", deleteChannelSchema), + async (c) => { + const data = c.req.valid("json") as DeleteChannelInput; + const result = await deleteExistingChannel(data); + if (result) { + return c.json({ success: true }); + } else { + return c.json({ error: "Failed to delete channel" }, 400); + } + } +); + +channelRoutes.delete( + "/channels/delete-by-category", + describeRoute({ + description: "Delete all channels by category id", + responses: { + 200: { + description: "Success deleting all channels in category", + content: { + "application/json": { schema: resolver(deleteChannelsByCategoryIdSchema) }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(deleteChannelsByCategoryIdSchema)}, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(deleteChannelsByCategoryIdSchema)}, + }, + }, + 404: { + description: "Category id or User Id not found", + content: { + "application/json": { schema: resolver(deleteChannelsByCategoryIdSchema)}, + }, + }, + }, + }), + zValidator("json", deleteChannelsByCategoryIdSchema), + async (c) => { + const data = c.req.valid("json") as DeleteChannelsByCategoryIdInput; + const result = await deleteAllChannelsByCategory(data); + if (result) { + return c.json({ success: true }); + } else { + return c.json({ error: "Failed to delete channels" }, 400); + } + } +) + + + +export default channelRoutes ; \ No newline at end of file diff --git a/concord-server/src/routes/index.ts b/concord-server/src/routes/index.ts index 0a8b476..a33ac3d 100644 --- a/concord-server/src/routes/index.ts +++ b/concord-server/src/routes/index.ts @@ -2,10 +2,12 @@ import { Hono } from "hono"; import userRoutes from "./userRoutes"; import messageRoutes from "./messageRoutes"; +import channelRoutes from "./channelRoutes"; const routes = new Hono(); routes.route("/user", userRoutes); routes.route("/message", messageRoutes); +routes.route("/channel", channelRoutes); export default routes; diff --git a/concord-server/src/services/channelService.ts b/concord-server/src/services/channelService.ts new file mode 100644 index 0000000..17c2b0f --- /dev/null +++ b/concord-server/src/services/channelService.ts @@ -0,0 +1,450 @@ +import{ + Channel, + Category +} from '@prisma/client'; +import { PrismaClient } from "@prisma/client"; +import { getUserInformation, getUserCredentials } from './userService'; +import { + CreateChannelInput, + UpdateChannelInput, + DeleteChannelInput, + DeleteChannelsByCategoryIdInput +} from '../validators/channelValidator'; +import{ + UpdateCategoryInput, + DeleteCategoryInput, + DeleteCategoriesByInstanceIdInput, + CreateCategoryInput +} from '../validators/categoryValidator'; + +const prisma = new PrismaClient(); + + +export async function createCategory(data: CreateCategoryInput): Promise{ + try{ + + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ) + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const newCategory = await prisma.category.create({ + data: { + name: data.name, + position: data.position + } + }); + + if(!newCategory){ + throw new Error("could not create category"); + } + + let curInstance; + if(data.instanceId){ + curInstance = await prisma.instance.findUnique({ + where: { + id: data.instanceId + }, + include: { + Category: true + } + }); + + if(!curInstance){ + throw new Error("could not find instance to add category to"); + } + + await prisma.category.update({ + where: { + id: newCategory.id + }, + data: { + instanceId: curInstance.id + } + }); + + return newCategory; + } + + return newCategory; + }catch(err){ + console.log("services::channelService::createCategory - ", err); + return null; + } +} + +export async function getCategory( + categoryId: string, +): Promise{ + try{ + const category = await prisma.category.findUnique({ + where: { + id: categoryId + } + }); + + if(!category){ + throw new Error("could not find category"); + } + + return category; + }catch(err){ + console.log("services::channelService::getCategory - ", err); + return null; + } +} + +export async function getCategoriesByInstance( + instanceId: string +): Promise{ + try{ + const categories = await prisma.category.findMany({ + where: { + instanceId: instanceId + }, + include: { + Channel: true + }, + orderBy: { + position: 'asc' + } + }); + + if(!categories){ + throw new Error("could not find categories for instance"); + } + + return categories; + }catch(err){ + console.log("services::channelService::getCategoriesByInstance - ", err); + return null; + } +} + +export async function updateCategory(data: UpdateCategoryInput): Promise{ + try{ + + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ) + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const updatedCategory = await prisma.category.update({ + where: { + id: data.id + }, + data: { + name: data.name, + position: data.position, + Channel: data.channels ? { set: data.channels } : undefined + } + }); + + if(!updatedCategory){ + throw new Error("could not update category"); + } + + return updatedCategory; + }catch(err){ + console.log("services::channelService::updateCategory - ", err); + return null; + } +} + +export async function deleteCategory(data: DeleteCategoryInput): Promise{ + try{ + + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ) + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const deleteAllChannels = await prisma.channel.deleteMany({ + where: { + categoryId: data.id + } + }); + + if(deleteAllChannels.count === 0){ + throw new Error("could not delete channels from category"); + } + + const deletedCategory = await prisma.category.delete({ + where: { + id: data.id + } + }); + + if(!deletedCategory){ + throw new Error("could not delete category"); + } + + return true; + }catch(err){ + console.log("services::channelService::deleteCategory - ", err); + return false; + } +} + +export async function deleteAllCategoriesFromInstance(data: DeleteCategoriesByInstanceIdInput): Promise{ + try{ + + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ) + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const deletedCategories = await prisma.category.deleteMany({ + where: { + instanceId: data.instanceId + } + }); + + if(deletedCategories.count === 0){ + throw new Error("could not delete categories from instance"); + } + + return true; + }catch(err){ + console.log("services::channelService::deleteAllCategoriesFromInstance - ", err); + return false; + } +} + +export async function createChannel(data: CreateChannelInput): Promise{ + try{ + + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ) + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const newChannel = await prisma.channel.create({ + data: { + type: data.type, + name: data.name, + description: data.description, + categoryId: data.categoryId ? data.categoryId : null + } + }); + + if(!newChannel){ + throw new Error("could not create channel"); + } + + return newChannel; + }catch(err){ + console.log("services::channelService::createChannel - ", err); + return null; + } +} + +export async function getChannel( + channelId: string +): Promise{ + try{ + const channel = await prisma.channel.findUnique({ + where: { + id: channelId + } + }); + + if(!channel){ + throw new Error("could not find channel"); + } + + return channel; + }catch(err){ + console.log("services::channelService::getChannel - ", err); + return null; + } +} + +export async function getChannelsByCategory( + categoryId: string +): Promise{ + try{ + const channels = await prisma.channel.findMany({ + where: { + categoryId: categoryId + } + }); + + if(!channels){ + throw new Error("could not find channels for category"); + } + return channels; + } + catch(err){ + console.log("services::channelService::getChannelsByCategory - ", err); + return null; + } +} + +export async function updateChannel(data: UpdateChannelInput): Promise{ + try{ + + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ) + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const updatedChannel = await prisma.channel.update({ + where: { + id: data.id + }, + data: { + name: data.name, + description: data.description, + categoryId: data.categoryId ? data.categoryId : undefined + } + }); + + if(!updatedChannel){ + throw new Error("could not update channel"); + } + + return updatedChannel; + }catch(err){ + console.log("services::channelService::updateChannel - ", err); + return null; + } +} + +export async function deleteChannel(data: DeleteChannelInput): Promise{ + try{ + + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ) + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const deletedChannel = await prisma.channel.delete({ + where: { + id: data.id + } + }); + + if(!deletedChannel){ + throw new Error("could not delete channel"); + } + + return true; + }catch(err){ + console.log("services::channelService::deleteChannel - ", err); + return false; + } +} + +export async function deleteAllChannelsFromCategory(data: DeleteChannelsByCategoryIdInput): Promise +{ + try{ + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ) + + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; + } + + const deletedChannels = await prisma.channel.deleteMany({ + where: { + categoryId: data.categoryId + } + }); + + if(deletedChannels.count === 0){ + throw new Error("could not delete channels from category"); + } + + return true; + }catch(err){ + console.log("services::channelService::deleteAllChannelsFromCategory - ", err); + return false; + } +} diff --git a/concord-server/src/services/userService.ts b/concord-server/src/services/userService.ts index eec7a47..e960534 100644 --- a/concord-server/src/services/userService.ts +++ b/concord-server/src/services/userService.ts @@ -1,3 +1,4 @@ + import { Message, MessagePing, diff --git a/concord-server/src/validators/categoryValidator.ts b/concord-server/src/validators/categoryValidator.ts new file mode 100644 index 0000000..5dde0ee --- /dev/null +++ b/concord-server/src/validators/categoryValidator.ts @@ -0,0 +1,55 @@ +import { z } from 'zod'; + +//category validators + +export const createCategorySchema = z.object({ + name: z.string().min(1).max(50), + position: z.number().min(0), + instanceId : z.uuidv7().optional(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4() +}) + +export const getCategorySchema = z.object({ + id: z.uuidv7() +}) + +export const getCategoriesByInstanceIdSchema = z.object({ + instanceId: z.uuidv7() + +}) + +export const updateCategorySchema = z.object({ + id: z.uuidv7(), + name: z.string().min(1).max(50).optional(), + position: z.number().min(0).optional(), + channels: z.array(z.object({ + id: z.string() + })).optional(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4() +}) + +export const deleteCategorySchema = z.object({ + id: z.uuidv7(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4() +}) + +export const deleteCategoriesByInstanceIdSchema = z.object({ + instanceId: z.uuidv7(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4() +}) + + +export type CreateCategoryInput = z.infer +export type GetCategoryInput = z.infer +export type GetCategoriesByInstanceIdInput = z.infer +export type UpdateCategoryInput = z.infer +export type DeleteCategoryInput = z.infer +export type DeleteCategoriesByInstanceIdInput = z.infer diff --git a/concord-server/src/validators/channelValidator.ts b/concord-server/src/validators/channelValidator.ts new file mode 100644 index 0000000..53e48bc --- /dev/null +++ b/concord-server/src/validators/channelValidator.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; + +//channel validators + +export const createChannelSchema = z.object({ + type: z.enum(['text', 'voice']), + name: z.string().min(1).max(50), + description: z.string().max(255), + categoryId: z.uuidv7().optional(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4() +}) + +export const getChannelSchema = z.object({ + id: z.uuidv7() +}) + +export const getChannelsByCategoryIdSchema = z.object({ + categoryId: z.uuidv7() +}) + +export const updateChannelSchema = z.object({ + id: z.uuidv7(), + name: z.string().min(1).max(50).optional(), + description: z.string().max(255).optional(), + categoryId: z.uuidv7().optional(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4() +}) + +export const deleteChannelSchema = z.object({ + id: z.uuidv7(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4() +}) + +export const deleteChannelsByCategoryIdSchema = z.object({ + categoryId: z.uuidv7(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4() +}) + +export type CreateChannelInput = z.infer +export type GetChannelInput = z.infer +export type GetChannelsByCategoryIdInput = z.infer +export type UpdateChannelInput = z.infer +export type DeleteChannelInput = z.infer +export type DeleteChannelsByCategoryIdInput = z.infer \ No newline at end of file diff --git a/concord-server/src/validators/userValidator.ts b/concord-server/src/validators/userValidator.ts index 9b9bfb6..0789128 100644 --- a/concord-server/src/validators/userValidator.ts +++ b/concord-server/src/validators/userValidator.ts @@ -7,7 +7,7 @@ export const queryUserByIdSchema = z.object({ export const queryAllUsersByInstanceId = z.object({ instanceId: z.uuidv7(), }); - +import { is } from 'zod/v4/locales'; export const createUserSchema = z.object({ username: z.string().min(3).max(30), nickname: z.string().min(1).max(30).optional(), From d7aeb2fec8c901f8921710597dffb130b0298300 Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:01:52 -0400 Subject: [PATCH 23/26] Basic instance stuff --- .../src/controller/instanceController.ts | 10 +++ concord-server/src/routes/index.ts | 2 + concord-server/src/routes/instanceRoutes.ts | 63 +++++++++++++++++++ .../src/services/instanceService.ts | 57 +++++++++++++++++ concord-server/src/services/userService.ts | 5 +- .../src/validators/instanceValidator.ts | 29 +++++++++ 6 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 concord-server/src/controller/instanceController.ts create mode 100644 concord-server/src/routes/instanceRoutes.ts create mode 100644 concord-server/src/services/instanceService.ts create mode 100644 concord-server/src/validators/instanceValidator.ts diff --git a/concord-server/src/controller/instanceController.ts b/concord-server/src/controller/instanceController.ts new file mode 100644 index 0000000..97578fd --- /dev/null +++ b/concord-server/src/controller/instanceController.ts @@ -0,0 +1,10 @@ +import { createInstance, getAllInstances } from "../services/instanceService"; +import { CreateInstanceRequest } from "../validators/instanceValidator"; + +export async function createInstanceReq(data:CreateInstanceRequest) { + return await createInstance(data); +} + +export async function getAllInstancesReq() { + return await getAllInstances(); +} \ No newline at end of file diff --git a/concord-server/src/routes/index.ts b/concord-server/src/routes/index.ts index a33ac3d..97440a1 100644 --- a/concord-server/src/routes/index.ts +++ b/concord-server/src/routes/index.ts @@ -3,11 +3,13 @@ import { Hono } from "hono"; import userRoutes from "./userRoutes"; import messageRoutes from "./messageRoutes"; import channelRoutes from "./channelRoutes"; +import instanceRoutes from "./instanceRoutes"; const routes = new Hono(); routes.route("/user", userRoutes); routes.route("/message", messageRoutes); routes.route("/channel", channelRoutes); +routes.route("/instance", instanceRoutes); export default routes; diff --git a/concord-server/src/routes/instanceRoutes.ts b/concord-server/src/routes/instanceRoutes.ts new file mode 100644 index 0000000..5fea872 --- /dev/null +++ b/concord-server/src/routes/instanceRoutes.ts @@ -0,0 +1,63 @@ +import { Hono } from "hono"; +import { describeRoute, resolver } from "hono-openapi"; +import { createInstanceRequestSchema, getAllInstancesResponseSchema } from "../validators/instanceValidator"; +import { zValidator } from "@hono/zod-validator"; +import { createInstanceReq, getAllInstancesReq } from "../controller/instanceController"; + +const instanceRoutes = new Hono(); + +instanceRoutes.post( + "", + describeRoute({ + description: "Create instance", + responses: { + 200: { + description: "Instance created", + content: { + "application/json": { schema: resolver(createInstanceRequestSchema) } + }, + }, + 400: { + description: "Invalid request", + }, + } + }), + zValidator('json', createInstanceRequestSchema), + async (c) => { + const data = await c.req.json(); + if (!data) { + return c.json({ error: "could not parse data" }, 400); + } + + const instance = await createInstanceReq(data); + return c.json(instance, 201); + } +); + +instanceRoutes.get( + "", + describeRoute({ + description: "Get all instances", + responses: { + 200: { + description: "List of all instances", + content: { + "application/json": { schema: resolver(getAllInstancesResponseSchema) } + }, + }, + 500: { + description: "Server error", + }, + } + }), + async (c) => { + const instances = await getAllInstancesReq(); + if (instances.success) { + return c.json(instances, 200); + } else { + return c.json({ success: false, error: instances.error || "Failed to fetch instances" }, 500); + } + } +); + +export default instanceRoutes; diff --git a/concord-server/src/services/instanceService.ts b/concord-server/src/services/instanceService.ts new file mode 100644 index 0000000..d37f3a0 --- /dev/null +++ b/concord-server/src/services/instanceService.ts @@ -0,0 +1,57 @@ +import { PrismaClient } from "@prisma/client"; +import { CreateInstanceRequest } from "../validators/instanceValidator"; +import { getUserCredentials, getUserInformation } from "./userService"; + +const prisma = new PrismaClient(); + +export async function createInstance(data: CreateInstanceRequest) { + try { + const creds = await getUserCredentials(data.requestingUserId); + const user = await getUserInformation(data.requestingUserId); + if (!creds + || creds.token != data.requestingUserToken + || !user + || !user.admin) { + return null; + } + + const newInstance = await prisma.instance.create({ + data: { + name: data.name, + icon: data.icon + } + }); + + return { + success: true, + data: newInstance + }; + } catch (error) { + console.error("Error creating instance:", error); + return { + success: false, + error: "Failed to create instance" + }; + } +} + +export async function getAllInstances() { + try { + const instances = await prisma.instance.findMany({ + orderBy: { + createdAt: 'desc' + } + }); + + return { + success: true, + data: instances + }; + } catch (error) { + console.error("Error fetching instances:", error); + return { + success: false, + error: "Failed to fetch instances" + }; + } +} \ No newline at end of file diff --git a/concord-server/src/services/userService.ts b/concord-server/src/services/userService.ts index e960534..6598b19 100644 --- a/concord-server/src/services/userService.ts +++ b/concord-server/src/services/userService.ts @@ -256,10 +256,7 @@ export async function getAllUsersFrom(instanceId: string): Promise< ).includes(u.status as any) ? (u.status as "online" | "offline" | "dnd" | "idle" | "invis") : "offline", - role: adminRoles.map((r) => ({ - userId: r.userId, - instanceId: r.instanceId, - })), + role: adminRoles, }; }), ); diff --git a/concord-server/src/validators/instanceValidator.ts b/concord-server/src/validators/instanceValidator.ts new file mode 100644 index 0000000..a4497fe --- /dev/null +++ b/concord-server/src/validators/instanceValidator.ts @@ -0,0 +1,29 @@ +import { z } from 'zod'; + +export const createInstanceRequestSchema = z.object({ + name: z.string().min(1, 'Instance name cannot be empty'), + icon: z.url().optional(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.string() +}); + +export const getAllInstancesResponseSchema = z.object({ + success: z.boolean(), + data: z.array( + z.object({ + id: z.string(), + name: z.string(), + icon: z.string().nullable(), + createdAt: z.string().refine((val) => !isNaN(Date.parse(val)), { + message: "Invalid date string format" + }), + updatedAt: z.string().refine((val) => !isNaN(Date.parse(val)), { + message: "Invalid date string format" + }) + }) + ).optional(), + error: z.string().optional() +}); + +export type CreateInstanceRequest = z.infer; +export type GetAllInstancesResponse = z.infer; \ No newline at end of file From 83718a0decb5748297441e8939dbb8721aca132b Mon Sep 17 00:00:00 2001 From: PrimarchPaul Date: Sat, 27 Sep 2025 21:11:13 -0400 Subject: [PATCH 24/26] minor refactor to categoryRoutes --- concord-server/src/routes/categoryRoutes.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/concord-server/src/routes/categoryRoutes.ts b/concord-server/src/routes/categoryRoutes.ts index 5db776f..a6eda64 100644 --- a/concord-server/src/routes/categoryRoutes.ts +++ b/concord-server/src/routes/categoryRoutes.ts @@ -19,7 +19,7 @@ import { describeRoute, resolver } from "hono-openapi"; const categoryRoutes = new Hono() categoryRoutes.post( - "/category/create", + "", describeRoute({ description: "Create a new category", responses: { @@ -121,7 +121,7 @@ categoryRoutes.get( ); categoryRoutes.put( - "/category/update", + "", describeRoute({ description: "Update an existing category", responses: { @@ -164,7 +164,7 @@ categoryRoutes.put( ); categoryRoutes.delete( - "/category/delete", + "", describeRoute({ description: "Delete an existing category", responses: { @@ -207,7 +207,7 @@ categoryRoutes.delete( ) categoryRoutes.delete( - "/categories/delete/:id", + "/:id/:userId", describeRoute({ description: "Delete all categories by instance id", responses: { From 11f25ca91509331fee2e527402cd1f3990df626c Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:30:08 -0400 Subject: [PATCH 25/26] refacto --- concord-server/src/routes/categoryRoutes.ts | 101 ++++++++++++++------ concord-server/src/routes/channelRoutes.ts | 57 ++++++++--- concord-server/src/routes/index.ts | 4 +- 3 files changed, 121 insertions(+), 41 deletions(-) diff --git a/concord-server/src/routes/categoryRoutes.ts b/concord-server/src/routes/categoryRoutes.ts index a6eda64..de396cf 100644 --- a/concord-server/src/routes/categoryRoutes.ts +++ b/concord-server/src/routes/categoryRoutes.ts @@ -9,8 +9,16 @@ import { import { createCategorySchema, + getCategorySchema, + getCategoriesByInstanceIdSchema, + updateCategorySchema, + deleteCategorySchema, + deleteCategoriesByInstanceIdSchema, CreateCategoryInput, + GetCategoryInput, + GetCategoriesByInstanceIdInput, UpdateCategoryInput, + DeleteCategoryInput, DeleteCategoriesByInstanceIdInput, } from "../validators/categoryValidator"; import { zValidator } from "@hono/zod-validator"; @@ -18,8 +26,9 @@ import { Hono } from "hono"; import { describeRoute, resolver } from "hono-openapi"; const categoryRoutes = new Hono() +// Create a new category categoryRoutes.post( - "", + "/", describeRoute({ description: "Create a new category", responses: { @@ -61,6 +70,7 @@ categoryRoutes.post( } ) +// Get a category by ID categoryRoutes.get( "/:id", describeRoute({ @@ -69,13 +79,13 @@ categoryRoutes.get( 200: { description: "Success getting category", content: { - "application/json": { schema: resolver(createCategorySchema) }, + "application/json": { schema: resolver(getCategorySchema) }, }, }, 404: { description: "Category id not found", content: { - "application/json": { schema: resolver(createCategorySchema) }, + "application/json": { schema: resolver(getCategorySchema) }, }, }, }, @@ -91,26 +101,32 @@ categoryRoutes.get( } ); +// Get all categories by instance ID categoryRoutes.get( - "", + "/instance/:instanceId", describeRoute({ description: "Get all categories by instance id", responses: { 200: { description: "Success getting all categories in instance", content: { - "application/json": { schema: resolver(createCategorySchema) }, + "application/json": { schema: resolver(getCategoriesByInstanceIdSchema) }, + }, + }, + 400: { + description: "Bad Request - Missing instance ID", + content: { + "application/json": { schema: resolver(getCategoriesByInstanceIdSchema) }, }, }, }, }), - zValidator("query", createCategorySchema), async (c) => { - const instanceId = c.req.query("instanceId"); + const instanceId = c.req.param("instanceId"); if (!instanceId) { return c.json({ error: "No instance id provided" }, 400); } - + const categoryData = await fetchCategoriesByInstance(instanceId); if (categoryData) { return c.json(categoryData); @@ -120,40 +136,53 @@ categoryRoutes.get( } ); +// Update a category categoryRoutes.put( - "", + "/:id", describeRoute({ description: "Update an existing category", responses: { 200: { description: "Success updating category", content: { - "application/json": { schema: resolver(createCategorySchema) }, + "application/json": { schema: resolver(updateCategorySchema) }, }, }, 400: { description: "Bad Request - Invalid input data", content: { - "application/json": { schema: resolver(createCategorySchema)}, + "application/json": { schema: resolver(updateCategorySchema)}, }, }, 401: { description: "Unauthorized - Admin access required", content: { - "application/json": { schema: resolver(createCategorySchema)}, + "application/json": { schema: resolver(updateCategorySchema)}, }, }, 404: { description: "Category id or User Id not found", content: { - "application/json": { schema: resolver(createCategorySchema)}, + "application/json": { schema: resolver(updateCategorySchema)}, }, }, }, }), - zValidator("json", createCategorySchema), + zValidator("json", updateCategorySchema), async (c) => { + const id = c.req.param("id"); const data = c.req.valid("json") as UpdateCategoryInput; + + // Ensure the ID in the path matches the one in the body + if (data.id && data.id !== id) { + return c.json({ error: "ID in path does not match ID in body" }, 400); + } + + // Set ID from path if not in body + if (!data.id) { + data.id = id; + } + const categoryData = await updateExistingCategory(data); if (categoryData) { return c.json(categoryData); @@ -163,40 +192,48 @@ categoryRoutes.put( } ); +// Delete a specific category categoryRoutes.delete( - "", + "/:id", describeRoute({ description: "Delete an existing category", responses: { 200: { description: "Success deleting category", content: { - "application/json": { schema: resolver(createCategorySchema) }, + "application/json": { schema: resolver(deleteCategorySchema) }, }, }, 400: { description: "Bad Request - Invalid input data", content: { - "application/json": { schema: resolver(createCategorySchema)}, + "application/json": { schema: resolver(deleteCategorySchema)}, }, }, 401: { description: "Unauthorized - Admin access required", content: { - "application/json": { schema: resolver(createCategorySchema)}, + "application/json": { schema: resolver(deleteCategorySchema)}, }, }, 404: { description: "Category id or User Id not found", content: { - "application/json": { schema: resolver(createCategorySchema)}, + "application/json": { schema: resolver(deleteCategorySchema)}, }, }, }, }), - zValidator("json", createCategorySchema), + zValidator("json", deleteCategorySchema), async (c) => { - const data = c.req.valid("json") as UpdateCategoryInput; + const id = c.req.param("id"); + const data = c.req.valid("json") as DeleteCategoryInput; + + // Ensure the ID in the path matches the one in the body + if (data.id !== id) { + return c.json({ error: "ID in path does not match ID in body" }, 400); + } + const categoryData = await deleteExistingCategory(data); if (categoryData) { return c.json(categoryData); @@ -206,40 +243,48 @@ categoryRoutes.delete( } ) +// Delete all categories by instance ID categoryRoutes.delete( - "/:id/:userId", + "/instance/:instanceId", describeRoute({ description: "Delete all categories by instance id", responses: { 200: { description: "Success deleting all categories in instance", content: { - "application/json": { schema: resolver(createCategorySchema) }, + "application/json": { schema: resolver(deleteCategoriesByInstanceIdSchema) }, }, }, 400: { description: "Bad Request - Invalid input data", content: { - "application/json": { schema: resolver(createCategorySchema)}, + "application/json": { schema: resolver(deleteCategoriesByInstanceIdSchema)}, }, }, 401: { description: "Unauthorized - Admin access required", content: { - "application/json": { schema: resolver(createCategorySchema)}, + "application/json": { schema: resolver(deleteCategoriesByInstanceIdSchema)}, }, }, 404: { description: "Instance id or User Id not found", content: { - "application/json": { schema: resolver(createCategorySchema)}, + "application/json": { schema: resolver(deleteCategoriesByInstanceIdSchema)}, }, }, }, }), - zValidator("json", createCategorySchema), + zValidator("json", deleteCategoriesByInstanceIdSchema), async (c) => { + const instanceId = c.req.param("instanceId"); const data = c.req.valid("json") as DeleteCategoriesByInstanceIdInput; + + // Ensure the instanceId in the path matches the one in the body + if (data.instanceId !== instanceId) { + return c.json({ error: "Instance ID in path does not match Instance ID in body" }, 400); + } + const categoryData = await deleteAllCategoriesByInstance(data); if (categoryData) { return c.json(categoryData); @@ -249,6 +294,4 @@ categoryRoutes.delete( } ) - - export { categoryRoutes }; \ No newline at end of file diff --git a/concord-server/src/routes/channelRoutes.ts b/concord-server/src/routes/channelRoutes.ts index b8a3dd6..e650baf 100644 --- a/concord-server/src/routes/channelRoutes.ts +++ b/concord-server/src/routes/channelRoutes.ts @@ -28,8 +28,9 @@ import { describeRoute, resolver } from "hono-openapi"; const channelRoutes = new Hono() +// Create a new channel channelRoutes.post( - "/channel/create", + "/", describeRoute({ description: "Create a new channel", responses: { @@ -71,6 +72,7 @@ channelRoutes.post( } ) +// Get a channel by ID channelRoutes.get( "/:id", describeRoute({ @@ -101,8 +103,9 @@ channelRoutes.get( } ); +// Get all channels by category ID channelRoutes.get( - "", + "/category/:categoryId", describeRoute({ description: "Get all channels by category id", responses: { @@ -112,15 +115,20 @@ channelRoutes.get( "application/json": { schema: resolver(getChannelsByCategoryIdSchema) }, }, }, + 400: { + description: "Bad Request - Missing category ID", + content: { + "application/json": { schema: resolver(getChannelsByCategoryIdSchema) }, + }, + }, }, }), - zValidator("query", getChannelsByCategoryIdSchema), async (c) => { - const categoryId = c.req.query("categoryId"); + const categoryId = c.req.param("categoryId"); if (!categoryId) { return c.json({ error: "No category id provided" }, 400); } - + const channels = await fetchChannelsByCategory(categoryId); if (channels) { return c.json(channels); @@ -130,8 +138,9 @@ channelRoutes.get( } ); +// Update a channel channelRoutes.put( - "/channel/update", + "/:id", describeRoute({ description: "Update an existing channel", responses: { @@ -163,7 +172,19 @@ channelRoutes.put( }), zValidator("json", updateChannelSchema), async (c) => { + const id = c.req.param("id"); const data = c.req.valid("json") as UpdateChannelInput; + + // Ensure the ID in the path matches the one in the body + if (data.id && data.id !== id) { + return c.json({ error: "ID in path does not match ID in body" }, 400); + } + + // Set ID from path if not in body + if (!data.id) { + data.id = id; + } + const result = await updateExistingChannel(data); if (result) { return c.json(result); @@ -173,8 +194,9 @@ channelRoutes.put( } ); +// Delete a specific channel channelRoutes.delete( - "/channel/delete", + "/:id", describeRoute({ description: "Delete an existing channel", responses: { @@ -206,7 +228,14 @@ channelRoutes.delete( }), zValidator("json", deleteChannelSchema), async (c) => { + const id = c.req.param("id"); const data = c.req.valid("json") as DeleteChannelInput; + + // Ensure the ID in the path matches the one in the body + if (data.id !== id) { + return c.json({ error: "ID in path does not match ID in body" }, 400); + } + const result = await deleteExistingChannel(data); if (result) { return c.json({ success: true }); @@ -216,8 +245,9 @@ channelRoutes.delete( } ); +// Delete all channels by category ID channelRoutes.delete( - "/channels/delete-by-category", + "/category/:categoryId", describeRoute({ description: "Delete all channels by category id", responses: { @@ -249,7 +279,14 @@ channelRoutes.delete( }), zValidator("json", deleteChannelsByCategoryIdSchema), async (c) => { + const categoryId = c.req.param("categoryId"); const data = c.req.valid("json") as DeleteChannelsByCategoryIdInput; + + // Ensure the categoryId in the path matches the one in the body + if (data.categoryId !== categoryId) { + return c.json({ error: "Category ID in path does not match Category ID in body" }, 400); + } + const result = await deleteAllChannelsByCategory(data); if (result) { return c.json({ success: true }); @@ -259,6 +296,4 @@ channelRoutes.delete( } ) - - -export default channelRoutes ; \ No newline at end of file +export { channelRoutes }; \ No newline at end of file diff --git a/concord-server/src/routes/index.ts b/concord-server/src/routes/index.ts index 97440a1..84ef367 100644 --- a/concord-server/src/routes/index.ts +++ b/concord-server/src/routes/index.ts @@ -2,8 +2,9 @@ import { Hono } from "hono"; import userRoutes from "./userRoutes"; import messageRoutes from "./messageRoutes"; -import channelRoutes from "./channelRoutes"; +import { channelRoutes } from "./channelRoutes"; import instanceRoutes from "./instanceRoutes"; +import { categoryRoutes } from "./categoryRoutes"; const routes = new Hono(); @@ -11,5 +12,6 @@ routes.route("/user", userRoutes); routes.route("/message", messageRoutes); routes.route("/channel", channelRoutes); routes.route("/instance", instanceRoutes); +routes.route("/category", categoryRoutes); export default routes; From c3052989275c300fd0262d0a2f000728a3b7555e Mon Sep 17 00:00:00 2001 From: Kevin Puig <119972216+k-puig@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:42:43 -0400 Subject: [PATCH 26/26] code made pretty --- .../src/controller/categoryController.ts | 41 +- .../src/controller/channelController.ts | 40 +- .../src/controller/instanceController.ts | 8 +- .../src/controller/messageController.ts | 40 +- concord-server/src/routes/categoryRoutes.ts | 530 ++++++------ concord-server/src/routes/channelRoutes.ts | 526 ++++++------ concord-server/src/routes/instanceRoutes.ts | 110 +-- concord-server/src/routes/messageRoutes.ts | 229 ++--- concord-server/src/services/channelService.ts | 799 +++++++++--------- .../src/services/instanceService.ts | 94 ++- concord-server/src/services/messageService.ts | 32 +- concord-server/src/services/userService.ts | 1 - .../src/validators/categoryValidator.ts | 86 +- .../src/validators/channelValidator.ts | 76 +- .../src/validators/instanceValidator.ts | 46 +- .../src/validators/messageValidator.ts | 26 +- .../src/validators/userValidator.ts | 2 +- 17 files changed, 1394 insertions(+), 1292 deletions(-) diff --git a/concord-server/src/controller/categoryController.ts b/concord-server/src/controller/categoryController.ts index 7177e5f..c2b5a1b 100644 --- a/concord-server/src/controller/categoryController.ts +++ b/concord-server/src/controller/categoryController.ts @@ -1,39 +1,40 @@ -import{ - createCategory, - getCategory, - getCategoriesByInstance, - updateCategory, - deleteCategory, - deleteAllCategoriesFromInstance, +import { + createCategory, + getCategory, + getCategoriesByInstance, + updateCategory, + deleteCategory, + deleteAllCategoriesFromInstance, } from "../services/channelService"; -import{ - CreateCategoryInput, - UpdateCategoryInput, - DeleteCategoryInput, - DeleteCategoriesByInstanceIdInput +import { + CreateCategoryInput, + UpdateCategoryInput, + DeleteCategoryInput, + DeleteCategoriesByInstanceIdInput, } from "../validators/categoryValidator"; export async function createNewCategory(data: CreateCategoryInput) { - return await createCategory(data); + return await createCategory(data); } export async function fetchCategoryData(id: string) { - return await getCategory(id); + return await getCategory(id); } export async function fetchCategoriesByInstance(instanceId: string) { - return await getCategoriesByInstance(instanceId); + return await getCategoriesByInstance(instanceId); } export async function updateExistingCategory(data: UpdateCategoryInput) { - return await updateCategory(data); + return await updateCategory(data); } export async function deleteExistingCategory(data: DeleteCategoryInput) { - return await deleteCategory(data); + return await deleteCategory(data); } -export async function deleteAllCategoriesByInstance(data: DeleteCategoriesByInstanceIdInput) { - return await deleteAllCategoriesFromInstance(data); +export async function deleteAllCategoriesByInstance( + data: DeleteCategoriesByInstanceIdInput, +) { + return await deleteAllCategoriesFromInstance(data); } - diff --git a/concord-server/src/controller/channelController.ts b/concord-server/src/controller/channelController.ts index b1c6cce..1c09a84 100644 --- a/concord-server/src/controller/channelController.ts +++ b/concord-server/src/controller/channelController.ts @@ -1,38 +1,40 @@ -import{ - createChannel, - getChannel, - getChannelsByCategory, - updateChannel, - deleteChannel, - deleteAllChannelsFromCategory +import { + createChannel, + getChannel, + getChannelsByCategory, + updateChannel, + deleteChannel, + deleteAllChannelsFromCategory, } from "../services/channelService"; import { - CreateChannelInput, - UpdateChannelInput, - DeleteChannelInput, - DeleteChannelsByCategoryIdInput + CreateChannelInput, + UpdateChannelInput, + DeleteChannelInput, + DeleteChannelsByCategoryIdInput, } from "../validators/channelValidator"; export async function createNewChannel(data: CreateChannelInput) { - return await createChannel(data); + return await createChannel(data); } export async function fetchChannelData(id: string) { - return await getChannel(id); + return await getChannel(id); } export async function fetchChannelsByCategory(categoryId: string) { - return await getChannelsByCategory(categoryId); + return await getChannelsByCategory(categoryId); } export async function updateExistingChannel(data: UpdateChannelInput) { - return await updateChannel(data); + return await updateChannel(data); } export async function deleteExistingChannel(data: DeleteChannelInput) { - return await deleteChannel(data); + return await deleteChannel(data); } -export async function deleteAllChannelsByCategory(data: DeleteChannelsByCategoryIdInput) { - return await deleteAllChannelsFromCategory(data); -} \ No newline at end of file +export async function deleteAllChannelsByCategory( + data: DeleteChannelsByCategoryIdInput, +) { + return await deleteAllChannelsFromCategory(data); +} diff --git a/concord-server/src/controller/instanceController.ts b/concord-server/src/controller/instanceController.ts index 97578fd..a586e35 100644 --- a/concord-server/src/controller/instanceController.ts +++ b/concord-server/src/controller/instanceController.ts @@ -1,10 +1,10 @@ import { createInstance, getAllInstances } from "../services/instanceService"; import { CreateInstanceRequest } from "../validators/instanceValidator"; -export async function createInstanceReq(data:CreateInstanceRequest) { - return await createInstance(data); +export async function createInstanceReq(data: CreateInstanceRequest) { + return await createInstance(data); } export async function getAllInstancesReq() { - return await getAllInstances(); -} \ No newline at end of file + return await getAllInstances(); +} diff --git a/concord-server/src/controller/messageController.ts b/concord-server/src/controller/messageController.ts index e96b404..483b184 100644 --- a/concord-server/src/controller/messageController.ts +++ b/concord-server/src/controller/messageController.ts @@ -1,25 +1,29 @@ -import { getMessageInformation, getMessagesBefore, sendMessageToChannel } from "../services/messageService"; +import { + getMessageInformation, + getMessagesBefore, + sendMessageToChannel, +} from "../services/messageService"; -export async function fetchMessageData(id:string) { - return await getMessageInformation(id); +export async function fetchMessageData(id: string) { + return await getMessageInformation(id); } -export async function fetchMessagesBefore(date:string, channelId:string) { - return getMessagesBefore(date, channelId); +export async function fetchMessagesBefore(date: string, channelId: string) { + return getMessagesBefore(date, channelId); } export async function sendMessage( - channelId: string, - userId: string, - content: string, - token: string, - repliedMessageId: string | null + channelId: string, + userId: string, + content: string, + token: string, + repliedMessageId: string | null, ) { - return await sendMessageToChannel( - channelId, - userId, - content, - token, - repliedMessageId - ); -} \ No newline at end of file + return await sendMessageToChannel( + channelId, + userId, + content, + token, + repliedMessageId, + ); +} diff --git a/concord-server/src/routes/categoryRoutes.ts b/concord-server/src/routes/categoryRoutes.ts index de396cf..9b48532 100644 --- a/concord-server/src/routes/categoryRoutes.ts +++ b/concord-server/src/routes/categoryRoutes.ts @@ -1,297 +1,315 @@ import { - createNewCategory, - fetchCategoryData, - fetchCategoriesByInstance, - updateExistingCategory, - deleteExistingCategory, - deleteAllCategoriesByInstance, + createNewCategory, + fetchCategoryData, + fetchCategoriesByInstance, + updateExistingCategory, + deleteExistingCategory, + deleteAllCategoriesByInstance, } from "../controller/categoryController"; import { - createCategorySchema, - getCategorySchema, - getCategoriesByInstanceIdSchema, - updateCategorySchema, - deleteCategorySchema, - deleteCategoriesByInstanceIdSchema, - CreateCategoryInput, - GetCategoryInput, - GetCategoriesByInstanceIdInput, - UpdateCategoryInput, - DeleteCategoryInput, - DeleteCategoriesByInstanceIdInput, + createCategorySchema, + getCategorySchema, + getCategoriesByInstanceIdSchema, + updateCategorySchema, + deleteCategorySchema, + deleteCategoriesByInstanceIdSchema, + CreateCategoryInput, + GetCategoryInput, + GetCategoriesByInstanceIdInput, + UpdateCategoryInput, + DeleteCategoryInput, + DeleteCategoriesByInstanceIdInput, } from "../validators/categoryValidator"; import { zValidator } from "@hono/zod-validator"; import { Hono } from "hono"; import { describeRoute, resolver } from "hono-openapi"; -const categoryRoutes = new Hono() +const categoryRoutes = new Hono(); // Create a new category categoryRoutes.post( - "/", - describeRoute({ - description: "Create a new category", - responses: { - 200: { - description: "Success creating category", - content: { - "application/json": { schema: resolver(createCategorySchema) }, - }, - }, - 400: { - description: "Bad Request - Invalid input data", - content: { - "application/json": { schema: resolver(createCategorySchema)}, - }, - }, - 401: { - description: "Unauthorized - Admin access required", - content: { - "application/json": { schema: resolver(createCategorySchema)}, - }, - }, - 404: { - description: "User Id not found", - content: { - "application/json": { schema: resolver(createCategorySchema)}, - }, - }, + "/", + describeRoute({ + description: "Create a new category", + responses: { + 200: { + description: "Success creating category", + content: { + "application/json": { schema: resolver(createCategorySchema) }, }, - }), - zValidator("json", createCategorySchema), - async (c) => { - const data = c.req.valid("json") as CreateCategoryInput; - const categoryData = await createNewCategory(data); - if (categoryData) { - return c.json(categoryData); - } else { - return c.json({ error: "Failed to create category" }, 400); - } + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(createCategorySchema) }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(createCategorySchema) }, + }, + }, + 404: { + description: "User Id not found", + content: { + "application/json": { schema: resolver(createCategorySchema) }, + }, + }, + }, + }), + zValidator("json", createCategorySchema), + async (c) => { + const data = c.req.valid("json") as CreateCategoryInput; + const categoryData = await createNewCategory(data); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Failed to create category" }, 400); } -) + }, +); // Get a category by ID categoryRoutes.get( - "/:id", - describeRoute({ - description: "Get category by id", - responses: { - 200: { - description: "Success getting category", - content: { - "application/json": { schema: resolver(getCategorySchema) }, - }, - }, - 404: { - description: "Category id not found", - content: { - "application/json": { schema: resolver(getCategorySchema) }, - }, - }, + "/:id", + describeRoute({ + description: "Get category by id", + responses: { + 200: { + description: "Success getting category", + content: { + "application/json": { schema: resolver(getCategorySchema) }, }, - }), - async (c) => { - const id = c.req.param("id"); - const categoryData = await fetchCategoryData(id); - if (categoryData) { - return c.json(categoryData); - } else { - return c.json({ error: "Category not found" }, 404); - } + }, + 404: { + description: "Category id not found", + content: { + "application/json": { schema: resolver(getCategorySchema) }, + }, + }, + }, + }), + async (c) => { + const id = c.req.param("id"); + const categoryData = await fetchCategoryData(id); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Category not found" }, 404); } + }, ); // Get all categories by instance ID categoryRoutes.get( - "/instance/:instanceId", - describeRoute({ - description: "Get all categories by instance id", - responses: { - 200: { - description: "Success getting all categories in instance", - content: { - "application/json": { schema: resolver(getCategoriesByInstanceIdSchema) }, - }, - }, - 400: { - description: "Bad Request - Missing instance ID", - content: { - "application/json": { schema: resolver(getCategoriesByInstanceIdSchema) }, - }, - }, + "/instance/:instanceId", + describeRoute({ + description: "Get all categories by instance id", + responses: { + 200: { + description: "Success getting all categories in instance", + content: { + "application/json": { + schema: resolver(getCategoriesByInstanceIdSchema), + }, }, - }), - async (c) => { - const instanceId = c.req.param("instanceId"); - if (!instanceId) { - return c.json({ error: "No instance id provided" }, 400); - } - - const categoryData = await fetchCategoriesByInstance(instanceId); - if (categoryData) { - return c.json(categoryData); - } else { - return c.json({ error: "Error getting all categories from instance" }, 500); - } + }, + 400: { + description: "Bad Request - Missing instance ID", + content: { + "application/json": { + schema: resolver(getCategoriesByInstanceIdSchema), + }, + }, + }, + }, + }), + async (c) => { + const instanceId = c.req.param("instanceId"); + if (!instanceId) { + return c.json({ error: "No instance id provided" }, 400); } + + const categoryData = await fetchCategoriesByInstance(instanceId); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json( + { error: "Error getting all categories from instance" }, + 500, + ); + } + }, ); // Update a category categoryRoutes.put( - "/:id", - describeRoute({ - description: "Update an existing category", - responses: { - 200: { - description: "Success updating category", - content: { - "application/json": { schema: resolver(updateCategorySchema) }, - }, - }, - 400: { - description: "Bad Request - Invalid input data", - content: { - "application/json": { schema: resolver(updateCategorySchema)}, - }, - }, - 401: { - description: "Unauthorized - Admin access required", - content: { - "application/json": { schema: resolver(updateCategorySchema)}, - }, - }, - 404: { - description: "Category id or User Id not found", - content: { - "application/json": { schema: resolver(updateCategorySchema)}, - }, - }, + "/:id", + describeRoute({ + description: "Update an existing category", + responses: { + 200: { + description: "Success updating category", + content: { + "application/json": { schema: resolver(updateCategorySchema) }, }, - }), - zValidator("json", updateCategorySchema), - async (c) => { - const id = c.req.param("id"); - const data = c.req.valid("json") as UpdateCategoryInput; - - // Ensure the ID in the path matches the one in the body - if (data.id && data.id !== id) { - return c.json({ error: "ID in path does not match ID in body" }, 400); - } - - // Set ID from path if not in body - if (!data.id) { - data.id = id; - } - - const categoryData = await updateExistingCategory(data); - if (categoryData) { - return c.json(categoryData); - } else { - return c.json({ error: "Failed to update category" }, 400); - } + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(updateCategorySchema) }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(updateCategorySchema) }, + }, + }, + 404: { + description: "Category id or User Id not found", + content: { + "application/json": { schema: resolver(updateCategorySchema) }, + }, + }, + }, + }), + zValidator("json", updateCategorySchema), + async (c) => { + const id = c.req.param("id"); + const data = c.req.valid("json") as UpdateCategoryInput; + + // Ensure the ID in the path matches the one in the body + if (data.id && data.id !== id) { + return c.json({ error: "ID in path does not match ID in body" }, 400); } + + // Set ID from path if not in body + if (!data.id) { + data.id = id; + } + + const categoryData = await updateExistingCategory(data); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Failed to update category" }, 400); + } + }, ); // Delete a specific category categoryRoutes.delete( - "/:id", - describeRoute({ - description: "Delete an existing category", - responses: { - 200: { - description: "Success deleting category", - content: { - "application/json": { schema: resolver(deleteCategorySchema) }, - }, - }, - 400: { - description: "Bad Request - Invalid input data", - content: { - "application/json": { schema: resolver(deleteCategorySchema)}, - }, - }, - 401: { - description: "Unauthorized - Admin access required", - content: { - "application/json": { schema: resolver(deleteCategorySchema)}, - }, - }, - 404: { - description: "Category id or User Id not found", - content: { - "application/json": { schema: resolver(deleteCategorySchema)}, - }, - }, + "/:id", + describeRoute({ + description: "Delete an existing category", + responses: { + 200: { + description: "Success deleting category", + content: { + "application/json": { schema: resolver(deleteCategorySchema) }, }, - }), - zValidator("json", deleteCategorySchema), - async (c) => { - const id = c.req.param("id"); - const data = c.req.valid("json") as DeleteCategoryInput; - - // Ensure the ID in the path matches the one in the body - if (data.id !== id) { - return c.json({ error: "ID in path does not match ID in body" }, 400); - } - - const categoryData = await deleteExistingCategory(data); - if (categoryData) { - return c.json(categoryData); - } else { - return c.json({ error: "Failed to delete category" }, 400); - } + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(deleteCategorySchema) }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(deleteCategorySchema) }, + }, + }, + 404: { + description: "Category id or User Id not found", + content: { + "application/json": { schema: resolver(deleteCategorySchema) }, + }, + }, + }, + }), + zValidator("json", deleteCategorySchema), + async (c) => { + const id = c.req.param("id"); + const data = c.req.valid("json") as DeleteCategoryInput; + + // Ensure the ID in the path matches the one in the body + if (data.id !== id) { + return c.json({ error: "ID in path does not match ID in body" }, 400); } -) + + const categoryData = await deleteExistingCategory(data); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Failed to delete category" }, 400); + } + }, +); // Delete all categories by instance ID categoryRoutes.delete( - "/instance/:instanceId", - describeRoute({ - description: "Delete all categories by instance id", - responses: { - 200: { - description: "Success deleting all categories in instance", - content: { - "application/json": { schema: resolver(deleteCategoriesByInstanceIdSchema) }, - }, - }, - 400: { - description: "Bad Request - Invalid input data", - content: { - "application/json": { schema: resolver(deleteCategoriesByInstanceIdSchema)}, - }, - }, - 401: { - description: "Unauthorized - Admin access required", - content: { - "application/json": { schema: resolver(deleteCategoriesByInstanceIdSchema)}, - }, - }, - 404: { - description: "Instance id or User Id not found", - content: { - "application/json": { schema: resolver(deleteCategoriesByInstanceIdSchema)}, - }, - }, + "/instance/:instanceId", + describeRoute({ + description: "Delete all categories by instance id", + responses: { + 200: { + description: "Success deleting all categories in instance", + content: { + "application/json": { + schema: resolver(deleteCategoriesByInstanceIdSchema), + }, }, - }), - zValidator("json", deleteCategoriesByInstanceIdSchema), - async (c) => { - const instanceId = c.req.param("instanceId"); - const data = c.req.valid("json") as DeleteCategoriesByInstanceIdInput; - - // Ensure the instanceId in the path matches the one in the body - if (data.instanceId !== instanceId) { - return c.json({ error: "Instance ID in path does not match Instance ID in body" }, 400); - } - - const categoryData = await deleteAllCategoriesByInstance(data); - if (categoryData) { - return c.json(categoryData); - } else { - return c.json({ error: "Failed to delete categories" }, 400); - } - } -) + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { + schema: resolver(deleteCategoriesByInstanceIdSchema), + }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { + schema: resolver(deleteCategoriesByInstanceIdSchema), + }, + }, + }, + 404: { + description: "Instance id or User Id not found", + content: { + "application/json": { + schema: resolver(deleteCategoriesByInstanceIdSchema), + }, + }, + }, + }, + }), + zValidator("json", deleteCategoriesByInstanceIdSchema), + async (c) => { + const instanceId = c.req.param("instanceId"); + const data = c.req.valid("json") as DeleteCategoriesByInstanceIdInput; -export { categoryRoutes }; \ No newline at end of file + // Ensure the instanceId in the path matches the one in the body + if (data.instanceId !== instanceId) { + return c.json( + { error: "Instance ID in path does not match Instance ID in body" }, + 400, + ); + } + + const categoryData = await deleteAllCategoriesByInstance(data); + if (categoryData) { + return c.json(categoryData); + } else { + return c.json({ error: "Failed to delete categories" }, 400); + } + }, +); + +export { categoryRoutes }; diff --git a/concord-server/src/routes/channelRoutes.ts b/concord-server/src/routes/channelRoutes.ts index e650baf..43f38ad 100644 --- a/concord-server/src/routes/channelRoutes.ts +++ b/concord-server/src/routes/channelRoutes.ts @@ -1,299 +1,313 @@ import { - createNewChannel, - fetchChannelData, - fetchChannelsByCategory, - updateExistingChannel, - deleteExistingChannel, - deleteAllChannelsByCategory, + createNewChannel, + fetchChannelData, + fetchChannelsByCategory, + updateExistingChannel, + deleteExistingChannel, + deleteAllChannelsByCategory, } from "../controller/channelController"; import { - createChannelSchema, - getChannelSchema, - getChannelsByCategoryIdSchema, - updateChannelSchema, - deleteChannelSchema, - deleteChannelsByCategoryIdSchema, - - CreateChannelInput, - GetChannelInput, - GetChannelsByCategoryIdInput, - UpdateChannelInput, - DeleteChannelInput, - DeleteChannelsByCategoryIdInput, + createChannelSchema, + getChannelSchema, + getChannelsByCategoryIdSchema, + updateChannelSchema, + deleteChannelSchema, + deleteChannelsByCategoryIdSchema, + CreateChannelInput, + GetChannelInput, + GetChannelsByCategoryIdInput, + UpdateChannelInput, + DeleteChannelInput, + DeleteChannelsByCategoryIdInput, } from "../validators/channelValidator"; import { zValidator } from "@hono/zod-validator"; import { Hono } from "hono"; import { describeRoute, resolver } from "hono-openapi"; -const channelRoutes = new Hono() +const channelRoutes = new Hono(); // Create a new channel channelRoutes.post( - "/", - describeRoute({ - description: "Create a new channel", - responses: { - 200: { - description: "Success creating channel", - content: { - "application/json": { schema: resolver(createChannelSchema) }, - }, - }, - 400: { - description: "Bad Request - Invalid input data", - content: { - "application/json": { schema: resolver(createChannelSchema)}, - }, - }, - 401: { - description: "Unauthorized - Admin access required", - content: { - "application/json": { schema: resolver(createChannelSchema)}, - }, - }, - 404: { - description: "User Id not found", - content: { - "application/json": { schema: resolver(createChannelSchema)}, - }, - }, + "/", + describeRoute({ + description: "Create a new channel", + responses: { + 200: { + description: "Success creating channel", + content: { + "application/json": { schema: resolver(createChannelSchema) }, }, - }), - zValidator("json", createChannelSchema), - async (c) => { - const data = c.req.valid("json") as CreateChannelInput; - const channelData = await createNewChannel(data); - if (channelData) { - return c.json(channelData); - } else { - return c.json({ error: "Failed to create channel" }, 400); - } + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(createChannelSchema) }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(createChannelSchema) }, + }, + }, + 404: { + description: "User Id not found", + content: { + "application/json": { schema: resolver(createChannelSchema) }, + }, + }, + }, + }), + zValidator("json", createChannelSchema), + async (c) => { + const data = c.req.valid("json") as CreateChannelInput; + const channelData = await createNewChannel(data); + if (channelData) { + return c.json(channelData); + } else { + return c.json({ error: "Failed to create channel" }, 400); } -) + }, +); // Get a channel by ID channelRoutes.get( - "/:id", - describeRoute({ - description: "Get channel by id", - responses: { - 200: { - description: "Success getting channel", - content: { - "application/json": { schema: resolver(getChannelSchema) }, - }, - }, - 404: { - description: "Channel id not found", - content: { - "application/json": { schema: resolver(getChannelSchema) }, - }, - }, + "/:id", + describeRoute({ + description: "Get channel by id", + responses: { + 200: { + description: "Success getting channel", + content: { + "application/json": { schema: resolver(getChannelSchema) }, }, - }), - async (c) => { - const id = c.req.param("id"); - const channelData = await fetchChannelData(id); - if (channelData) { - return c.json(channelData); - } else { - return c.json({ error: "Channel not found" }, 404); - } + }, + 404: { + description: "Channel id not found", + content: { + "application/json": { schema: resolver(getChannelSchema) }, + }, + }, + }, + }), + async (c) => { + const id = c.req.param("id"); + const channelData = await fetchChannelData(id); + if (channelData) { + return c.json(channelData); + } else { + return c.json({ error: "Channel not found" }, 404); } + }, ); // Get all channels by category ID channelRoutes.get( - "/category/:categoryId", - describeRoute({ - description: "Get all channels by category id", - responses: { - 200: { - description: "Success getting all channels in category", - content: { - "application/json": { schema: resolver(getChannelsByCategoryIdSchema) }, - }, - }, - 400: { - description: "Bad Request - Missing category ID", - content: { - "application/json": { schema: resolver(getChannelsByCategoryIdSchema) }, - }, - }, + "/category/:categoryId", + describeRoute({ + description: "Get all channels by category id", + responses: { + 200: { + description: "Success getting all channels in category", + content: { + "application/json": { + schema: resolver(getChannelsByCategoryIdSchema), + }, }, - }), - async (c) => { - const categoryId = c.req.param("categoryId"); - if (!categoryId) { - return c.json({ error: "No category id provided" }, 400); - } - - const channels = await fetchChannelsByCategory(categoryId); - if (channels) { - return c.json(channels); - } else { - return c.json({ error: "Error getting channels from category" }, 500); - } + }, + 400: { + description: "Bad Request - Missing category ID", + content: { + "application/json": { + schema: resolver(getChannelsByCategoryIdSchema), + }, + }, + }, + }, + }), + async (c) => { + const categoryId = c.req.param("categoryId"); + if (!categoryId) { + return c.json({ error: "No category id provided" }, 400); } + + const channels = await fetchChannelsByCategory(categoryId); + if (channels) { + return c.json(channels); + } else { + return c.json({ error: "Error getting channels from category" }, 500); + } + }, ); // Update a channel channelRoutes.put( - "/:id", - describeRoute({ - description: "Update an existing channel", - responses: { - 200: { - description: "Success updating channel", - content: { - "application/json": { schema: resolver(updateChannelSchema) }, - }, - }, - 400: { - description: "Bad Request - Invalid input data", - content: { - "application/json": { schema: resolver(updateChannelSchema)}, - }, - }, - 401: { - description: "Unauthorized - Admin access required", - content: { - "application/json": { schema: resolver(updateChannelSchema)}, - }, - }, - 404: { - description: "Channel id or User Id not found", - content: { - "application/json": { schema: resolver(updateChannelSchema)}, - }, - }, + "/:id", + describeRoute({ + description: "Update an existing channel", + responses: { + 200: { + description: "Success updating channel", + content: { + "application/json": { schema: resolver(updateChannelSchema) }, }, - }), - zValidator("json", updateChannelSchema), - async (c) => { - const id = c.req.param("id"); - const data = c.req.valid("json") as UpdateChannelInput; - - // Ensure the ID in the path matches the one in the body - if (data.id && data.id !== id) { - return c.json({ error: "ID in path does not match ID in body" }, 400); - } - - // Set ID from path if not in body - if (!data.id) { - data.id = id; - } - - const result = await updateExistingChannel(data); - if (result) { - return c.json(result); - } else { - return c.json({ error: "Failed to update channel" }, 400); - } + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(updateChannelSchema) }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(updateChannelSchema) }, + }, + }, + 404: { + description: "Channel id or User Id not found", + content: { + "application/json": { schema: resolver(updateChannelSchema) }, + }, + }, + }, + }), + zValidator("json", updateChannelSchema), + async (c) => { + const id = c.req.param("id"); + const data = c.req.valid("json") as UpdateChannelInput; + + // Ensure the ID in the path matches the one in the body + if (data.id && data.id !== id) { + return c.json({ error: "ID in path does not match ID in body" }, 400); } + + // Set ID from path if not in body + if (!data.id) { + data.id = id; + } + + const result = await updateExistingChannel(data); + if (result) { + return c.json(result); + } else { + return c.json({ error: "Failed to update channel" }, 400); + } + }, ); // Delete a specific channel channelRoutes.delete( - "/:id", - describeRoute({ - description: "Delete an existing channel", - responses: { - 200: { - description: "Success deleting channel", - content: { - "application/json": { schema: resolver(deleteChannelSchema) }, - }, - }, - 400: { - description: "Bad Request - Invalid input data", - content: { - "application/json": { schema: resolver(deleteChannelSchema)}, - }, - }, - 401: { - description: "Unauthorized - Admin access required", - content: { - "application/json": { schema: resolver(deleteChannelSchema)}, - }, - }, - 404: { - description: "Channel id or User Id not found", - content: { - "application/json": { schema: resolver(deleteChannelSchema)}, - }, - }, + "/:id", + describeRoute({ + description: "Delete an existing channel", + responses: { + 200: { + description: "Success deleting channel", + content: { + "application/json": { schema: resolver(deleteChannelSchema) }, }, - }), - zValidator("json", deleteChannelSchema), - async (c) => { - const id = c.req.param("id"); - const data = c.req.valid("json") as DeleteChannelInput; - - // Ensure the ID in the path matches the one in the body - if (data.id !== id) { - return c.json({ error: "ID in path does not match ID in body" }, 400); - } - - const result = await deleteExistingChannel(data); - if (result) { - return c.json({ success: true }); - } else { - return c.json({ error: "Failed to delete channel" }, 400); - } + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(deleteChannelSchema) }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { schema: resolver(deleteChannelSchema) }, + }, + }, + 404: { + description: "Channel id or User Id not found", + content: { + "application/json": { schema: resolver(deleteChannelSchema) }, + }, + }, + }, + }), + zValidator("json", deleteChannelSchema), + async (c) => { + const id = c.req.param("id"); + const data = c.req.valid("json") as DeleteChannelInput; + + // Ensure the ID in the path matches the one in the body + if (data.id !== id) { + return c.json({ error: "ID in path does not match ID in body" }, 400); } + + const result = await deleteExistingChannel(data); + if (result) { + return c.json({ success: true }); + } else { + return c.json({ error: "Failed to delete channel" }, 400); + } + }, ); // Delete all channels by category ID channelRoutes.delete( - "/category/:categoryId", - describeRoute({ - description: "Delete all channels by category id", - responses: { - 200: { - description: "Success deleting all channels in category", - content: { - "application/json": { schema: resolver(deleteChannelsByCategoryIdSchema) }, - }, - }, - 400: { - description: "Bad Request - Invalid input data", - content: { - "application/json": { schema: resolver(deleteChannelsByCategoryIdSchema)}, - }, - }, - 401: { - description: "Unauthorized - Admin access required", - content: { - "application/json": { schema: resolver(deleteChannelsByCategoryIdSchema)}, - }, - }, - 404: { - description: "Category id or User Id not found", - content: { - "application/json": { schema: resolver(deleteChannelsByCategoryIdSchema)}, - }, - }, + "/category/:categoryId", + describeRoute({ + description: "Delete all channels by category id", + responses: { + 200: { + description: "Success deleting all channels in category", + content: { + "application/json": { + schema: resolver(deleteChannelsByCategoryIdSchema), + }, }, - }), - zValidator("json", deleteChannelsByCategoryIdSchema), - async (c) => { - const categoryId = c.req.param("categoryId"); - const data = c.req.valid("json") as DeleteChannelsByCategoryIdInput; - - // Ensure the categoryId in the path matches the one in the body - if (data.categoryId !== categoryId) { - return c.json({ error: "Category ID in path does not match Category ID in body" }, 400); - } - - const result = await deleteAllChannelsByCategory(data); - if (result) { - return c.json({ success: true }); - } else { - return c.json({ error: "Failed to delete channels" }, 400); - } - } -) + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { + schema: resolver(deleteChannelsByCategoryIdSchema), + }, + }, + }, + 401: { + description: "Unauthorized - Admin access required", + content: { + "application/json": { + schema: resolver(deleteChannelsByCategoryIdSchema), + }, + }, + }, + 404: { + description: "Category id or User Id not found", + content: { + "application/json": { + schema: resolver(deleteChannelsByCategoryIdSchema), + }, + }, + }, + }, + }), + zValidator("json", deleteChannelsByCategoryIdSchema), + async (c) => { + const categoryId = c.req.param("categoryId"); + const data = c.req.valid("json") as DeleteChannelsByCategoryIdInput; -export { channelRoutes }; \ No newline at end of file + // Ensure the categoryId in the path matches the one in the body + if (data.categoryId !== categoryId) { + return c.json( + { error: "Category ID in path does not match Category ID in body" }, + 400, + ); + } + + const result = await deleteAllChannelsByCategory(data); + if (result) { + return c.json({ success: true }); + } else { + return c.json({ error: "Failed to delete channels" }, 400); + } + }, +); + +export { channelRoutes }; diff --git a/concord-server/src/routes/instanceRoutes.ts b/concord-server/src/routes/instanceRoutes.ts index 5fea872..4e27d75 100644 --- a/concord-server/src/routes/instanceRoutes.ts +++ b/concord-server/src/routes/instanceRoutes.ts @@ -1,63 +1,77 @@ import { Hono } from "hono"; import { describeRoute, resolver } from "hono-openapi"; -import { createInstanceRequestSchema, getAllInstancesResponseSchema } from "../validators/instanceValidator"; +import { + createInstanceRequestSchema, + getAllInstancesResponseSchema, +} from "../validators/instanceValidator"; import { zValidator } from "@hono/zod-validator"; -import { createInstanceReq, getAllInstancesReq } from "../controller/instanceController"; +import { + createInstanceReq, + getAllInstancesReq, +} from "../controller/instanceController"; const instanceRoutes = new Hono(); instanceRoutes.post( - "", - describeRoute({ - description: "Create instance", - responses: { - 200: { - description: "Instance created", - content: { - "application/json": { schema: resolver(createInstanceRequestSchema) } - }, - }, - 400: { - description: "Invalid request", - }, - } - }), - zValidator('json', createInstanceRequestSchema), - async (c) => { - const data = await c.req.json(); - if (!data) { - return c.json({ error: "could not parse data" }, 400); - } - - const instance = await createInstanceReq(data); - return c.json(instance, 201); + "", + describeRoute({ + description: "Create instance", + responses: { + 200: { + description: "Instance created", + content: { + "application/json": { schema: resolver(createInstanceRequestSchema) }, + }, + }, + 400: { + description: "Invalid request", + }, + }, + }), + zValidator("json", createInstanceRequestSchema), + async (c) => { + const data = await c.req.json(); + if (!data) { + return c.json({ error: "could not parse data" }, 400); } + + const instance = await createInstanceReq(data); + return c.json(instance, 201); + }, ); instanceRoutes.get( - "", - describeRoute({ - description: "Get all instances", - responses: { - 200: { - description: "List of all instances", - content: { - "application/json": { schema: resolver(getAllInstancesResponseSchema) } - }, - }, - 500: { - description: "Server error", - }, - } - }), - async (c) => { - const instances = await getAllInstancesReq(); - if (instances.success) { - return c.json(instances, 200); - } else { - return c.json({ success: false, error: instances.error || "Failed to fetch instances" }, 500); - } + "", + describeRoute({ + description: "Get all instances", + responses: { + 200: { + description: "List of all instances", + content: { + "application/json": { + schema: resolver(getAllInstancesResponseSchema), + }, + }, + }, + 500: { + description: "Server error", + }, + }, + }), + async (c) => { + const instances = await getAllInstancesReq(); + if (instances.success) { + return c.json(instances, 200); + } else { + return c.json( + { + success: false, + error: instances.error || "Failed to fetch instances", + }, + 500, + ); } + }, ); export default instanceRoutes; diff --git a/concord-server/src/routes/messageRoutes.ts b/concord-server/src/routes/messageRoutes.ts index 2733231..17a576e 100644 --- a/concord-server/src/routes/messageRoutes.ts +++ b/concord-server/src/routes/messageRoutes.ts @@ -1,121 +1,146 @@ import { Hono } from "hono"; import { describeResponse, describeRoute, resolver } from "hono-openapi"; -import { getMessageByIdSchema, getMessagesBeforeDate, sendMessageSchema } from "../validators/messageValidator"; +import { + getMessageByIdSchema, + getMessagesBeforeDate, + sendMessageSchema, +} from "../validators/messageValidator"; import { zValidator } from "@hono/zod-validator"; -import { fetchMessageData, fetchMessagesBefore, sendMessage } from "../controller/messageController"; +import { + fetchMessageData, + fetchMessagesBefore, + sendMessage, +} from "../controller/messageController"; const messageRoutes = new Hono(); messageRoutes.get( - "/:id", - describeRoute({ - description: "Get message by id", - responses: { - 200: { - description: "Success getting message", - content: { - "application/json": { schema: resolver(getMessageByIdSchema) } - } - }, - 404: { - description: "Message id not found", - content: { - "application/json": { schema: resolver(getMessageByIdSchema) } - } - } - } - }), - zValidator("param", getMessageByIdSchema), - async (c) => { - const id = c.req.param("id"); - const messageData = await fetchMessageData(id); + "/:id", + describeRoute({ + description: "Get message by id", + responses: { + 200: { + description: "Success getting message", + content: { + "application/json": { schema: resolver(getMessageByIdSchema) }, + }, + }, + 404: { + description: "Message id not found", + content: { + "application/json": { schema: resolver(getMessageByIdSchema) }, + }, + }, + }, + }), + zValidator("param", getMessageByIdSchema), + async (c) => { + const id = c.req.param("id"); + const messageData = await fetchMessageData(id); - if (messageData) { - return c.json(messageData, 200); - } else { - return c.json({ error: "Message not found" }, 404); - } + if (messageData) { + return c.json(messageData, 200); + } else { + return c.json({ error: "Message not found" }, 404); } -) + }, +); messageRoutes.get( - "", - describeRoute({ - description: "Get up to 50 messages prior to given datetime", - responses: { - 200: { - description: "Success getting up to 50 messages", - content: { - "application/json": { schema: resolver(getMessagesBeforeDate) } - } - } - } - }), - zValidator("query", getMessagesBeforeDate), - async (c) => { - const date = c.req.query("date"); - if (!date) { - return c.json({ error: "date not provided" }, 400); - } - - const channelId = c.req.query("channelId"); - if (!channelId) { - return c.json({ error: "channelId not provided" }, 400); - } - - const messagesArr = await fetchMessagesBefore(date, channelId); - - if (messagesArr) { - return c.json(messagesArr, 200); - } else { - return c.json({ error: "Failed to fetch messages" }, 500); - } + "", + describeRoute({ + description: "Get up to 50 messages prior to given datetime", + responses: { + 200: { + description: "Success getting up to 50 messages", + content: { + "application/json": { schema: resolver(getMessagesBeforeDate) }, + }, + }, + }, + }), + zValidator("query", getMessagesBeforeDate), + async (c) => { + const date = c.req.query("date"); + if (!date) { + return c.json({ error: "date not provided" }, 400); } -) + + const channelId = c.req.query("channelId"); + if (!channelId) { + return c.json({ error: "channelId not provided" }, 400); + } + + const messagesArr = await fetchMessagesBefore(date, channelId); + + if (messagesArr) { + return c.json(messagesArr, 200); + } else { + return c.json({ error: "Failed to fetch messages" }, 500); + } + }, +); messageRoutes.post( - "", - describeRoute({ - description: "Send a message to a channel", - responses: { - 201: { - description: "Message sent successfully", - content: { - "application/json": { schema: resolver(sendMessageSchema) } - } + "", + describeRoute({ + description: "Send a message to a channel", + responses: { + 201: { + description: "Message sent successfully", + content: { + "application/json": { schema: resolver(sendMessageSchema) }, + }, + }, + 401: { + description: "Unauthorized - invalid token or user credentials", + content: { + "application/json": { + schema: { + type: "object", + properties: { error: { type: "string" } }, }, - 401: { - description: "Unauthorized - invalid token or user credentials", - content: { - "application/json": { schema: { type: "object", properties: { error: { type: "string" } } } } - } + }, + }, + }, + 500: { + description: "Server error", + content: { + "application/json": { + schema: { + type: "object", + properties: { error: { type: "string" } }, }, - 500: { - description: "Server error", - content: { - "application/json": { schema: { type: "object", properties: { error: { type: "string" } } } } - } - } - } - }), - zValidator("json", sendMessageSchema), - async (c) => { - const { channelId, userId, content, token, repliedMessageId } = await c.req.json(); - - const result = await sendMessage( - channelId, - userId, - content, - token, - repliedMessageId || null - ); + }, + }, + }, + }, + }), + zValidator("json", sendMessageSchema), + async (c) => { + const { channelId, userId, content, token, repliedMessageId } = + await c.req.json(); - if (result) { - return c.json(result, 201); - } else { - return c.json({ error: "Failed to send message. Check your credentials and try again." }, 401); - } + const result = await sendMessage( + channelId, + userId, + content, + token, + repliedMessageId || null, + ); + + if (result) { + return c.json(result, 201); + } else { + return c.json( + { + error: + "Failed to send message. Check your credentials and try again.", + }, + 401, + ); } -) + }, +); -export default messageRoutes; \ No newline at end of file +export default messageRoutes; diff --git a/concord-server/src/services/channelService.ts b/concord-server/src/services/channelService.ts index 17c2b0f..4e12c51 100644 --- a/concord-server/src/services/channelService.ts +++ b/concord-server/src/services/channelService.ts @@ -1,450 +1,457 @@ -import{ - Channel, - Category -} from '@prisma/client'; +import { Channel, Category } from "@prisma/client"; import { PrismaClient } from "@prisma/client"; -import { getUserInformation, getUserCredentials } from './userService'; +import { getUserInformation, getUserCredentials } from "./userService"; import { - CreateChannelInput, - UpdateChannelInput, - DeleteChannelInput, - DeleteChannelsByCategoryIdInput -} from '../validators/channelValidator'; -import{ - UpdateCategoryInput, - DeleteCategoryInput, - DeleteCategoriesByInstanceIdInput, - CreateCategoryInput -} from '../validators/categoryValidator'; + CreateChannelInput, + UpdateChannelInput, + DeleteChannelInput, + DeleteChannelsByCategoryIdInput, +} from "../validators/channelValidator"; +import { + UpdateCategoryInput, + DeleteCategoryInput, + DeleteCategoriesByInstanceIdInput, + CreateCategoryInput, +} from "../validators/categoryValidator"; const prisma = new PrismaClient(); +export async function createCategory( + data: CreateCategoryInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); -export async function createCategory(data: CreateCategoryInput): Promise{ - try{ - - //Confirm if user exists and is admin - const requestingUser = await getUserInformation(data.requestingUserId); - const requestingUserCredentials = await getUserCredentials( - data.requestingUserId, - ) - - if ( - !requestingUser || - !requestingUserCredentials || - !requestingUser.admin || - requestingUserCredentials.token == null || - data.requestingUserToken != requestingUserCredentials.token - ) { - return null; - } - - const newCategory = await prisma.category.create({ - data: { - name: data.name, - position: data.position - } - }); - - if(!newCategory){ - throw new Error("could not create category"); - } - - let curInstance; - if(data.instanceId){ - curInstance = await prisma.instance.findUnique({ - where: { - id: data.instanceId - }, - include: { - Category: true - } - }); - - if(!curInstance){ - throw new Error("could not find instance to add category to"); - } - - await prisma.category.update({ - where: { - id: newCategory.id - }, - data: { - instanceId: curInstance.id - } - }); - - return newCategory; - } - - return newCategory; - }catch(err){ - console.log("services::channelService::createCategory - ", err); - return null; + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; } + + const newCategory = await prisma.category.create({ + data: { + name: data.name, + position: data.position, + }, + }); + + if (!newCategory) { + throw new Error("could not create category"); + } + + let curInstance; + if (data.instanceId) { + curInstance = await prisma.instance.findUnique({ + where: { + id: data.instanceId, + }, + include: { + Category: true, + }, + }); + + if (!curInstance) { + throw new Error("could not find instance to add category to"); + } + + await prisma.category.update({ + where: { + id: newCategory.id, + }, + data: { + instanceId: curInstance.id, + }, + }); + + return newCategory; + } + + return newCategory; + } catch (err) { + console.log("services::channelService::createCategory - ", err); + return null; + } } export async function getCategory( - categoryId: string, -): Promise{ - try{ - const category = await prisma.category.findUnique({ - where: { - id: categoryId - } - }); - - if(!category){ - throw new Error("could not find category"); - } + categoryId: string, +): Promise { + try { + const category = await prisma.category.findUnique({ + where: { + id: categoryId, + }, + }); - return category; - }catch(err){ - console.log("services::channelService::getCategory - ", err); - return null; + if (!category) { + throw new Error("could not find category"); } + + return category; + } catch (err) { + console.log("services::channelService::getCategory - ", err); + return null; + } } export async function getCategoriesByInstance( - instanceId: string -): Promise{ - try{ - const categories = await prisma.category.findMany({ - where: { - instanceId: instanceId - }, - include: { - Channel: true - }, - orderBy: { - position: 'asc' - } - }); + instanceId: string, +): Promise { + try { + const categories = await prisma.category.findMany({ + where: { + instanceId: instanceId, + }, + include: { + Channel: true, + }, + orderBy: { + position: "asc", + }, + }); - if(!categories){ - throw new Error("could not find categories for instance"); - } - - return categories; - }catch(err){ - console.log("services::channelService::getCategoriesByInstance - ", err); - return null; + if (!categories) { + throw new Error("could not find categories for instance"); } + + return categories; + } catch (err) { + console.log("services::channelService::getCategoriesByInstance - ", err); + return null; + } } -export async function updateCategory(data: UpdateCategoryInput): Promise{ - try{ +export async function updateCategory( + data: UpdateCategoryInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); - //Confirm if user exists and is admin - const requestingUser = await getUserInformation(data.requestingUserId); - const requestingUserCredentials = await getUserCredentials( - data.requestingUserId, - ) - - if ( - !requestingUser || - !requestingUserCredentials || - !requestingUser.admin || - requestingUserCredentials.token == null || - data.requestingUserToken != requestingUserCredentials.token - ) { - return null; - } - - const updatedCategory = await prisma.category.update({ - where: { - id: data.id - }, - data: { - name: data.name, - position: data.position, - Channel: data.channels ? { set: data.channels } : undefined - } - }); - - if(!updatedCategory){ - throw new Error("could not update category"); - } - - return updatedCategory; - }catch(err){ - console.log("services::channelService::updateCategory - ", err); - return null; + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; } + + const updatedCategory = await prisma.category.update({ + where: { + id: data.id, + }, + data: { + name: data.name, + position: data.position, + Channel: data.channels ? { set: data.channels } : undefined, + }, + }); + + if (!updatedCategory) { + throw new Error("could not update category"); + } + + return updatedCategory; + } catch (err) { + console.log("services::channelService::updateCategory - ", err); + return null; + } } -export async function deleteCategory(data: DeleteCategoryInput): Promise{ - try{ +export async function deleteCategory( + data: DeleteCategoryInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); - //Confirm if user exists and is admin - const requestingUser = await getUserInformation(data.requestingUserId); - const requestingUserCredentials = await getUserCredentials( - data.requestingUserId, - ) - - if ( - !requestingUser || - !requestingUserCredentials || - !requestingUser.admin || - requestingUserCredentials.token == null || - data.requestingUserToken != requestingUserCredentials.token - ) { - return null; - } - - const deleteAllChannels = await prisma.channel.deleteMany({ - where: { - categoryId: data.id - } - }); - - if(deleteAllChannels.count === 0){ - throw new Error("could not delete channels from category"); - } - - const deletedCategory = await prisma.category.delete({ - where: { - id: data.id - } - }); - - if(!deletedCategory){ - throw new Error("could not delete category"); - } - - return true; - }catch(err){ - console.log("services::channelService::deleteCategory - ", err); - return false; + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; } + + const deleteAllChannels = await prisma.channel.deleteMany({ + where: { + categoryId: data.id, + }, + }); + + if (deleteAllChannels.count === 0) { + throw new Error("could not delete channels from category"); + } + + const deletedCategory = await prisma.category.delete({ + where: { + id: data.id, + }, + }); + + if (!deletedCategory) { + throw new Error("could not delete category"); + } + + return true; + } catch (err) { + console.log("services::channelService::deleteCategory - ", err); + return false; + } } -export async function deleteAllCategoriesFromInstance(data: DeleteCategoriesByInstanceIdInput): Promise{ - try{ +export async function deleteAllCategoriesFromInstance( + data: DeleteCategoriesByInstanceIdInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); - //Confirm if user exists and is admin - const requestingUser = await getUserInformation(data.requestingUserId); - const requestingUserCredentials = await getUserCredentials( - data.requestingUserId, - ) - - if ( - !requestingUser || - !requestingUserCredentials || - !requestingUser.admin || - requestingUserCredentials.token == null || - data.requestingUserToken != requestingUserCredentials.token - ) { - return null; - } - - const deletedCategories = await prisma.category.deleteMany({ - where: { - instanceId: data.instanceId - } - }); - - if(deletedCategories.count === 0){ - throw new Error("could not delete categories from instance"); - } - - return true; - }catch(err){ - console.log("services::channelService::deleteAllCategoriesFromInstance - ", err); - return false; + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; } + + const deletedCategories = await prisma.category.deleteMany({ + where: { + instanceId: data.instanceId, + }, + }); + + if (deletedCategories.count === 0) { + throw new Error("could not delete categories from instance"); + } + + return true; + } catch (err) { + console.log( + "services::channelService::deleteAllCategoriesFromInstance - ", + err, + ); + return false; + } } -export async function createChannel(data: CreateChannelInput): Promise{ - try{ +export async function createChannel( + data: CreateChannelInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); - //Confirm if user exists and is admin - const requestingUser = await getUserInformation(data.requestingUserId); - const requestingUserCredentials = await getUserCredentials( - data.requestingUserId, - ) - - if ( - !requestingUser || - !requestingUserCredentials || - !requestingUser.admin || - requestingUserCredentials.token == null || - data.requestingUserToken != requestingUserCredentials.token - ) { - return null; - } - - const newChannel = await prisma.channel.create({ - data: { - type: data.type, - name: data.name, - description: data.description, - categoryId: data.categoryId ? data.categoryId : null - } - }); - - if(!newChannel){ - throw new Error("could not create channel"); - } - - return newChannel; - }catch(err){ - console.log("services::channelService::createChannel - ", err); - return null; + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; } + + const newChannel = await prisma.channel.create({ + data: { + type: data.type, + name: data.name, + description: data.description, + categoryId: data.categoryId ? data.categoryId : null, + }, + }); + + if (!newChannel) { + throw new Error("could not create channel"); + } + + return newChannel; + } catch (err) { + console.log("services::channelService::createChannel - ", err); + return null; + } } -export async function getChannel( - channelId: string -): Promise{ - try{ - const channel = await prisma.channel.findUnique({ - where: { - id: channelId - } - }); +export async function getChannel(channelId: string): Promise { + try { + const channel = await prisma.channel.findUnique({ + where: { + id: channelId, + }, + }); - if(!channel){ - throw new Error("could not find channel"); - } - - return channel; - }catch(err){ - console.log("services::channelService::getChannel - ", err); - return null; + if (!channel) { + throw new Error("could not find channel"); } + + return channel; + } catch (err) { + console.log("services::channelService::getChannel - ", err); + return null; + } } export async function getChannelsByCategory( - categoryId: string -): Promise{ - try{ - const channels = await prisma.channel.findMany({ - where: { - categoryId: categoryId - } - }); - - if(!channels){ - throw new Error("could not find channels for category"); - } - return channels; + categoryId: string, +): Promise { + try { + const channels = await prisma.channel.findMany({ + where: { + categoryId: categoryId, + }, + }); + + if (!channels) { + throw new Error("could not find channels for category"); } - catch(err){ - console.log("services::channelService::getChannelsByCategory - ", err); - return null; - } + return channels; + } catch (err) { + console.log("services::channelService::getChannelsByCategory - ", err); + return null; + } } -export async function updateChannel(data: UpdateChannelInput): Promise{ - try{ +export async function updateChannel( + data: UpdateChannelInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); - //Confirm if user exists and is admin - const requestingUser = await getUserInformation(data.requestingUserId); - const requestingUserCredentials = await getUserCredentials( - data.requestingUserId, - ) - - if ( - !requestingUser || - !requestingUserCredentials || - !requestingUser.admin || - requestingUserCredentials.token == null || - data.requestingUserToken != requestingUserCredentials.token - ) { - return null; - } - - const updatedChannel = await prisma.channel.update({ - where: { - id: data.id - }, - data: { - name: data.name, - description: data.description, - categoryId: data.categoryId ? data.categoryId : undefined - } - }); - - if(!updatedChannel){ - throw new Error("could not update channel"); - } - - return updatedChannel; - }catch(err){ - console.log("services::channelService::updateChannel - ", err); - return null; + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; } + + const updatedChannel = await prisma.channel.update({ + where: { + id: data.id, + }, + data: { + name: data.name, + description: data.description, + categoryId: data.categoryId ? data.categoryId : undefined, + }, + }); + + if (!updatedChannel) { + throw new Error("could not update channel"); + } + + return updatedChannel; + } catch (err) { + console.log("services::channelService::updateChannel - ", err); + return null; + } } -export async function deleteChannel(data: DeleteChannelInput): Promise{ - try{ +export async function deleteChannel( + data: DeleteChannelInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); - //Confirm if user exists and is admin - const requestingUser = await getUserInformation(data.requestingUserId); - const requestingUserCredentials = await getUserCredentials( - data.requestingUserId, - ) - - if ( - !requestingUser || - !requestingUserCredentials || - !requestingUser.admin || - requestingUserCredentials.token == null || - data.requestingUserToken != requestingUserCredentials.token - ) { - return null; - } - - const deletedChannel = await prisma.channel.delete({ - where: { - id: data.id - } - }); - - if(!deletedChannel){ - throw new Error("could not delete channel"); - } - - return true; - }catch(err){ - console.log("services::channelService::deleteChannel - ", err); - return false; + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; } + + const deletedChannel = await prisma.channel.delete({ + where: { + id: data.id, + }, + }); + + if (!deletedChannel) { + throw new Error("could not delete channel"); + } + + return true; + } catch (err) { + console.log("services::channelService::deleteChannel - ", err); + return false; + } } -export async function deleteAllChannelsFromCategory(data: DeleteChannelsByCategoryIdInput): Promise -{ - try{ - //Confirm if user exists and is admin - const requestingUser = await getUserInformation(data.requestingUserId); - const requestingUserCredentials = await getUserCredentials( - data.requestingUserId, - ) +export async function deleteAllChannelsFromCategory( + data: DeleteChannelsByCategoryIdInput, +): Promise { + try { + //Confirm if user exists and is admin + const requestingUser = await getUserInformation(data.requestingUserId); + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); - if ( - !requestingUser || - !requestingUserCredentials || - !requestingUser.admin || - requestingUserCredentials.token == null || - data.requestingUserToken != requestingUserCredentials.token - ) { - return null; - } - - const deletedChannels = await prisma.channel.deleteMany({ - where: { - categoryId: data.categoryId - } - }); - - if(deletedChannels.count === 0){ - throw new Error("could not delete channels from category"); - } - - return true; - }catch(err){ - console.log("services::channelService::deleteAllChannelsFromCategory - ", err); - return false; + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { + return null; } + + const deletedChannels = await prisma.channel.deleteMany({ + where: { + categoryId: data.categoryId, + }, + }); + + if (deletedChannels.count === 0) { + throw new Error("could not delete channels from category"); + } + + return true; + } catch (err) { + console.log( + "services::channelService::deleteAllChannelsFromCategory - ", + err, + ); + return false; + } } diff --git a/concord-server/src/services/instanceService.ts b/concord-server/src/services/instanceService.ts index d37f3a0..bb4008a 100644 --- a/concord-server/src/services/instanceService.ts +++ b/concord-server/src/services/instanceService.ts @@ -5,53 +5,55 @@ import { getUserCredentials, getUserInformation } from "./userService"; const prisma = new PrismaClient(); export async function createInstance(data: CreateInstanceRequest) { - try { - const creds = await getUserCredentials(data.requestingUserId); - const user = await getUserInformation(data.requestingUserId); - if (!creds - || creds.token != data.requestingUserToken - || !user - || !user.admin) { - return null; - } - - const newInstance = await prisma.instance.create({ - data: { - name: data.name, - icon: data.icon - } - }); - - return { - success: true, - data: newInstance - }; - } catch (error) { - console.error("Error creating instance:", error); - return { - success: false, - error: "Failed to create instance" - }; + try { + const creds = await getUserCredentials(data.requestingUserId); + const user = await getUserInformation(data.requestingUserId); + if ( + !creds || + creds.token != data.requestingUserToken || + !user || + !user.admin + ) { + return null; } + + const newInstance = await prisma.instance.create({ + data: { + name: data.name, + icon: data.icon, + }, + }); + + return { + success: true, + data: newInstance, + }; + } catch (error) { + console.error("Error creating instance:", error); + return { + success: false, + error: "Failed to create instance", + }; + } } export async function getAllInstances() { - try { - const instances = await prisma.instance.findMany({ - orderBy: { - createdAt: 'desc' - } - }); - - return { - success: true, - data: instances - }; - } catch (error) { - console.error("Error fetching instances:", error); - return { - success: false, - error: "Failed to fetch instances" - }; - } -} \ No newline at end of file + try { + const instances = await prisma.instance.findMany({ + orderBy: { + createdAt: "desc", + }, + }); + + return { + success: true, + data: instances, + }; + } catch (error) { + console.error("Error fetching instances:", error); + return { + success: false, + error: "Failed to fetch instances", + }; + } +} diff --git a/concord-server/src/services/messageService.ts b/concord-server/src/services/messageService.ts index 33c3ca1..a01f59e 100644 --- a/concord-server/src/services/messageService.ts +++ b/concord-server/src/services/messageService.ts @@ -1,16 +1,14 @@ -import { - PrismaClient, -} from "@prisma/client"; +import { PrismaClient } from "@prisma/client"; import { getUserCredentials } from "./userService"; const prisma = new PrismaClient(); -export async function getMessageInformation(id:string): Promise<{ - id: string, - channelId: string, - userId: string, - text: string, - deleted: boolean, +export async function getMessageInformation(id: string): Promise<{ + id: string; + channelId: string; + userId: string; + text: string; + deleted: boolean; replies: null | { messageId: string; repliesToId: string; @@ -66,26 +64,28 @@ export async function getMessageInformation(id:string): Promise<{ const errMessage = err as Error; if (errMessage.message === "missing messageId") { - console.log("services::actions::getMessageInformation - missing messageId"); + console.log( + "services::actions::getMessageInformation - missing messageId", + ); return null; } if (errMessage.message === "could not find message") { console.log( - "services::actions::getMessageInformation - unable to find message" + "services::actions::getMessageInformation - unable to find message", ); return null; } console.log( "services::actions::getMessageInformation - unknown error", - errMessage + errMessage, ); return null; } } -export async function getMessagesBefore(date:string, channelId:string) { +export async function getMessagesBefore(date: string, channelId: string) { try { if (!date || !channelId) { throw new Error("missing date or channelId"); @@ -141,13 +141,15 @@ export async function getMessagesBefore(date:string, channelId:string) { const errMessage = err as Error; if (errMessage.message === "missing date or channelId") { - console.log("services::actions::getMessagesBefore - missing date or channelId"); + console.log( + "services::actions::getMessagesBefore - missing date or channelId", + ); return null; } console.log( "services::actions::getMessagesBefore - unknown error", - errMessage + errMessage, ); return null; } diff --git a/concord-server/src/services/userService.ts b/concord-server/src/services/userService.ts index 6598b19..4730981 100644 --- a/concord-server/src/services/userService.ts +++ b/concord-server/src/services/userService.ts @@ -1,4 +1,3 @@ - import { Message, MessagePing, diff --git a/concord-server/src/validators/categoryValidator.ts b/concord-server/src/validators/categoryValidator.ts index 5dde0ee..f8ef491 100644 --- a/concord-server/src/validators/categoryValidator.ts +++ b/concord-server/src/validators/categoryValidator.ts @@ -1,55 +1,61 @@ -import { z } from 'zod'; +import { z } from "zod"; //category validators export const createCategorySchema = z.object({ - name: z.string().min(1).max(50), - position: z.number().min(0), - instanceId : z.uuidv7().optional(), - admin: z.boolean(), - requestingUserId: z.uuidv7(), - requestingUserToken: z.uuidv4() -}) + name: z.string().min(1).max(50), + position: z.number().min(0), + instanceId: z.uuidv7().optional(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); export const getCategorySchema = z.object({ - id: z.uuidv7() -}) + id: z.uuidv7(), +}); export const getCategoriesByInstanceIdSchema = z.object({ - instanceId: z.uuidv7() - -}) + instanceId: z.uuidv7(), +}); export const updateCategorySchema = z.object({ - id: z.uuidv7(), - name: z.string().min(1).max(50).optional(), - position: z.number().min(0).optional(), - channels: z.array(z.object({ - id: z.string() - })).optional(), - admin: z.boolean(), - requestingUserId: z.uuidv7(), - requestingUserToken: z.uuidv4() -}) + id: z.uuidv7(), + name: z.string().min(1).max(50).optional(), + position: z.number().min(0).optional(), + channels: z + .array( + z.object({ + id: z.string(), + }), + ) + .optional(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); export const deleteCategorySchema = z.object({ - id: z.uuidv7(), - admin: z.boolean(), - requestingUserId: z.uuidv7(), - requestingUserToken: z.uuidv4() -}) + id: z.uuidv7(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); export const deleteCategoriesByInstanceIdSchema = z.object({ - instanceId: z.uuidv7(), - admin: z.boolean(), - requestingUserId: z.uuidv7(), - requestingUserToken: z.uuidv4() -}) + instanceId: z.uuidv7(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); - -export type CreateCategoryInput = z.infer -export type GetCategoryInput = z.infer -export type GetCategoriesByInstanceIdInput = z.infer -export type UpdateCategoryInput = z.infer -export type DeleteCategoryInput = z.infer -export type DeleteCategoriesByInstanceIdInput = z.infer +export type CreateCategoryInput = z.infer; +export type GetCategoryInput = z.infer; +export type GetCategoriesByInstanceIdInput = z.infer< + typeof getCategoriesByInstanceIdSchema +>; +export type UpdateCategoryInput = z.infer; +export type DeleteCategoryInput = z.infer; +export type DeleteCategoriesByInstanceIdInput = z.infer< + typeof deleteCategoriesByInstanceIdSchema +>; diff --git a/concord-server/src/validators/channelValidator.ts b/concord-server/src/validators/channelValidator.ts index 53e48bc..23cb918 100644 --- a/concord-server/src/validators/channelValidator.ts +++ b/concord-server/src/validators/channelValidator.ts @@ -3,50 +3,54 @@ import { z } from "zod"; //channel validators export const createChannelSchema = z.object({ - type: z.enum(['text', 'voice']), - name: z.string().min(1).max(50), - description: z.string().max(255), - categoryId: z.uuidv7().optional(), - admin: z.boolean(), - requestingUserId: z.uuidv7(), - requestingUserToken: z.uuidv4() -}) + type: z.enum(["text", "voice"]), + name: z.string().min(1).max(50), + description: z.string().max(255), + categoryId: z.uuidv7().optional(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); export const getChannelSchema = z.object({ - id: z.uuidv7() -}) + id: z.uuidv7(), +}); export const getChannelsByCategoryIdSchema = z.object({ - categoryId: z.uuidv7() -}) + categoryId: z.uuidv7(), +}); export const updateChannelSchema = z.object({ - id: z.uuidv7(), - name: z.string().min(1).max(50).optional(), - description: z.string().max(255).optional(), - categoryId: z.uuidv7().optional(), - admin: z.boolean(), - requestingUserId: z.uuidv7(), - requestingUserToken: z.uuidv4() -}) + id: z.uuidv7(), + name: z.string().min(1).max(50).optional(), + description: z.string().max(255).optional(), + categoryId: z.uuidv7().optional(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); export const deleteChannelSchema = z.object({ - id: z.uuidv7(), - admin: z.boolean(), - requestingUserId: z.uuidv7(), - requestingUserToken: z.uuidv4() -}) + id: z.uuidv7(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); export const deleteChannelsByCategoryIdSchema = z.object({ - categoryId: z.uuidv7(), - admin: z.boolean(), - requestingUserId: z.uuidv7(), - requestingUserToken: z.uuidv4() -}) + categoryId: z.uuidv7(), + admin: z.boolean(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.uuidv4(), +}); -export type CreateChannelInput = z.infer -export type GetChannelInput = z.infer -export type GetChannelsByCategoryIdInput = z.infer -export type UpdateChannelInput = z.infer -export type DeleteChannelInput = z.infer -export type DeleteChannelsByCategoryIdInput = z.infer \ No newline at end of file +export type CreateChannelInput = z.infer; +export type GetChannelInput = z.infer; +export type GetChannelsByCategoryIdInput = z.infer< + typeof getChannelsByCategoryIdSchema +>; +export type UpdateChannelInput = z.infer; +export type DeleteChannelInput = z.infer; +export type DeleteChannelsByCategoryIdInput = z.infer< + typeof deleteChannelsByCategoryIdSchema +>; diff --git a/concord-server/src/validators/instanceValidator.ts b/concord-server/src/validators/instanceValidator.ts index a4497fe..880f957 100644 --- a/concord-server/src/validators/instanceValidator.ts +++ b/concord-server/src/validators/instanceValidator.ts @@ -1,29 +1,33 @@ -import { z } from 'zod'; +import { z } from "zod"; export const createInstanceRequestSchema = z.object({ - name: z.string().min(1, 'Instance name cannot be empty'), - icon: z.url().optional(), - requestingUserId: z.uuidv7(), - requestingUserToken: z.string() + name: z.string().min(1, "Instance name cannot be empty"), + icon: z.url().optional(), + requestingUserId: z.uuidv7(), + requestingUserToken: z.string(), }); export const getAllInstancesResponseSchema = z.object({ - success: z.boolean(), - data: z.array( - z.object({ - id: z.string(), - name: z.string(), - icon: z.string().nullable(), - createdAt: z.string().refine((val) => !isNaN(Date.parse(val)), { - message: "Invalid date string format" - }), - updatedAt: z.string().refine((val) => !isNaN(Date.parse(val)), { - message: "Invalid date string format" - }) - }) - ).optional(), - error: z.string().optional() + success: z.boolean(), + data: z + .array( + z.object({ + id: z.string(), + name: z.string(), + icon: z.string().nullable(), + createdAt: z.string().refine((val) => !isNaN(Date.parse(val)), { + message: "Invalid date string format", + }), + updatedAt: z.string().refine((val) => !isNaN(Date.parse(val)), { + message: "Invalid date string format", + }), + }), + ) + .optional(), + error: z.string().optional(), }); export type CreateInstanceRequest = z.infer; -export type GetAllInstancesResponse = z.infer; \ No newline at end of file +export type GetAllInstancesResponse = z.infer< + typeof getAllInstancesResponseSchema +>; diff --git a/concord-server/src/validators/messageValidator.ts b/concord-server/src/validators/messageValidator.ts index 83e4920..baead86 100644 --- a/concord-server/src/validators/messageValidator.ts +++ b/concord-server/src/validators/messageValidator.ts @@ -1,20 +1,20 @@ import { z } from "zod"; export const getMessageByIdSchema = z.object({ - id: z.uuidv7() -}) + id: z.uuidv7(), +}); export const getMessagesBeforeDate = z.object({ - date: z.string().refine((val) => !isNaN(Date.parse(val)), { - message: "Invalid date string format" - }), - channelId: z.uuidv7() -}) + date: z.string().refine((val) => !isNaN(Date.parse(val)), { + message: "Invalid date string format", + }), + channelId: z.uuidv7(), +}); export const sendMessageSchema = z.object({ - channelId: z.uuidv7(), - userId: z.uuidv7(), - content: z.string(), - token: z.string(), - repliedMessageId: z.uuidv7().nullable().optional() -}) \ No newline at end of file + channelId: z.uuidv7(), + userId: z.uuidv7(), + content: z.string(), + token: z.string(), + repliedMessageId: z.uuidv7().nullable().optional(), +}); diff --git a/concord-server/src/validators/userValidator.ts b/concord-server/src/validators/userValidator.ts index 0789128..e4f1632 100644 --- a/concord-server/src/validators/userValidator.ts +++ b/concord-server/src/validators/userValidator.ts @@ -7,7 +7,7 @@ export const queryUserByIdSchema = z.object({ export const queryAllUsersByInstanceId = z.object({ instanceId: z.uuidv7(), }); -import { is } from 'zod/v4/locales'; +import { is } from "zod/v4/locales"; export const createUserSchema = z.object({ username: z.string().min(3).max(30), nickname: z.string().min(1).max(30).optional(),