From 3c7462ad344b2e3681742f6f8a6721249c1db67c Mon Sep 17 00:00:00 2001 From: nurhudajoantama Date: Thu, 23 May 2024 17:01:01 +0700 Subject: [PATCH 1/5] docs(chats): add chats documentation to swagger --- openapi.json | 674 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 674 insertions(+) diff --git a/openapi.json b/openapi.json index 3ebd1b5..1917dd4 100644 --- a/openapi.json +++ b/openapi.json @@ -2457,6 +2457,497 @@ } } } + }, + "/chat-rooms": { + "get": { + "tags": ["Chat Room"], + "summary": "Get all chat rooms", + "description": "Retrieve all chat rooms", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "Get chat rooms by user id", + "required": false, + "schema": { + "type": "string", + "nullable": true + }, + "examples": { + "User 1": { + "description": "Get chat rooms by user id 1", + "value": "d969ee14-3362-46cd-ace9-af4d7b131733" + } + } + } + ], + "responses": { + "200": { + "description": "Successfully retrieved all chat rooms", + "content": { + "application/json": { + "schema": { + "type": "object", + "$ref": "#/components/schemas/SuccessGetAllChatRooms" + }, + "examples": { + "All chat rooms": { + "description": "Successfully get all chat rooms", + "value": { + "status": "success", + "message": "Get all chat rooms", + "data": { + "chatRooms": [ + { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733", + "type": "AI", + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733" + }, + { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733", + "type": "Expert", + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733" + } + ] + } + } + } + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": ["Chat Room"], + "summary": "Create a new chat room", + "description": "Add a new chat room to the app", + "requestBody": { + "description": "Chat room object that needs to be added", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsertAllFieldChatRoom" + }, + "examples": { + "New chat room": { + "description": "Example of a new chat room", + "value": { + "type": "AI", + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Successfully created a new chat room", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessSignupOrRegisUser" + }, + "examples": { + "Chat Room 1": { + "description": "Successfully created a new chat room", + "value": { + "status": "success", + "message": "Chat room created successfully", + "data": { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733" + } + } + } + } + } + } + }, + "400": { + "description": "Failed add new chat room because validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailValidationSignupOrRegister" + }, + "examples": { + "name empty": { + "description": "Fail add new chat room because name field empty", + "value": { + "status": "fail", + "message": "Validation error", + "errors": [ + { + "field": "name", + "message": "name is required" + } + ] + } + } + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/chat-rooms/{chatRoomId}": { + "get": { + "tags": ["Chat Room"], + "summary": "Get a chat room by id", + "description": "Get specific chat room by id", + "parameters": [ + { + "$ref": "#/components/parameters/ChatRoomId" + } + ], + "responses": { + "200": { + "description": "Successfully retrieved the chat room", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessGetChatRoom" + }, + "examples": { + "Chat Room": { + "description": "Successfully retrieved the chat room", + "value": { + "status": "success", + "message": "Get chat room data by id", + "data": { + "chatRoom": { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733", + "type": "AI", + "user": { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733", + "name": "User 1" + } + } + } + } + } + } + } + } + }, + "404": { + "$ref": "#/components/responses/ErrorChatRoomNotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "put": { + "tags": ["Chat Room"], + "summary": "Update a chat room by id", + "description": "Update specific chat room by id", + "parameters": [ + { + "$ref": "#/components/parameters/ChatRoomId" + } + ], + "requestBody": { + "description": "Chat room object that needs to be updated", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAllFieldChatRoom" + }, + "examples": { + "Update chat room": { + "description": "Example of a chat room update", + "value": { + "type": "Expert", + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733", + "expertId": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully update the chat room", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessSignupOrRegisUser" + }, + "examples": { + "Chat Room updated": { + "description": "Successfully updated the chat room", + "value": { + "status": "success", + "message": "Chat room updated successfully", + "data": { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733" + } + } + } + } + } + } + }, + "400": { + "description": "Failed to update chat room due to validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailValidationSignupOrRegister" + }, + "examples": { + "name empty": { + "description": "Failed to update chat room because name field is empty", + "value": { + "status": "fail", + "message": "Validation error", + "errors": [ + { + "field": "name", + "message": "name is required" + } + ] + } + } + } + } + } + }, + "404": { + "$ref": "#/components/responses/ErrorChatRoomNotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": ["Chat Room"], + "summary": "Delete a chat room by id", + "description": "Delete a specific chat room by its ID", + "parameters": [ + { + "$ref": "#/components/parameters/ChatRoomId" + } + ], + "responses": { + "200": { + "description": "Successfully deleted the chat room", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessSignupOrRegisUser" + }, + "examples": { + "Chat Room deleted": { + "description": "Successfully deleted the chat room", + "value": { + "status": "success", + "message": "Chat room deleted successfully", + "data": { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733" + } + } + } + } + } + } + }, + "404": { + "$ref": "#/components/responses/ErrorChatRoomNotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/chats/ai/predict":{ + "post": { + "tags": ["Chat Room"], + "summary": "Predict chat message", + "description": "Predict chat message", + "requestBody": { + "description": "Chat message object that needs to be predicted", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsertAllFieldChatMessage" + }, + "examples": { + "New chat message": { + "description": "Example of a new chat message", + "value": { + "question": "Halo" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully predicted the chat message", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessPredictChatMessage" + }, + "examples": { + "Chat Message": { + "description": "Successfully predicted the chat message", + "value": { + "status": "success", + "message": "Predict chat message", + "data": { + "answer": "Halo" + } + } + } + } + } + } + }, + "400": { + "description": "Failed predict chat message because validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailValidationPredictChatMessage" + }, + "examples": { + "message empty": { + "description": "Fail predict chat message because message field empty", + "value": { + "status": "fail", + "message": "Validation error", + "errors": [ + { + "field": "message", + "message": "message is required" + } + ] + } + } + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/chats/ai/suggestions" : { + "post": { + "tags": ["Chat Room"], + "summary": "Get all chat suggestions", + "description": "Retrieve all chat suggestions", + "requestBody": { + "description": "Chat suggestion object that needs to be retrieved", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsertAllFieldChatSuggestion" + }, + "examples": { + "New chat suggestion": { + "description": "Example of a new chat suggestion", + "value": { + "user_input": "Halo", + "total_questions": 4 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully retrieved all chat suggestions", + "content": { + "application/json": { + "schema": { + "type": "object", + "$ref": "#/components/schemas/SuccessGetAllChatSuggestions" + }, + "examples": { + "All chat suggestions": { + "description": "Successfully get all chat suggestions", + "value": { + "status": "success", + "message": "Get all chat suggestions", + "data": { + "suggested_questions": [ + "Apa saja jenis reksadana yang tersedia di Indonesia?", + "Apa itu reksadana offshore dan bagaimana cara berinvestasi di dalamnya?", + "Apa itu reksadana terproteksi dan bagaimana cara kerjanya?", + "Bagaimana cara menghindari penipuan dalam investasi reksadana?" + ], + "probability" : 0.9, + "top_category": "Reksadana" + } + } + } + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } } }, "components": { @@ -2536,6 +3027,21 @@ "value": "8a69ef80-8d1d-4832-9c6a-fba9d9f16230" } } + }, + "ChatRoomId": { + "name": "chatRoomId", + "in": "path", + "description": "Room chat ID", + "required": true, + "schema": { + "type": "string" + }, + "examples": { + "Room Chat 1": { + "description": "Sample room chat id for room chat 1", + "value": "d969ee14-3362-46cd-ace9-af4d7b131733" + } + } } }, "schemas": { @@ -3146,7 +3652,175 @@ "type": "string" } } + }, + "GetChatRoom": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["Expert", "AI"] + }, + "user": { + "$ref": "#/components/schemas/User" + }, + "expert": { + "$ref": "#/components/schemas/User" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, + "SuccessGetChatRoom": { + "allOf": [ + { + "$ref": "#/components/schemas/Success" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "chatRoom": { + "$ref": "#/components/schemas/GetChatRoom" + } + } + } + } + } + ] + }, + "SuccessGetAllChatRooms": { + "allOf": [ + { + "$ref": "#/components/schemas/Success" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "chatRooms": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GetChatRoom" + } + } + } + } + } + } + ] + }, + "InsertAllFieldChatRoom": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["Expert", "AI"] + }, + "userId": { + "type": "string" + }, + "expertId": { + "type": "string", + "nullable": true + } + } + }, + "UpdateAllFieldChatRoom": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["Expert", "AI"] + }, + "userId": { + "type": "string" + }, + "expertId": { + "type": "string", + "nullable": true + } + } + }, + "InsertAllFieldChatMessage": { + "type": "object", + "properties": { + "question": { + "type": "string" + } + } + }, + "InsertAllFieldChatSuggestion": { + "type": "object", + "properties": { + "user_input": { + "type": "string" + }, + "total_questions": { + "type": "integer" + } + } + }, + "SuccessPredictChatAi": { + "allOf": [ + { + "$ref": "#/components/schemas/Success" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "answer": { + "type": "string" + } + } + } + } + } + ] + }, + "SuccessSuggestChatAi": { + "allOf": [ + { + "$ref": "#/components/schemas/Success" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "suggested_questions": { + "type": "array", + "items": { + "type": "string" + } + }, + "probability": { + "type": "number" + }, + "top_category": { + "type": "string" + } + } + } + } + } + ] } + }, "responses": { "MiddlewareUnauthorizedError": { From 14f549149ee0a263e0db2a6dc48db3e0b1930e89 Mon Sep 17 00:00:00 2001 From: nurhudajoantama Date: Thu, 30 May 2024 11:59:10 +0700 Subject: [PATCH 2/5] docs(chats): change chat doc follow spec --- openapi.json | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/openapi.json b/openapi.json index 1917dd4..785e37c 100644 --- a/openapi.json +++ b/openapi.json @@ -2948,6 +2948,78 @@ } ] } + }, + "/chats/ai/{id}/send":{ + "post": { + "tags": ["Chat"], + "summary": "Send message to AI chat by ID", + "description": "Send message to AI chat by ID. Requires valid access token", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Room chat ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, + "examples": { + "Example": { + "description": "Example send message to AI chat", + "value": { + "message": "Hello AI" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully send message to AI chat", + "content": { + "application/json": { + "schema": { + "type": "object", + "$ref": "#/components/schemas/Success" + }, + "examples": { + "Success": { + "description": "Successfully send message to AI chat", + "value": { + "status": "success", + "message": "Send message to AI chat successfully" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/MiddlewareUnauthorizedError" + }, + "403": { + "$ref": "#/components/responses/MiddlewareForbiddenError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } } }, "components": { From 7fe0bc8bc4b81fd71e93edb20ad415402170c3e5 Mon Sep 17 00:00:00 2001 From: nurhudajoantama Date: Sat, 8 Jun 2024 14:56:22 +0700 Subject: [PATCH 3/5] feat(chats): implement restfull api for room chats --- openapi.json | 145 ++++++++++++++---- .../migration.sql | 17 ++ prisma/schema.prisma | 24 +++ src/controller/chat-rooms.controller.js | 131 ++++++++++++++++ src/repository/chat-rooms.repository.js | 136 ++++++++++++++++ src/routes/api.js | 2 + src/routes/router/chat-rooms.router.js | 42 +++++ src/schema/chat-rooms.schema.js | 13 ++ src/service/chat-rooms.service.js | 52 +++++++ 9 files changed, 534 insertions(+), 28 deletions(-) create mode 100644 prisma/migrations/20240608071749_create_chat_rooms_table/migration.sql create mode 100644 src/controller/chat-rooms.controller.js create mode 100644 src/repository/chat-rooms.repository.js create mode 100644 src/routes/router/chat-rooms.router.js create mode 100644 src/schema/chat-rooms.schema.js create mode 100644 src/service/chat-rooms.service.js diff --git a/openapi.json b/openapi.json index 785e37c..2ef40c9 100644 --- a/openapi.json +++ b/openapi.json @@ -25,6 +25,10 @@ ] } } + }, + { + "description": "Local Finboost Back-End URL", + "url": "http://localhost:3000/api/v1" } ], "tags": [ @@ -2956,32 +2960,24 @@ "description": "Send message to AI chat by ID. Requires valid access token", "parameters": [ { - "name": "id", - "in": "path", - "description": "Room chat ID", - "required": true, - "schema": { - "type": "string" - } + "$ref": "#/components/parameters/ChatRoomId" } ], "requestBody": { + "description": "Chat room object that needs to be updated", "required": true, "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } + "$ref": "#/components/schemas/UpdateAllFieldChatRoom" }, "examples": { - "Example": { - "description": "Example send message to AI chat", + "Update chat room": { + "description": "Example of a chat room update", "value": { - "message": "Hello AI" + "type": "Expert", + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733", + "expertId": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" } } } @@ -2990,35 +2986,109 @@ }, "responses": { "200": { - "description": "Successfully send message to AI chat", + "description": "Successfully update the chat room", "content": { "application/json": { "schema": { - "type": "object", - "$ref": "#/components/schemas/Success" + "$ref": "#/components/schemas/SuccessSignupOrRegisUser" }, "examples": { - "Success": { - "description": "Successfully send message to AI chat", + "Chat Room updated": { + "description": "Successfully updated the chat room", "value": { "status": "success", - "message": "Send message to AI chat successfully" + "message": "Chat room updated successfully", + "data": { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733" + } } } } } } }, - "401": { - "$ref": "#/components/responses/MiddlewareUnauthorizedError" + "400": { + "description": "Failed to update chat room due to validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailValidationSignupOrRegister" + }, + "examples": { + "name empty": { + "description": "Failed to update chat room because name field is empty", + "value": { + "status": "fail", + "message": "Validation error", + "errors": [ + { + "field": "name", + "message": "name is required" + } + ] + } + } + } + } + } }, - "403": { - "$ref": "#/components/responses/MiddlewareForbiddenError" + "404": { + "$ref": "#/components/responses/ErrorChatRoomNotFound" }, "500": { "$ref": "#/components/responses/InternalServerError" } - } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": ["Chat Room"], + "summary": "Delete a chat room by id", + "description": "Delete a specific chat room by its ID", + "parameters": [ + { + "$ref": "#/components/parameters/ChatRoomId" + } + ], + "responses": { + "200": { + "description": "Successfully deleted the chat room", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessSignupOrRegisUser" + }, + "examples": { + "Chat Room deleted": { + "description": "Successfully deleted the chat room", + "value": { + "status": "success", + "message": "Chat room deleted successfully", + "data": { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733" + } + } + } + } + } + } + }, + "404": { + "$ref": "#/components/responses/ErrorChatRoomNotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] } } }, @@ -3892,7 +3962,6 @@ } ] } - }, "responses": { "MiddlewareUnauthorizedError": { @@ -4034,6 +4103,26 @@ } } } + }, + "ErrorChatRoomNotFound": { + "description": "Chat room not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "fail" + }, + "message": { + "type": "string", + "example": "Chat room not found" + } + } + } + } + } } } } diff --git a/prisma/migrations/20240608071749_create_chat_rooms_table/migration.sql b/prisma/migrations/20240608071749_create_chat_rooms_table/migration.sql new file mode 100644 index 0000000..f821885 --- /dev/null +++ b/prisma/migrations/20240608071749_create_chat_rooms_table/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE `chat_rooms` ( + `id` VARCHAR(191) NOT NULL, + `type` ENUM('Expert', 'AI') NOT NULL, + `userId` VARCHAR(191) NOT NULL, + `expertId` VARCHAR(191) NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `chat_rooms` ADD CONSTRAINT `chat_rooms_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `chat_rooms` ADD CONSTRAINT `chat_rooms_expertId_fkey` FOREIGN KEY (`expertId`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4f59c44..7bb2333 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -31,6 +31,9 @@ model User { profile Profile? + UserChatRoom ChatRoom[] @relation("UserChatRoom") + ExpertChatRoom ChatRoom[] @relation("ExpertChatRoom") + @@map("users") } @@ -87,6 +90,22 @@ model Education { @@map("educations") } +model ChatRoom { + id String @id @default(uuid()) + type ChatRoomType + + user User @relation("UserChatRoom", fields: [userId], references: [id]) + userId String + + expert User? @relation("ExpertChatRoom", fields: [expertId], references: [id]) + expertId String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("chat_rooms") +} + enum UserGender { Laki_laki @map("Laki-laki") Perempuan @@ -97,3 +116,8 @@ enum ProfileMarital { Menikah Cerai } + +enum ChatRoomType { + Expert + AI +} diff --git a/src/controller/chat-rooms.controller.js b/src/controller/chat-rooms.controller.js new file mode 100644 index 0000000..7feb0ca --- /dev/null +++ b/src/controller/chat-rooms.controller.js @@ -0,0 +1,131 @@ +import { handleServerError } from "../exceptions/server.exception.js"; +import { handleZodError } from "../exceptions/zod.exception.js"; +import { NotFoundError } from "../exceptions/client.exception.js"; +import { + editChatRoomById, + getAllChatRooms, + getAllChatRoomsByUserId, + getChatRoomById, + insertChatRoom, + removeChatRoomById, +} from "../service/chat-rooms.service.js"; +import { + EditChatRoomSchema, + InsertChatRoomSchema, +} from "../schema/chat-rooms.schema.js"; + +export const getAllChatRoomsHandler = async (req, res) => { + try { + const userId = req.query.userId; + + let chatRooms; + if (userId) { + chatRooms = await getAllChatRoomsByUserId(userId); + } else { + chatRooms = await getAllChatRooms(); + } + + res.status(200).send({ + status: "success", + message: "Get all chat room profile user", + data: { + chatRooms, + }, + }); + } catch (error) { + console.log(error); + handleServerError(error, res); + } +}; + +export const insertChatRoomHandler = async (req, res) => { + try { + const validateData = InsertChatRoomSchema.parse(req.body); + + const newChatRoomData = await insertChatRoom(validateData); + + res.status(201).send({ + status: "success", + message: "Chat room created successfully", + data: { + id: newChatRoomData.id, + }, + }); + } catch (error) { + try { + handleZodError(error, res); + } catch (err) { + handleServerError(err, res); + } + } +}; + +export const getChatRoomByIdHandler = async (req, res) => { + try { + const chatRoomId = req.params.chatRoomId; + const chatRoom = await getChatRoomById(chatRoomId, res); + + res.status(200).send({ + status: "success", + message: "Get chat room data by id", + data: { + chatRoom, + }, + }); + } catch (error) { + if (error instanceof NotFoundError) { + return; + } + handleServerError(error, res); + } +}; + +export const editChatRoomByIdHandler = async (req, res) => { + try { + const chatRoomId = req.params.chatRoomId; + const validateData = EditChatRoomSchema.parse(req.body); + + const newChatRoomData = await editChatRoomById( + chatRoomId, + validateData, + res + ); + + res.status(200).send({ + status: "success", + message: "Chat room updated successfully", + data: { + id: newChatRoomData.id, + }, + }); + } catch (error) { + try { + handleZodError(error, res); + } catch (err) { + if (err instanceof NotFoundError) { + return; + } + handleServerError(err, res); + } + } +}; + +export const removeChatRoomByIdHandler = async (req, res) => { + try { + const chatRoomId = req.params.chatRoomId; + await removeChatRoomById(chatRoomId, res); + + res.status(200).send({ + status: "success", + message: "Chat room deleted successfully", + data: { + id: chatRoomId, + }, + }); + } catch (error) { + if (error instanceof NotFoundError) { + return; + } + handleServerError(error, res); + } +}; \ No newline at end of file diff --git a/src/repository/chat-rooms.repository.js b/src/repository/chat-rooms.repository.js new file mode 100644 index 0000000..0228dd8 --- /dev/null +++ b/src/repository/chat-rooms.repository.js @@ -0,0 +1,136 @@ +import prisma from "../../db/prisma.js"; + +export const findChatRooms = async () => { + const chatRooms = await prisma.chatRoom.findMany({ + select: { + id: true, + type: true, + userId: true, + user: { + select: { + id: true, + fullName: true, + email: true, + createdAt: true, + updatedAt: true, + }, + }, + expertId: true, + expert: { + select: { + id: true, + fullName: true, + email: true, + createdAt: true, + updatedAt: true, + }, + }, + createdAt: true, + updatedAt: true, + } + }); + + return chatRooms; +}; + +export const findChatRoomsByUserId = async (userId) => { + const chatRooms = await prisma.chatRoom.findMany({ + where: { + userId, + }, + select: { + id: true, + type: true, + userId: true, + user: { + select: { + id: true, + fullName: true, + email: true, + createdAt: true, + updatedAt: true, + }, + }, + expertId: true, + expert: { + select: { + id: true, + fullName: true, + email: true, + createdAt: true, + updatedAt: true, + }, + }, + createdAt: true, + updatedAt: true, + }, + }); + + return chatRooms; +}; + +export const createChatRoom = async (newChatRoomData) => { + const newChatRoom = await prisma.chatRoom.create({ + data: newChatRoomData, + }); + + return newChatRoom; +}; + +export const findChatRoomById = async (chatRoomId) => { + const chatRoom = await prisma.chatRoom.findUnique({ + where: { + id: chatRoomId, + }, + select: { + id: true, + type: true, + userId: true, + user: { + select: { + id: true, + fullName: true, + email: true, + createdAt: true, + updatedAt: true, + }, + }, + expertId: true, + expert: { + select: { + id: true, + fullName: true, + email: true, + createdAt: true, + updatedAt: true, + }, + }, + createdAt: true, + updatedAt: true, + }, + }); + + return chatRoom; +}; + +export const updateChatRoomById = async (chatRoomId, newChatRoomData) => { + console.log(chatRoomId, newChatRoomData); + const chatRoom = await prisma.chatRoom.update({ + where: { + id: chatRoomId, + }, + data: newChatRoomData, + }); + + return chatRoom; +}; + +export const deleteChatRoomById = async (chatRoomId) => { + const chatRoom = await prisma.chatRoom.delete({ + where: { + id: chatRoomId, + }, + }); + + return chatRoom; +}; \ No newline at end of file diff --git a/src/routes/api.js b/src/routes/api.js index e72d2f9..85b6eaa 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -5,6 +5,7 @@ import tokensRouter from "./router/tokens.router.js"; import usersRouter from "./router/users.router.js"; import worksRouter from "./router/works.router.js"; import educationsRouter from "./router/educations.router.js"; +import chatRoomsRouter from "./router/chat-rooms.router.js"; const app = express(); @@ -14,5 +15,6 @@ app.use(tokensRouter); app.use(usersRouter); app.use(worksRouter); app.use(educationsRouter); +app.use(chatRoomsRouter) export default app; diff --git a/src/routes/router/chat-rooms.router.js b/src/routes/router/chat-rooms.router.js new file mode 100644 index 0000000..35b1f13 --- /dev/null +++ b/src/routes/router/chat-rooms.router.js @@ -0,0 +1,42 @@ +import express from "express"; +import { + editChatRoomByIdHandler, + getAllChatRoomsHandler, + getChatRoomByIdHandler, + insertChatRoomHandler, + removeChatRoomByIdHandler, +} from "../../controller/chat-rooms.controller.js"; +import { verifyAccessTokenHandler } from "../../middlewares/tokens.middleware.js"; + +const router = express.Router(); + +router.get( + "/chat-rooms", + verifyAccessTokenHandler, + getAllChatRoomsHandler +); +router.post( + "/chat-rooms", + verifyAccessTokenHandler, + insertChatRoomHandler +); + +router.get( + "/chat-rooms/:chatRoomId", + verifyAccessTokenHandler, + getChatRoomByIdHandler +); + +router.put( + "/chat-rooms/:chatRoomId", + verifyAccessTokenHandler, + editChatRoomByIdHandler +); + +router.delete( + "/chat-rooms/:chatRoomId", + verifyAccessTokenHandler, + removeChatRoomByIdHandler +); + +export default router; \ No newline at end of file diff --git a/src/schema/chat-rooms.schema.js b/src/schema/chat-rooms.schema.js new file mode 100644 index 0000000..56b4614 --- /dev/null +++ b/src/schema/chat-rooms.schema.js @@ -0,0 +1,13 @@ +import { z } from "zod"; + +export const InsertChatRoomSchema = z.object({ + type: z.enum(['Expert', 'AI']), + userId: z.string(), + expertId: z.string().optional(), +}); + +export const EditChatRoomSchema = z.object({ + type: z.enum(['Expert', 'AI']), + userId: z.string(), + expertId: z.string().optional(), +}); \ No newline at end of file diff --git a/src/service/chat-rooms.service.js b/src/service/chat-rooms.service.js new file mode 100644 index 0000000..d377ccc --- /dev/null +++ b/src/service/chat-rooms.service.js @@ -0,0 +1,52 @@ +import { + NotFoundError, + handleNotFoundError, +} from "../exceptions/client.exception.js"; +import { + createChatRoom, + deleteChatRoomById, + findChatRoomById, + findChatRooms, + findChatRoomsByUserId, + updateChatRoomById, +} from "../repository/chat-rooms.repository.js"; + +export const getAllChatRooms = async () => { + const chatRooms = await findChatRooms(); + + return chatRooms; +}; + +export const getAllChatRoomsByUserId = async (userId) => { + const chatRooms = await findChatRoomsByUserId(userId); + + return chatRooms; +}; + +export const insertChatRoom = async (newChatRoomData) => { + const newChatRoom = await createChatRoom(newChatRoomData); + + return newChatRoom; +}; + +export const getChatRoomById = async (chatRoomId, res) => { + const chatRoom = await findChatRoomById(chatRoomId); + + if (!chatRoom) { + handleNotFoundError("Chat Room not found", res); + } + + return chatRoom; +}; + +export const editChatRoomById = async (chatRoomId, newChatRoomData, res) => { + await getChatRoomById(chatRoomId, res); + const chatRoom = await updateChatRoomById(chatRoomId, newChatRoomData); + + return chatRoom; +}; + +export const removeChatRoomById = async (chatRoomId, res) => { + await getChatRoomById(chatRoomId, res); + await deleteChatRoomById(chatRoomId); +}; \ No newline at end of file From 09a3cf30129abf3a7a72eb106a5de105eeb82614 Mon Sep 17 00:00:00 2001 From: nurhudajoantama Date: Sun, 9 Jun 2024 10:51:27 +0700 Subject: [PATCH 4/5] feat(chat-ai): implement forward request for chat ai service --- .env.example | 3 + openapi.json | 138 --------------------------- src/controller/chat-ai.controller.js | 44 +++++++++ src/routes/api.js | 2 + src/routes/router/chat-ai.router.js | 19 ++++ src/schema/chat-ai.schema.js | 10 ++ src/service/chat-ai.service.js | 35 +++++++ 7 files changed, 113 insertions(+), 138 deletions(-) create mode 100644 src/controller/chat-ai.controller.js create mode 100644 src/routes/router/chat-ai.router.js create mode 100644 src/schema/chat-ai.schema.js create mode 100644 src/service/chat-ai.service.js diff --git a/.env.example b/.env.example index 9494ba0..95ed38d 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,6 @@ JWT_REFRESH_TOKEN_SECRET='' GCLOUD_PROJECT_ID="your-gcloud-project-id" GCLOUD_BUCKET_NAME="your-bucket-name" + +GENERATIVE_AI_SERVICE_URL="" +SUGGESTION_AI_SERVICE_URL="" diff --git a/openapi.json b/openapi.json index 2ef40c9..5380d92 100644 --- a/openapi.json +++ b/openapi.json @@ -2952,144 +2952,6 @@ } ] } - }, - "/chats/ai/{id}/send":{ - "post": { - "tags": ["Chat"], - "summary": "Send message to AI chat by ID", - "description": "Send message to AI chat by ID. Requires valid access token", - "parameters": [ - { - "$ref": "#/components/parameters/ChatRoomId" - } - ], - "requestBody": { - "description": "Chat room object that needs to be updated", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateAllFieldChatRoom" - }, - "examples": { - "Update chat room": { - "description": "Example of a chat room update", - "value": { - "type": "Expert", - "userId": "d969ee14-3362-46cd-ace9-af4d7b131733", - "expertId": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Successfully update the chat room", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessSignupOrRegisUser" - }, - "examples": { - "Chat Room updated": { - "description": "Successfully updated the chat room", - "value": { - "status": "success", - "message": "Chat room updated successfully", - "data": { - "id": "d969ee14-3362-46cd-ace9-af4d7b131733" - } - } - } - } - } - } - }, - "400": { - "description": "Failed to update chat room due to validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FailValidationSignupOrRegister" - }, - "examples": { - "name empty": { - "description": "Failed to update chat room because name field is empty", - "value": { - "status": "fail", - "message": "Validation error", - "errors": [ - { - "field": "name", - "message": "name is required" - } - ] - } - } - } - } - } - }, - "404": { - "$ref": "#/components/responses/ErrorChatRoomNotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "delete": { - "tags": ["Chat Room"], - "summary": "Delete a chat room by id", - "description": "Delete a specific chat room by its ID", - "parameters": [ - { - "$ref": "#/components/parameters/ChatRoomId" - } - ], - "responses": { - "200": { - "description": "Successfully deleted the chat room", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessSignupOrRegisUser" - }, - "examples": { - "Chat Room deleted": { - "description": "Successfully deleted the chat room", - "value": { - "status": "success", - "message": "Chat room deleted successfully", - "data": { - "id": "d969ee14-3362-46cd-ace9-af4d7b131733" - } - } - } - } - } - } - }, - "404": { - "$ref": "#/components/responses/ErrorChatRoomNotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } } }, "components": { diff --git a/src/controller/chat-ai.controller.js b/src/controller/chat-ai.controller.js new file mode 100644 index 0000000..8f5246f --- /dev/null +++ b/src/controller/chat-ai.controller.js @@ -0,0 +1,44 @@ +import { handleServerError } from "../exceptions/server.exception.js"; +import { handleZodError } from "../exceptions/zod.exception.js"; +import { GenerativeAiInput, SugesstionQuestionInput } from "../schema/chat-ai.schema.js"; +import { getGenerativeAi, getSugesstionQuestion } from "../service/chat-ai.service.js"; + +export const getGenerativeAiHandler = async (req, res) => { + try { + const validateData = GenerativeAiInput.parse(req.body); + + const data = await getGenerativeAi(validateData); + + res.status(200).send({ + status: "success", + message: "Successfully get generative AI", + data, + }); + } catch (error) { + try { + handleZodError(error, res); + } catch (err) { + handleServerError(err, res); + } + } +}; + +export const getSugesstionQuestionHandler = async (req, res) => { + try { + const validateData = SugesstionQuestionInput.parse(req.body); + + const data = await getSugesstionQuestion(validateData); + + res.status(200).send({ + status: "success", + message: "Successfully get sugesstion question", + data, + }); + } catch (error) { + try { + handleZodError(error, res); + } catch (err) { + handleServerError(err, res); + } + } +} \ No newline at end of file diff --git a/src/routes/api.js b/src/routes/api.js index 85b6eaa..98609c0 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -6,6 +6,7 @@ import usersRouter from "./router/users.router.js"; import worksRouter from "./router/works.router.js"; import educationsRouter from "./router/educations.router.js"; import chatRoomsRouter from "./router/chat-rooms.router.js"; +import chatAiRouter from "./router/chat-ai.router.js"; const app = express(); @@ -16,5 +17,6 @@ app.use(usersRouter); app.use(worksRouter); app.use(educationsRouter); app.use(chatRoomsRouter) +app.use(chatAiRouter) export default app; diff --git a/src/routes/router/chat-ai.router.js b/src/routes/router/chat-ai.router.js new file mode 100644 index 0000000..2556db7 --- /dev/null +++ b/src/routes/router/chat-ai.router.js @@ -0,0 +1,19 @@ +import express from "express"; +import { verifyAccessTokenHandler } from "../../middlewares/tokens.middleware.js"; +import { getGenerativeAiHandler, getSugesstionQuestionHandler } from "../../controller/chat-ai.controller.js"; + +const router = express.Router(); + +router.post( + "/chats/ai/predict", + verifyAccessTokenHandler, + getGenerativeAiHandler +); + +router.post( + "/chats/ai/suggestions", + verifyAccessTokenHandler, + getSugesstionQuestionHandler +); + +export default router; \ No newline at end of file diff --git a/src/schema/chat-ai.schema.js b/src/schema/chat-ai.schema.js new file mode 100644 index 0000000..a0e7fdd --- /dev/null +++ b/src/schema/chat-ai.schema.js @@ -0,0 +1,10 @@ +import { z } from "zod"; + +export const GenerativeAiInput = z.object({ + question: z.string(), +}); + +export const SugesstionQuestionInput = z.object({ + user_input: z.string(), + total_questions: z.number() +}); \ No newline at end of file diff --git a/src/service/chat-ai.service.js b/src/service/chat-ai.service.js new file mode 100644 index 0000000..e26b79c --- /dev/null +++ b/src/service/chat-ai.service.js @@ -0,0 +1,35 @@ +const GENERATIVE_AI_SERVICE_URL = process.env.GENERATIVE_AI_SERVICE_URL || ""; +const SUGGESTION_AI_SERVICE_URL = process.env.SUGGESTION_AI_SERVICE_URL || ""; + +export const getGenerativeAi = async ({question}) => { + const response = await fetch(GENERATIVE_AI_SERVICE_URL , { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + question, + }), + }); + + const data = await response.json(); + + return data; +} + +export const getSugesstionQuestion = async ({user_input, total_questions}) => { + const response = await fetch(SUGGESTION_AI_SERVICE_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + user_input, + total_questions, + }), + }); + + const data = await response.json(); + + return data; +} From a2ae16ade4b347bd086dc6e7fda84efcf9c6924e Mon Sep 17 00:00:00 2001 From: nurhudajoantama Date: Sun, 9 Jun 2024 13:05:26 +0700 Subject: [PATCH 5/5] ci: add ml url env variable to github workflow --- .github/workflows/google-cloudrun-docker.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/google-cloudrun-docker.yml b/.github/workflows/google-cloudrun-docker.yml index ec67e0e..80e40ce 100644 --- a/.github/workflows/google-cloudrun-docker.yml +++ b/.github/workflows/google-cloudrun-docker.yml @@ -88,6 +88,8 @@ jobs: echo "JWT_REFRESH_TOKEN_SECRET=${{ secrets.JWT_REFRESH_TOKEN_SECRET }}" >> .env.production echo "GCLOUD_PROJECT_ID=${{ secrets.GCLOUD_PROJECT_ID }}" >> .env.production echo "GCLOUD_BUCKET_NAME=${{ secrets.GCLOUD_BUCKET_NAME }}" >> .env.production + echo "GENERATIVE_AI_SERVICE_URL=${{ secrets.GENERATIVE_AI_SERVICE_URL }}" >> .env.production + echo "SUGGESTION_AI_SERVICE_URL=${{ secrets.SUGGESTION_AI_SERVICE_URL }}" >> .env.production elif [[ "${GITHUB_REF##*/}" == "staging" ]]; then echo "NODE_ENV=staging" >> $GITHUB_ENV echo "DOCKER_TAG=staging-${{ github.sha }}" >> $GITHUB_ENV @@ -99,6 +101,8 @@ jobs: echo "JWT_REFRESH_TOKEN_SECRET=${{ secrets.JWT_REFRESH_TOKEN_SECRET }}" >> .env.staging echo "GCLOUD_PROJECT_ID=${{ secrets.GCLOUD_PROJECT_ID }}" >> .env.staging echo "GCLOUD_BUCKET_NAME=${{ secrets.GCLOUD_BUCKET_NAME }}" >> .env.staging + echo "GENERATIVE_AI_SERVICE_URL=${{ secrets.GENERATIVE_AI_SERVICE_URL }}" >> .env.staging + echo "SUGGESTION_AI_SERVICE_URL=${{ secrets.SUGGESTION_AI_SERVICE_URL }}" >> .env.staging elif [[ "${GITHUB_REF##*/}" == "feat/setup-node" ]]; then echo "NODE_ENV=dev" >> $GITHUB_ENV echo "DOCKER_TAG=dev-${{ github.sha }}" >> $GITHUB_ENV @@ -110,6 +114,8 @@ jobs: echo "JWT_REFRESH_TOKEN_SECRET=${{ secrets.JWT_REFRESH_TOKEN_SECRET }}" >> .env.dev echo "GCLOUD_PROJECT_ID=${{ secrets.GCLOUD_PROJECT_ID }}" >> .env.dev echo "GCLOUD_BUCKET_NAME=${{ secrets.GCLOUD_BUCKET_NAME }}" >> .env.dev + echo "GENERATIVE_AI_SERVICE_URL=${{ secrets.GENERATIVE_AI_SERVICE_URL }}" >> .env.dev + echo "SUGGESTION_AI_SERVICE_URL=${{ secrets.SUGGESTION_AI_SERVICE_URL }}" >> .env.dev else echo "NODE_ENV=dev" >> $GITHUB_ENV echo "DOCKER_TAG=dev-${{ github.sha }}" >> $GITHUB_ENV @@ -121,6 +127,8 @@ jobs: echo "JWT_REFRESH_TOKEN_SECRET=${{ secrets.JWT_REFRESH_TOKEN_SECRET }}" >> .env.dev echo "GCLOUD_PROJECT_ID=${{ secrets.GCLOUD_PROJECT_ID }}" >> .env.dev echo "GCLOUD_BUCKET_NAME=${{ secrets.GCLOUD_BUCKET_NAME }}" >> .env.dev + echo "GENERATIVE_AI_SERVICE_URL=${{ secrets.GENERATIVE_AI_SERVICE_URL }}" >> .env.dev + echo "SUGGESTION_AI_SERVICE_URL=${{ secrets.SUGGESTION_AI_SERVICE_URL }}" >> .env.dev fi - name: Clear Docker cache