From 5987b1c35375907e065a2a7d2e55f821a9e95faf Mon Sep 17 00:00:00 2001 From: Abdallah Elhadad Date: Wed, 15 May 2024 05:59:47 +0300 Subject: [PATCH 1/2] refactor(all): majorrr --- .eslintrc.json | 6 +- .prettierrc | 2 +- .../20240409163927_create_users_table.ts | 15 +- .../20240412000348_add_workspace_table.ts | 13 +- .../20240412002911_add_channel_table.ts | 12 +- .../20240412002952_add_messages_table.ts | 12 +- .../20240412003023_add_invites_table.ts | 15 +- .../20240412003051_add_notification_table.ts | 2 +- db/seeds/01-userSeeding.ts | 48 ++++- db/seeds/02-workspaceSeeding.ts | 40 ++++- db/seeds/03-channelSeeding.ts | 63 ++++++- db/seeds/06-messageSeeding.ts | 40 ++--- docker-compose.yaml | 12 +- global.d.ts | 6 + package-lock.json | 14 +- package.json | 3 +- src/api-docs/openAPIDocumentGenerator.ts | 15 +- src/api-docs/openAPIResponseBuilders.ts | 6 +- src/api/auth/authApi.ts | 40 +++++ src/api/auth/authController.ts | 49 +++-- src/api/auth/authRouter.ts | 47 +---- src/api/auth/authService.ts | 128 ++++++-------- src/api/channels/channelRepository.ts | 11 +- src/api/channels/channelRouter.ts | 19 +- .../__tests__/coworkersRepository.test.ts | 14 +- src/api/coworkers/coworkersRepository.ts | 38 +++- src/api/coworkers/coworkersService.ts | 13 ++ src/api/files/filesController.ts | 20 ++- src/api/files/filesRepository.ts | 10 +- .../__tests__/00_healthCheckRouter.test.ts | 4 +- src/api/healthCheck/healthCheckRouter.ts | 4 +- src/api/invites/invitesController.ts | 10 +- src/api/invites/invitesRepository.ts | 32 +++- src/api/members/memberRepository.ts | 33 +++- src/api/messages/messageController.ts | 3 +- src/api/messages/messageModel.ts | 6 + src/api/messages/messageRepository.ts | 21 ++- src/api/messages/messageRouter.ts | 14 +- .../__tests__/notificationsRepository.test.ts | 5 +- .../notifications/notificationsRepository.ts | 48 ++++- src/api/notifications/notificationsRoutes.ts | 24 ++- src/api/reactions/reactionRepository.ts | 5 +- src/api/reactions/reactionsRouter.ts | 26 ++- .../__tests__/threadsRepository.test.ts | 10 +- src/api/threads/threadsRepository.ts | 27 ++- src/api/user/__tests__/userController.test.ts | 8 + src/api/user/__tests__/userRepository.ts | 21 +++ src/api/user/userApi.ts | 86 +++++++++ src/api/user/userController.ts | 53 ++++-- src/api/user/userModel.ts | 1 + src/api/user/userRepository.ts | 51 ++++-- src/api/user/userRouter.ts | 86 ++++----- src/api/user/userService.ts | 167 ++++++------------ .../__tests__/workspaceRepository.test.ts | 8 +- src/api/workspace/workspaceApi.ts | 109 ++++++++++++ src/api/workspace/workspaceController.ts | 4 - src/api/workspace/workspaceModel.ts | 2 + src/api/workspace/workspaceRepository.ts | 42 +++-- src/api/workspace/workspaceRouter.ts | 129 +++----------- src/api/workspace/workspaceService.ts | 25 +++ src/common/__tests__/entityFactory.ts | 63 ++++--- src/common/__tests__/errorHandler.test.ts | 8 +- src/common/__tests__/mocks.ts | 1 + src/common/__tests__/requestLogger.test.ts | 8 +- src/common/middleware/errorHandler.ts | 50 +++++- src/common/middleware/requestLogger.ts | 25 ++- src/common/middleware/trxHandler.ts | 7 + src/common/models/serviceResponse.ts | 7 +- src/common/utils/httpHandlers.ts | 108 +++++++++-- src/server.ts | 11 +- src/sockets/__tests__/basicSocket.test.ts | 5 +- src/sockets/__tests__/setupSocketTesting.ts | 13 +- src/sockets/socket.ts | 13 +- src/sockets/sockets.types.ts | 6 +- 74 files changed, 1425 insertions(+), 677 deletions(-) create mode 100644 src/api/auth/authApi.ts create mode 100644 src/api/coworkers/coworkersService.ts create mode 100644 src/api/user/__tests__/userController.test.ts create mode 100644 src/api/user/__tests__/userRepository.ts create mode 100644 src/api/user/userApi.ts create mode 100644 src/api/workspace/workspaceApi.ts create mode 100644 src/api/workspace/workspaceService.ts create mode 100644 src/common/middleware/trxHandler.ts diff --git a/.eslintrc.json b/.eslintrc.json index 3c07b8e..7f1932b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,7 +9,11 @@ "sourceType": "module" }, "plugins": ["@typescript-eslint", "simple-import-sort", "prettier"], - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], "rules": { "@typescript-eslint/no-explicit-any": "off", "simple-import-sort/imports": "error", diff --git a/.prettierrc b/.prettierrc index a44e7a1..9400503 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ { "trailingComma": "es5", "tabWidth": 4, - "printWidth": 120, + "printWidth": 80, "singleQuote": true, "semi": true, "useTabs": true diff --git a/db/migrations/20240409163927_create_users_table.ts b/db/migrations/20240409163927_create_users_table.ts index a648ab2..e1eca07 100644 --- a/db/migrations/20240409163927_create_users_table.ts +++ b/db/migrations/20240409163927_create_users_table.ts @@ -25,8 +25,19 @@ export async function up(knex: Knex): Promise { table.string('password').notNullable(); table.string('bio'); table.string('avatarUrl'); - table.enum('status', ['online', 'offline', 'away']).defaultTo('offline'); - table.timestamps(true, true, true); + table + .enum('status', ['online', 'offline', 'away']) + .defaultTo('offline'); + table + .dateTime('createdAt') + .notNullable() + .defaultTo(knex.raw('CURRENT_TIMESTAMP')); + table + .dateTime('updatedAt') + .notNullable() + .defaultTo( + knex.raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP') + ); }); } diff --git a/db/migrations/20240412000348_add_workspace_table.ts b/db/migrations/20240412000348_add_workspace_table.ts index 8968dc1..0837092 100644 --- a/db/migrations/20240412000348_add_workspace_table.ts +++ b/db/migrations/20240412000348_add_workspace_table.ts @@ -8,7 +8,18 @@ export async function up(knex: Knex): Promise { table.string('avatarUrl'); table.integer('ownerId').unsigned(); table.foreign('ownerId').references('id').inTable('users'); - table.timestamps(true, true, true); + + table + .dateTime('createdAt') + .notNullable() + .defaultTo(knex.raw('CURRENT_TIMESTAMP')); + + table + .dateTime('updatedAt') + .notNullable() + .defaultTo( + knex.raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP') + ); }); } diff --git a/db/migrations/20240412002911_add_channel_table.ts b/db/migrations/20240412002911_add_channel_table.ts index 72d4af9..a1a22c7 100644 --- a/db/migrations/20240412002911_add_channel_table.ts +++ b/db/migrations/20240412002911_add_channel_table.ts @@ -26,10 +26,18 @@ export async function up(knex: Knex): Promise { table.integer('creatorId').unsigned(); table.integer('workspaceId').unsigned(); - //TODO: add this to another migrationFile table.foreign('creatorId').references('id').inTable('users'); table.foreign('workspaceId').references('id').inTable('workspaces'); - table.timestamps(true, true, true); + table + .dateTime('createdAt') + .notNullable() + .defaultTo(knex.raw('CURRENT_TIMESTAMP')); + table + .dateTime('updatedAt') + .notNullable() + .defaultTo( + knex.raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP') + ); }); } diff --git a/db/migrations/20240412002952_add_messages_table.ts b/db/migrations/20240412002952_add_messages_table.ts index 53ff3c0..f42776d 100644 --- a/db/migrations/20240412002952_add_messages_table.ts +++ b/db/migrations/20240412002952_add_messages_table.ts @@ -29,8 +29,16 @@ export async function up(knex: Knex): Promise { table.foreign('senderId').references('id').inTable('users'); table.foreign('channelId').references('id').inTable('channels'); table.foreign('parentMessageId').references('id').inTable('messages'); - - table.timestamps(true, true, true); + table + .dateTime('createdAt') + .notNullable() + .defaultTo(knex.raw('CURRENT_TIMESTAMP')); + table + .dateTime('updatedAt') + .notNullable() + .defaultTo( + knex.raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP') + ); }); } diff --git a/db/migrations/20240412003023_add_invites_table.ts b/db/migrations/20240412003023_add_invites_table.ts index 4ca6d9f..f5864a3 100644 --- a/db/migrations/20240412003023_add_invites_table.ts +++ b/db/migrations/20240412003023_add_invites_table.ts @@ -23,14 +23,25 @@ export async function up(knex: Knex): Promise { table.integer('senderId').unsigned(); table.integer('inviteeId').unsigned(); - table.timestamps(true, true, true); table.timestamp('expiresAt').notNullable(); - table.enum('status', ['pending', 'accepted', 'cancelled']).defaultTo('pending'); + table + .enum('status', ['pending', 'accepted', 'cancelled']) + .defaultTo('pending'); // TODO: add this to another migrationFile table.foreign('workspaceId').references('id').inTable('workspaces'); table.foreign('inviteeId').references('id').inTable('users'); table.foreign('senderId').references('id').inTable('users'); + table + .dateTime('createdAt') + .notNullable() + .defaultTo(knex.raw('CURRENT_TIMESTAMP')); + table + .dateTime('updatedAt') + .notNullable() + .defaultTo( + knex.raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP') + ); }); } diff --git a/db/migrations/20240412003051_add_notification_table.ts b/db/migrations/20240412003051_add_notification_table.ts index 8767e0d..ebf4084 100644 --- a/db/migrations/20240412003051_add_notification_table.ts +++ b/db/migrations/20240412003051_add_notification_table.ts @@ -14,7 +14,7 @@ import type { Knex } from 'knex'; notifications.message_id < messages.id */ -//TODO: fix related repo and stuff. +//TODO: fix related repo and stuff, also add URL. export async function up(knex: Knex): Promise { await knex.schema.createTable('notifications', (table) => { table.increments('id').primary(); diff --git a/db/seeds/01-userSeeding.ts b/db/seeds/01-userSeeding.ts index 1c6aaff..b6cbfb0 100644 --- a/db/seeds/01-userSeeding.ts +++ b/db/seeds/01-userSeeding.ts @@ -8,11 +8,47 @@ export async function seed(knex: Knex): Promise { // Inserts seed entries await Promise.all([ - EntityFactory.createUser(1, 'user 1', 'email1@gmail.com', 'Test User 1', 'password'), - EntityFactory.createUser(2, 'user 2', 'email2@gmail.com', 'Test User 2', 'password'), - EntityFactory.createUser(3, 'user 3', 'email3@gmail.com', 'Test User 3', 'password'), - EntityFactory.createUser(4, 'user 4', 'email4@gmail.com', 'Test User 4', 'password'), - EntityFactory.createUser(5, 'user 5', 'email5@gmail.com', 'Test User 5', 'password'), - EntityFactory.createUser(6, 'user 6', 'email6@gmail.com', 'Test User 6', 'password'), + EntityFactory.createUser( + 1, + 'user 1', + 'email1@gmail.com', + 'Test User 1', + 'password' + ), + EntityFactory.createUser( + 2, + 'user 2', + 'email2@gmail.com', + 'Test User 2', + 'password' + ), + EntityFactory.createUser( + 3, + 'user 3', + 'email3@gmail.com', + 'Test User 3', + 'password' + ), + EntityFactory.createUser( + 4, + 'user 4', + 'email4@gmail.com', + 'Test User 4', + 'password' + ), + EntityFactory.createUser( + 5, + 'user 5', + 'email5@gmail.com', + 'Test User 5', + 'password' + ), + EntityFactory.createUser( + 6, + 'user 6', + 'email6@gmail.com', + 'Test User 6', + 'password' + ), ]); } diff --git a/db/seeds/02-workspaceSeeding.ts b/db/seeds/02-workspaceSeeding.ts index 13f395b..4ef9774 100644 --- a/db/seeds/02-workspaceSeeding.ts +++ b/db/seeds/02-workspaceSeeding.ts @@ -15,10 +15,40 @@ export async function seed(knex: Knex): Promise { * */ await Promise.all([ - EntityFactory.createWorkspace(1, 1, 'workspace 1', 'Descrcription', 'url'), - EntityFactory.createWorkspace(2, 1, 'workspace 2', 'Descrcription', 'url'), - EntityFactory.createWorkspace(3, 6, 'workspace 3', 'Descrcription', 'url'), - EntityFactory.createWorkspace(4, 2, 'workspace 4', 'Descrcription', 'url'), - EntityFactory.createWorkspace(5, 5, 'workspace 5', 'Descrcription', 'url'), + EntityFactory.createWorkspace( + 1, + 1, + 'workspace 1', + 'Descrcription', + 'url' + ), + EntityFactory.createWorkspace( + 2, + 1, + 'workspace 2', + 'Descrcription', + 'url' + ), + EntityFactory.createWorkspace( + 3, + 6, + 'workspace 3', + 'Descrcription', + 'url' + ), + EntityFactory.createWorkspace( + 4, + 2, + 'workspace 4', + 'Descrcription', + 'url' + ), + EntityFactory.createWorkspace( + 5, + 5, + 'workspace 5', + 'Descrcription', + 'url' + ), ]); } diff --git a/db/seeds/03-channelSeeding.ts b/db/seeds/03-channelSeeding.ts index d614bf5..df85ffc 100644 --- a/db/seeds/03-channelSeeding.ts +++ b/db/seeds/03-channelSeeding.ts @@ -16,12 +16,61 @@ export async function seed(knex: Knex): Promise { * */ await Promise.all([ - EntityFactory.createChannel(1, 1, 1, 'channel 1', 'Descrcription', 'public'), - EntityFactory.createChannel(2, 1, 1, 'channel 2', 'Descrcription', 'public'), - EntityFactory.createChannel(3, 1, 2, 'channel 3', 'Descrcription', 'public'), - EntityFactory.createChannel(4, 2, 3, 'channel 4', 'Descrcription', 'public'), - EntityFactory.createChannel(5, 3, 4, 'channel 5', 'Descrcription', 'public'), - EntityFactory.createChannel(6, 3, 4, 'channel 6', 'Descrcription', 'public'), - EntityFactory.createChannel(7, 5, 5, 'channel 7', 'Descrcription', 'public'), + EntityFactory.createChannel( + 1, + 1, + 1, + 'channel 1', + 'Descrcription', + 'public' + ), + EntityFactory.createChannel( + 2, + 1, + 1, + 'channel 2', + 'Descrcription', + 'public' + ), + EntityFactory.createChannel( + 3, + 1, + 2, + 'channel 3', + 'Descrcription', + 'public' + ), + EntityFactory.createChannel( + 4, + 2, + 3, + 'channel 4', + 'Descrcription', + 'public' + ), + EntityFactory.createChannel( + 5, + 3, + 4, + 'channel 5', + 'Descrcription', + 'public' + ), + EntityFactory.createChannel( + 6, + 3, + 4, + 'channel 6', + 'Descrcription', + 'public' + ), + EntityFactory.createChannel( + 7, + 5, + 5, + 'channel 7', + 'Descrcription', + 'public' + ), ]); } diff --git a/db/seeds/06-messageSeeding.ts b/db/seeds/06-messageSeeding.ts index 4aef597..a0b6447 100644 --- a/db/seeds/06-messageSeeding.ts +++ b/db/seeds/06-messageSeeding.ts @@ -18,12 +18,12 @@ export async function seed(knex: Knex): Promise { * */ // workspace 1 channel 1(1,2,3,4) - EntityFactory.createMessage(1, 1, 1, 1, null, ''), - EntityFactory.createMessage(2, 2, 1, 1, null, ''), - EntityFactory.createMessage(3, 3, 1, 1, null, ''), - EntityFactory.createMessage(4, 4, 1, 1, 1, ''), - EntityFactory.createMessage(5, 1, 1, 1, 1, ''), - EntityFactory.createMessage(6, 3, 1, 1, 1, ''), + EntityFactory.createMessage(1, 1, 1, null, ''), + EntityFactory.createMessage(2, 2, 1, null, ''), + EntityFactory.createMessage(3, 3, 1, null, ''), + EntityFactory.createMessage(4, 4, 1, 1, ''), + EntityFactory.createMessage(5, 1, 1, 1, ''), + EntityFactory.createMessage(6, 3, 1, 1, ''), /* * 7[1]: @@ -34,11 +34,11 @@ export async function seed(knex: Knex): Promise { * */ // workspace 1 channel 2(1,2) - EntityFactory.createMessage(7, 1, 2, 1, null, ''), - EntityFactory.createMessage(8, 4, 2, 1, null, ''), - EntityFactory.createMessage(9, 1, 2, 1, null, ''), - EntityFactory.createMessage(10, 4, 2, 1, null, ''), - EntityFactory.createMessage(11, 4, 2, 1, 7, ''), + EntityFactory.createMessage(7, 1, 2, null, ''), + EntityFactory.createMessage(8, 4, 2, null, ''), + EntityFactory.createMessage(9, 1, 2, null, ''), + EntityFactory.createMessage(10, 4, 2, null, ''), + EntityFactory.createMessage(11, 4, 2, 7, ''), /* * 12[1]: @@ -47,9 +47,9 @@ export async function seed(knex: Knex): Promise { * */ // workspace 2 channel 3(1,5) - EntityFactory.createMessage(12, 1, 3, 2, null, ''), - EntityFactory.createMessage(13, 5, 3, 2, 12, ''), - EntityFactory.createMessage(14, 1, 3, 2, 12, ''), + EntityFactory.createMessage(12, 1, 3, null, ''), + EntityFactory.createMessage(13, 5, 3, 12, ''), + EntityFactory.createMessage(14, 1, 3, 12, ''), /* * 15[2]: @@ -58,9 +58,9 @@ export async function seed(knex: Knex): Promise { * */ // workspace 3 channel 4(2,6) - EntityFactory.createMessage(15, 2, 4, 3, null, ''), - EntityFactory.createMessage(16, 6, 4, 3, null, ''), - EntityFactory.createMessage(17, 2, 4, 3, null, ''), + EntityFactory.createMessage(15, 2, 4, null, ''), + EntityFactory.createMessage(16, 6, 4, null, ''), + EntityFactory.createMessage(17, 2, 4, null, ''), /* * 18[2]: @@ -68,14 +68,14 @@ export async function seed(knex: Knex): Promise { * */ // workspace 4 channel 5(2,3) - EntityFactory.createMessage(18, 2, 5, 4, null, ''), - EntityFactory.createMessage(19, 3, 5, 4, null, ''), + EntityFactory.createMessage(18, 2, 5, null, ''), + EntityFactory.createMessage(19, 3, 5, null, ''), /* * 20[3]: * */ // workspace 4 channel 6(3) - EntityFactory.createMessage(20, 3, 6, 4, 19, '4'), + EntityFactory.createMessage(20, 3, 6, 19, '4'), ]); } diff --git a/docker-compose.yaml b/docker-compose.yaml index 408a74c..bc9ccf7 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -14,7 +14,17 @@ services: networks: - backend_network healthcheck: - test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-u', 'root', '-p$$MYSQL_ROOT_PASSWORD'] + test: + [ + 'CMD', + 'mysqladmin', + 'ping', + '-h', + 'localhost', + '-u', + 'root', + '-p$$MYSQL_ROOT_PASSWORD', + ] timeout: 20s retries: 10 redis: diff --git a/global.d.ts b/global.d.ts index da798e3..bddecdd 100644 --- a/global.d.ts +++ b/global.d.ts @@ -6,3 +6,9 @@ declare global { // eslint-disable-next-line no-var var io: Server; } + +declare module 'express' { + export interface Response { + trx?: Transaction; + } +} diff --git a/package-lock.json b/package-lock.json index 71676c6..5ffd5da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,8 @@ "socket.io": "^4.7.5", "socket.io-client": "^4.7.5", "swagger-ui-express": "^5.0.0", - "zod": "^3.22.4" + "zod": "^3.22.4", + "zod-validation-error": "^3.3.0" }, "devDependencies": { "@commitlint/cli": "^19.2.1", @@ -11952,6 +11953,17 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-validation-error": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.3.0.tgz", + "integrity": "sha512-Syib9oumw1NTqEv4LT0e6U83Td9aVRk9iTXPUQr1otyV1PuXQKOvOwhMNqZIq5hluzHP2pMgnOmHEo7kPdI2mw==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.18.0" + } } } } diff --git a/package.json b/package.json index 796638b..075dc92 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "socket.io": "^4.7.5", "socket.io-client": "^4.7.5", "swagger-ui-express": "^5.0.0", - "zod": "^3.22.4" + "zod": "^3.22.4", + "zod-validation-error": "^3.3.0" }, "devDependencies": { "@commitlint/cli": "^19.2.1", diff --git a/src/api-docs/openAPIDocumentGenerator.ts b/src/api-docs/openAPIDocumentGenerator.ts index 0b63be1..ee6fc1f 100644 --- a/src/api-docs/openAPIDocumentGenerator.ts +++ b/src/api-docs/openAPIDocumentGenerator.ts @@ -1,17 +1,18 @@ -import { OpenApiGeneratorV3, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import { + OpenApiGeneratorV3, + OpenAPIRegistry, +} from '@asteasolutions/zod-to-openapi'; import { channelRegistery } from '@/api/channels/channelRouter'; -import { healthCheckRegistry } from '@/api/healthCheck/healthCheckRouter'; -import { messageRegistery } from '@/api/messages/messageRouter'; import { filesRegistry } from '@/api/files/filesRoutes'; -// import { channelRegistery } from '@/api/channels/channelRouter'; import { healthCheckRegistry } from '@/api/healthCheck/healthCheckRouter'; +import { messageRegistery } from '@/api/messages/messageRouter'; import { notificationsRegistry } from '@/api/notifications/notificationsRoutes'; import { reactionsRegistry } from '@/api/reactions/reactionsRouter'; -import { userRegistry } from '@/api/user/userRouter'; -import { workspaceRegistry } from '@/api/workspace/workspaceRouter'; +import { userRegistry } from '@/api/user/userApi'; +import { workspaceRegistry } from '@/api/workspace/workspaceApi'; -import { authRegistry } from '../api/auth/authRouter'; +import { authRegistry } from '../api/auth/authApi'; export function generateOpenAPIDocument() { const registry = new OpenAPIRegistry([ diff --git a/src/api-docs/openAPIResponseBuilders.ts b/src/api-docs/openAPIResponseBuilders.ts index c27780f..3edc835 100644 --- a/src/api-docs/openAPIResponseBuilders.ts +++ b/src/api-docs/openAPIResponseBuilders.ts @@ -3,7 +3,11 @@ import { z } from 'zod'; import { ServiceResponseSchema } from '@/common/models/serviceResponse'; -export function createApiResponse(schema: z.ZodTypeAny, description: string, statusCode = StatusCodes.OK) { +export function createApiResponse( + schema: z.ZodTypeAny, + description: string, + statusCode = StatusCodes.OK +) { return { [statusCode]: { description, diff --git a/src/api/auth/authApi.ts b/src/api/auth/authApi.ts new file mode 100644 index 0000000..6ffa061 --- /dev/null +++ b/src/api/auth/authApi.ts @@ -0,0 +1,40 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; + +import { UserSchema } from '@/api/user/userModel'; +import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; + +import { loginSchema, signupSchema, tokenSchema } from './authModel'; + +export const authRegistry = new OpenAPIRegistry(); + +authRegistry.registerPath({ + method: 'post', + path: '/auth/signup', + tags: ['Auth'], + request: { + body: { + content: { + 'application/json': { + schema: signupSchema.shape.body, + }, + }, + }, + }, + responses: createApiResponse(UserSchema, 'Success'), +}); + +authRegistry.registerPath({ + method: 'post', + path: '/auth/login', + tags: ['Auth'], + request: { + body: { + content: { + 'application/json': { + schema: loginSchema.shape.body, + }, + }, + }, + }, + responses: createApiResponse(tokenSchema, 'Success'), +}); diff --git a/src/api/auth/authController.ts b/src/api/auth/authController.ts index 476cfdf..b905892 100644 --- a/src/api/auth/authController.ts +++ b/src/api/auth/authController.ts @@ -1,13 +1,16 @@ import { NextFunction, Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; -import { handleServiceResponse } from '../../common/utils/httpHandlers'; +import { + asyncHandler, + handleServiceResponse, +} from '../../common/utils/httpHandlers'; import { CreateUser } from '../user/userModel'; import authService from './authService'; const AuthController = { - signup: async (req: Request, res: Response) => { + signup: asyncHandler(async (req: Request, res: Response) => { const { email, username, name, password } = req.body; - const createUserPayload: CreateUser = { email, username, @@ -15,24 +18,34 @@ const AuthController = { password, }; - const serviceResponse = await authService.signup(createUserPayload); - handleServiceResponse(serviceResponse, res); - }, - login: async (req: Request, res: Response) => { + const data = await authService.signup(createUserPayload, res.trx); + handleServiceResponse(res, data, 'OK'); + }), + login: asyncHandler(async (req: Request, res: Response) => { const { email, password } = req.body; - const serviceResponse = await authService.login(email, password); - handleServiceResponse(serviceResponse, res); - }, - authenticate: async (req: Request, res: Response, next: NextFunction) => { - const response = await authService.authenticate(req.headers.authorization || ''); - if (response.success) { - res.locals.user = response.responseObject; - return next(); + const tokenAndUser = await authService.login(email, password, res.trx); + handleServiceResponse(res, tokenAndUser, 'OK'); + }), + authenticate: asyncHandler( + async (req: Request, res: Response, next: NextFunction) => { + const user = await authService.authenticate( + req.headers.authorization || '', + res.trx + ); + if (user) { + res.locals.user = user; + next(); + return; + } + handleServiceResponse( + res, + null, + 'TOKENN', + StatusCodes.UNAUTHORIZED + ); } - - handleServiceResponse(response, res); - }, + ), }; export default AuthController; diff --git a/src/api/auth/authRouter.ts b/src/api/auth/authRouter.ts index bb4b4c3..e8fac1f 100644 --- a/src/api/auth/authRouter.ts +++ b/src/api/auth/authRouter.ts @@ -1,52 +1,17 @@ -import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; import express, { Router } from 'express'; -import { UserSchema } from '@/api/user/userModel'; -import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; - import { validateRequest } from '../../common/utils/httpHandlers'; import AuthController from './authController'; -import { loginSchema, signupSchema, tokenSchema } from './authModel'; - -export const authRegistry = new OpenAPIRegistry(); +import { loginSchema, signupSchema } from './authModel'; export const authRouter: Router = (() => { const router = express.Router(); - authRegistry.registerPath({ - method: 'post', - path: '/auth/signup', - tags: ['Auth'], - request: { - body: { - content: { - 'application/json': { - schema: signupSchema.shape.body, - }, - }, - }, - }, - responses: createApiResponse(UserSchema, 'Success'), - }); - - router.post('/signup', validateRequest(signupSchema), AuthController.signup); - - authRegistry.registerPath({ - method: 'post', - path: '/auth/login', - tags: ['Auth'], - request: { - body: { - content: { - 'application/json': { - schema: loginSchema.shape.body, - }, - }, - }, - }, - responses: createApiResponse(tokenSchema, 'Success'), - }); - + router.post( + '/signup', + validateRequest(signupSchema), + AuthController.signup + ); router.post('/login', validateRequest(loginSchema), AuthController.login); return router; diff --git a/src/api/auth/authService.ts b/src/api/auth/authService.ts index 77388ac..1dfe98b 100644 --- a/src/api/auth/authService.ts +++ b/src/api/auth/authService.ts @@ -1,97 +1,71 @@ import { StatusCodes } from 'http-status-codes'; import jwt from 'jsonwebtoken'; +import { Knex } from 'knex'; + +import { CustomError } from '@/common/middleware/errorHandler'; -import { ResponseStatus, ServiceResponse } from '../../common/models/serviceResponse'; import { env } from '../../common/utils/envConfig'; -import { logger } from '../../server'; import { CreateUser } from '../user/userModel'; import { User } from '../user/userModel'; -import { userService } from '../user/userService'; +import userService from '../user/userService'; const authService = { - signup: async (payload: CreateUser) => { - try { - if ((await userService.checkEmailExists(payload.email)).success) { - return new ServiceResponse(ResponseStatus.Failed, 'Email already exists', null, StatusCodes.CONFLICT); - } - - if ((await userService.usernameExists(payload.username)).success) { - return new ServiceResponse( - ResponseStatus.Failed, - 'Username already exists', - null, - StatusCodes.CONFLICT - ); - } + signup: async (payload: CreateUser, trx: Knex.Transaction) => { + console.log(await userService.getByEmail(payload.email, trx)); + if (await userService.checkEmailExists(payload.email, trx)) { + throw new CustomError('Email already exists', StatusCodes.CONFLICT); + } - const user = await userService.createUser(payload); - return new ServiceResponse( - ResponseStatus.Success, - 'User created', - user.responseObject, - StatusCodes.CREATED + if (await userService.usernameExists(payload.username, trx)) { + throw new CustomError( + 'Username already exists', + StatusCodes.CONFLICT ); - } catch (ex) { - const errorMessage = `Error creating user: ${(ex as Error).message}`; - logger.error(errorMessage); - return new ServiceResponse(ResponseStatus.Failed, errorMessage, null, StatusCodes.INTERNAL_SERVER_ERROR); } - }, - login: async (email: string, password: string) => { - try { - if (!(await userService.checkEmailExists(email)).success) { - return new ServiceResponse(ResponseStatus.Failed, 'Email does not exist', null, StatusCodes.NOT_FOUND); - } - - const user = (await userService.findByEmail(email)).responseObject; - if (!user) { - return new ServiceResponse( - ResponseStatus.Failed, - 'Error finding user', - null, - StatusCodes.INTERNAL_SERVER_ERROR - ); - } - - if (user.password !== password) { - return new ServiceResponse(ResponseStatus.Failed, 'Invalid password', null, StatusCodes.UNAUTHORIZED); - } - - delete user?.password; - delete user?.bio; - delete user?.avatarUrl; - delete user?.status; - const token = jwt.sign(user, env.JWT_SECRET, { - expiresIn: 99999, - }); - return new ServiceResponse(ResponseStatus.Success, 'Login successful', token, StatusCodes.OK); - } catch (ex) { - const errorMessage = `Error logging in: ${(ex as Error).message}`; - logger.error(errorMessage); - return new ServiceResponse(ResponseStatus.Failed, errorMessage, null, StatusCodes.INTERNAL_SERVER_ERROR); - } + return await userService.createUser(payload, trx); }, + login: async ( + email: string, + password: string, + trx: Knex.Transaction + ): Promise<{ user: User; token: string }> => { + const user = await userService.getByEmail(email, trx); + if (!user) { + throw new CustomError("Email doesn't exists", StatusCodes.CONFLICT); + } - authenticate: async (header: string): Promise> => { - try { - const token = header.split(' ')[1]; - const result = jwt.verify(token, 'secret'); - if (!result) { - return new ServiceResponse(ResponseStatus.Failed, 'Unauthorized', null, StatusCodes.UNAUTHORIZED); - } + if (user.password !== password) { + throw new CustomError('Wrong password.', StatusCodes.CONFLICT); + } - const user = await userService.findById((result as User).id); - if (!user) { - return new ServiceResponse(ResponseStatus.Failed, 'User not found', null, StatusCodes.NOT_FOUND); - } + delete user?.password; + delete user?.bio; + delete user?.avatarUrl; + delete user?.status; - return new ServiceResponse(ResponseStatus.Success, 'Authorized', result as User, StatusCodes.OK); - } catch (ex) { - const errorMessage = `Error authenticating user: ${(ex as Error).message}`; - logger.error(errorMessage); - return new ServiceResponse(ResponseStatus.Failed, errorMessage, null, StatusCodes.INTERNAL_SERVER_ERROR); + const token = jwt.sign(user, env.JWT_SECRET, { + expiresIn: 99999, + }); + return { + token: token, + user: user, + }; + }, + authenticate: ( + header: string, + trx: Knex.Transaction + ): Promise => { + const token = header.split(' ')[1]; + if (!token) { + throw new CustomError('Unauthorized', StatusCodes.UNAUTHORIZED); + } + const result = jwt.verify(token, 'secret'); + if (!result) { + throw new CustomError('Unauthorized', StatusCodes.UNAUTHORIZED); } + + return userService.getById((result as User).id, trx); }, }; diff --git a/src/api/channels/channelRepository.ts b/src/api/channels/channelRepository.ts index ed2bb42..cb9cacc 100644 --- a/src/api/channels/channelRepository.ts +++ b/src/api/channels/channelRepository.ts @@ -1,9 +1,16 @@ import { Channel, CreateChannelDto } from '@/api/channels/channelModel'; export const channelRepository = { - createChannel: async (trx: any, channel: CreateChannelDto): Promise => { + createChannel: async ( + trx: any, + channel: CreateChannelDto + ): Promise => { const ids = await trx.insert(channel).into('channels'); - const newChannel = await trx.select('*').from('channels').where('id', ids[0]).first(); + const newChannel = await trx + .select('*') + .from('channels') + .where('id', ids[0]) + .first(); return newChannel; }, getWorkspaceChannels: async (trx: any, id: number): Promise => { diff --git a/src/api/channels/channelRouter.ts b/src/api/channels/channelRouter.ts index c8b4633..faf46c8 100644 --- a/src/api/channels/channelRouter.ts +++ b/src/api/channels/channelRouter.ts @@ -21,11 +21,15 @@ import ChannelController from './channelController'; export const channelRegistery = new OpenAPIRegistry(); -const bearerAuth = channelRegistery.registerComponent('securitySchemes', 'bearerAuth', { - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', -}); +const bearerAuth = channelRegistery.registerComponent( + 'securitySchemes', + 'bearerAuth', + { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + } +); channelRegistery.register('Channel', ChannelSchema); @@ -135,7 +139,10 @@ export const channelRouter: Router = (() => { path: '/channels/{id}/messages', tags: ['Channel'], security: [{ bearerAuth: [] }], - request: { params: GetChannelSchema.shape.params, query: GetChannelSchema.shape.query }, + request: { + params: GetChannelSchema.shape.params, + query: GetChannelSchema.shape.query, + }, responses: createApiResponse(Messages, 'Success'), }); diff --git a/src/api/coworkers/__tests__/coworkersRepository.test.ts b/src/api/coworkers/__tests__/coworkersRepository.test.ts index 36ed1c9..cc3bdad 100644 --- a/src/api/coworkers/__tests__/coworkersRepository.test.ts +++ b/src/api/coworkers/__tests__/coworkersRepository.test.ts @@ -7,25 +7,31 @@ describe('coworkerRepository', () => { }); test('createCoworker', async () => { - await coworkerRepository.createCoworker(trx, { userId: 1, workspaceId: 2 }); + await coworkerRepository.createCoworker( + { + userId: 1, + workspaceId: 2, + }, + trx + ); expect(trx.insert).toBeCalledWith({ userId: 1, workspaceId: 2 }); expect(trx.into).toBeCalledWith('coworkers'); }); test('getAllUserWorkspaces', async () => { - await coworkerRepository.getAllUserWorkspaces(trx, 1); + await coworkerRepository.getWorkspacesIds(1, trx); expect(trx.from).toBeCalledWith('coworkers'); expect(trx.where).toBeCalledWith('userId', 1); }); test('getAllWorkspaceUsers', async () => { - await coworkerRepository.getAllWorkspaceUsers(trx, 3); + await coworkerRepository.getUserIds(3, trx); expect(trx.from).toBeCalledWith('coworkers'); expect(trx.where).toBeCalledWith('workspaceId', 3); }); test('removeCoworker', async () => { - await coworkerRepository.removeCoworker(trx, 1, 1); + await coworkerRepository.removeCoworker(1, 1, trx); expect(trx.delete).toBeCalled(); expect(trx.from).toBeCalledWith('coworkers'); }); diff --git a/src/api/coworkers/coworkersRepository.ts b/src/api/coworkers/coworkersRepository.ts index 6c0bba3..745f25e 100644 --- a/src/api/coworkers/coworkersRepository.ts +++ b/src/api/coworkers/coworkersRepository.ts @@ -1,17 +1,39 @@ +import { Knex } from 'knex'; + import { Coworker, CreateCoworkerDto } from '@/api/coworkers/coworkersModel'; export const coworkerRepository = { - createCoworker: async (trx: any, coworker: CreateCoworkerDto): Promise => { + createCoworker: async ( + coworker: CreateCoworkerDto, + trx: Knex.Transaction + ): Promise => { const ids = await trx.insert(coworker).into('coworkers'); - return await trx.select('*').from('coworkers').where('id', ids[0]).first(); + return trx.select('*').from('coworkers').where('id', ids[0]).first(); }, - getAllUserWorkspaces: async (trx: any, userId: number): Promise => { - return await trx.select('*').from('coworkers').where('userId', userId); + getWorkspacesIds: ( + userId: number, + trx: Knex.Transaction + ): Promise => { + return trx.select('*').from('coworkers').where('userId', userId); }, - getAllWorkspaceUsers: async (trx: any, workspaceId: number): Promise => { - return await trx.select('*').from('coworkers').where('workspaceId', workspaceId); + getUserIds: ( + workspaceId: number, + trx: Knex.Transaction + ): Promise => { + return trx + .select('*') + .from('coworkers') + .where('workspaceId', workspaceId); }, - removeCoworker: async (trx: any, userId: number, workspaceId: number): Promise => { - return await trx.delete().from('coworkers').where('userId', userId).where('workspaceId', workspaceId); + removeCoworker: ( + userId: number, + workspaceId: number, + trx: Knex.Transaction + ): Promise => { + return trx + .delete() + .from('coworkers') + .where('userId', userId) + .where('workspaceId', workspaceId); }, }; diff --git a/src/api/coworkers/coworkersService.ts b/src/api/coworkers/coworkersService.ts new file mode 100644 index 0000000..a755c4f --- /dev/null +++ b/src/api/coworkers/coworkersService.ts @@ -0,0 +1,13 @@ +import { Knex } from 'knex'; + +import { Coworker } from './coworkersModel'; +import { coworkerRepository } from './coworkersRepository'; + +const coworkersService = { + getUserWorkspaces: async (userId: number, trx: Knex.Transaction) => { + const rows = await coworkerRepository.getWorkspacesIds(userId, trx); + return rows.map((row: Coworker) => row.workspaceId); + }, +}; + +export default coworkersService; diff --git a/src/api/files/filesController.ts b/src/api/files/filesController.ts index ce1f486..2f33a43 100644 --- a/src/api/files/filesController.ts +++ b/src/api/files/filesController.ts @@ -2,8 +2,24 @@ import { Request, Response } from 'express'; const filesController = { create: (req: Request, res: Response) => { - const { fileName, fileSize, fileType, content, messageId, uploadedBy, uploadAt } = req.body; - const createFileDto = { fileName, fileSize, fileType, content, messageId, uploadedBy, uploadAt }; + const { + fileName, + fileSize, + fileType, + content, + messageId, + uploadedBy, + uploadAt, + } = req.body; + const createFileDto = { + fileName, + fileSize, + fileType, + content, + messageId, + uploadedBy, + uploadAt, + }; console.log(createFileDto); res.status(201).send(createFileDto); diff --git a/src/api/files/filesRepository.ts b/src/api/files/filesRepository.ts index b570d71..17982d8 100644 --- a/src/api/files/filesRepository.ts +++ b/src/api/files/filesRepository.ts @@ -8,8 +8,14 @@ const filesRepository = { getFileById: async (trx: any, id: number): Promise => { return await trx.select('*').from('files').where('id', id).first(); }, - getFilesByMessageId: async (trx: any, messageId: number): Promise => { - return await trx.select('*').from('files').where('messageId', messageId); + getFilesByMessageId: async ( + trx: any, + messageId: number + ): Promise => { + return await trx + .select('*') + .from('files') + .where('messageId', messageId); }, getUserFiles: async (trx: any, userId: number): Promise => { return await trx.select('*').from('files').where('uploadedBy', userId); diff --git a/src/api/healthCheck/__tests__/00_healthCheckRouter.test.ts b/src/api/healthCheck/__tests__/00_healthCheckRouter.test.ts index 1f9c96e..5e5b218 100644 --- a/src/api/healthCheck/__tests__/00_healthCheckRouter.test.ts +++ b/src/api/healthCheck/__tests__/00_healthCheckRouter.test.ts @@ -10,8 +10,6 @@ describe('Health Check API endpoints', () => { const result: ServiceResponse = response.body; expect(response.statusCode).toEqual(StatusCodes.OK); - expect(result.success).toBeTruthy(); - expect(result.responseObject).toBeNull(); - expect(result.message).toEqual('Service is healthy'); + expect(result.message).toEqual('Service is good'); }); }); diff --git a/src/api/healthCheck/healthCheckRouter.ts b/src/api/healthCheck/healthCheckRouter.ts index 9ee7194..5688ca3 100644 --- a/src/api/healthCheck/healthCheckRouter.ts +++ b/src/api/healthCheck/healthCheckRouter.ts @@ -4,7 +4,6 @@ import { StatusCodes } from 'http-status-codes'; import { z } from 'zod'; import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; -import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; import { handleServiceResponse } from '@/common/utils/httpHandlers'; export const healthCheckRegistry = new OpenAPIRegistry(); @@ -20,8 +19,7 @@ export const healthCheckRouter: Router = (() => { }); router.get('/', (_req: Request, res: Response) => { - const serviceResponse = new ServiceResponse(ResponseStatus.Success, 'Service is healthy', null, StatusCodes.OK); - handleServiceResponse(serviceResponse, res); + handleServiceResponse(res, {}, 'Service is good', StatusCodes.OK); }); return router; diff --git a/src/api/invites/invitesController.ts b/src/api/invites/invitesController.ts index 20be75f..169e9d0 100644 --- a/src/api/invites/invitesController.ts +++ b/src/api/invites/invitesController.ts @@ -16,7 +16,10 @@ const InvitesController = { expiresAt: new Date(Date.now() + 200 * 60 * 60 * 1000), }; - const invite = await invitesRepository.createInvite(db, createInvitePayload); + const invite = await invitesRepository.createInvite( + db, + createInvitePayload + ); res.json(invite); }, getInviteById: async (req: Request, res: Response) => { @@ -27,7 +30,10 @@ const InvitesController = { getWorkspaceInvites: async (req: Request, res: Response) => { const workspaceId = req.params.id; - const invites = await invitesRepository.getInviteByWorkspaceId(db, workspaceId); + const invites = await invitesRepository.getInviteByWorkspaceId( + db, + workspaceId + ); res.json(invites); }, acceptInvite: async (req: Request, res: Response) => { diff --git a/src/api/invites/invitesRepository.ts b/src/api/invites/invitesRepository.ts index 35dce24..10ecd7b 100644 --- a/src/api/invites/invitesRepository.ts +++ b/src/api/invites/invitesRepository.ts @@ -1,21 +1,41 @@ import { CreateInviteDto, Invite } from './invitesModel'; const invitesRepository = { - createInvite: async (trx: any, invite: CreateInviteDto): Promise => { + createInvite: async ( + trx: any, + invite: CreateInviteDto + ): Promise => { const ids = await trx.insert(invite).into('invites'); - return await trx.select('*').from('invites').where('id', ids[0]).first(); + return await trx + .select('*') + .from('invites') + .where('id', ids[0]) + .first(); }, getInviteById: async (trx: any, id: string): Promise => { return await trx.select('*').from('invites').where('id', id).first(); }, - getInviteByWorkspaceId: async (trx: any, workspaceId: string): Promise => { - return await trx.select('*').from('invites').where('workspaceId', workspaceId).andWhere('status', 'pending'); + getInviteByWorkspaceId: async ( + trx: any, + workspaceId: string + ): Promise => { + return await trx + .select('*') + .from('invites') + .where('workspaceId', workspaceId) + .andWhere('status', 'pending'); }, acceptInvite: async (trx: any, id: string): Promise => { - return await trx.update({ status: 'accepted' }).from('invites').where('id', id); + return await trx + .update({ status: 'accepted' }) + .from('invites') + .where('id', id); }, cancelInvite: async (trx: any, id: string): Promise => { - return await trx.update({ status: 'cancelled' }).from('invites').where('id', id); + return await trx + .update({ status: 'cancelled' }) + .from('invites') + .where('id', id); }, }; diff --git a/src/api/members/memberRepository.ts b/src/api/members/memberRepository.ts index 51aa077..f21bf56 100644 --- a/src/api/members/memberRepository.ts +++ b/src/api/members/memberRepository.ts @@ -1,17 +1,38 @@ import { CreateMemberDto, Member } from '@/api/members/memberModel'; export const memberRepository = { - createMember: async (trx: any, member: CreateMemberDto): Promise => { + createMember: async ( + trx: any, + member: CreateMemberDto + ): Promise => { const ids = await trx.insert(member).into('members'); - return await trx.select('*').from('members').where('userId', ids[0]).first(); + return await trx + .select('*') + .from('members') + .where('userId', ids[0]) + .first(); }, getAllUserChannels: async (trx: any, userId: number): Promise => { return await trx.select('*').from('members').where('userId', userId); }, - getAllChannelUsers: async (trx: any, channelId: number): Promise => { - return await trx.select('*').from('members').where('channelId', channelId); + getAllChannelUsers: async ( + trx: any, + channelId: number + ): Promise => { + return await trx + .select('*') + .from('members') + .where('channelId', channelId); }, - removeMember: async (trx: any, userId: number, channelId: number): Promise => { - return await trx.delete().from('members').where('userId', userId).where('channelId', channelId); + removeMember: async ( + trx: any, + userId: number, + channelId: number + ): Promise => { + return await trx + .delete() + .from('members') + .where('userId', userId) + .where('channelId', channelId); }, }; diff --git a/src/api/messages/messageController.ts b/src/api/messages/messageController.ts index 9715b81..a1625fe 100644 --- a/src/api/messages/messageController.ts +++ b/src/api/messages/messageController.ts @@ -8,7 +8,8 @@ const MessageController = { const channelId = parseInt(req.params.id); const { cursor, limit } = req.query; console.log(cursor, ' ', limit); - const messages = await messageRepository.getAllChannelMessages(channelId); + const messages = + await messageRepository.getAllChannelMessages(channelId); res.json(messages); }, getMessageById: async (req: Request, res: Response) => { diff --git a/src/api/messages/messageModel.ts b/src/api/messages/messageModel.ts index 0f47f4a..e5ebf4f 100644 --- a/src/api/messages/messageModel.ts +++ b/src/api/messages/messageModel.ts @@ -58,4 +58,10 @@ export const GetThreadSchema = z.object({ params: z.object({ id: z.number() }), }); +export type deleteMessageData = { + id: number; + channelId: number; + workspaceId: number; +}; + export const Messages = z.array(MessageSchema); diff --git a/src/api/messages/messageRepository.ts b/src/api/messages/messageRepository.ts index 6d4ff50..431416a 100644 --- a/src/api/messages/messageRepository.ts +++ b/src/api/messages/messageRepository.ts @@ -12,12 +12,25 @@ export const messageRepository = { return await db('messages').where('id', id).first(); }, getAllChannelMessages: async (channelId: number): Promise => { - return await db.select('*').from('messages').where('channelId', channelId); + return await db + .select('*') + .from('messages') + .where('channelId', channelId); }, - getAllUserMessages: async (userId: number, channelId: number): Promise => { - return await db.select('*').from('messages').where('userId', userId).where('channelId', channelId); + getAllUserMessages: async ( + userId: number, + channelId: number + ): Promise => { + return await db + .select('*') + .from('messages') + .where('userId', userId) + .where('channelId', channelId); }, - editMessage: async (id: number, message: CreateMessageDto): Promise => { + editMessage: async ( + id: number, + message: CreateMessageDto + ): Promise => { await db('messages').where('id', id).update(message); const updatedMessage = await db('messages').where('id', id).first(); return updatedMessage; diff --git a/src/api/messages/messageRouter.ts b/src/api/messages/messageRouter.ts index e09b987..2b7fe8e 100644 --- a/src/api/messages/messageRouter.ts +++ b/src/api/messages/messageRouter.ts @@ -17,11 +17,15 @@ import { export const messageRegistery = new OpenAPIRegistry(); -const bearerAuth = messageRegistery.registerComponent('securitySchemes', 'bearerAuth', { - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', -}); +const bearerAuth = messageRegistery.registerComponent( + 'securitySchemes', + 'bearerAuth', + { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + } +); messageRegistery.register('Message', MessageSchema); diff --git a/src/api/notifications/__tests__/notificationsRepository.test.ts b/src/api/notifications/__tests__/notificationsRepository.test.ts index a666535..426d2d4 100644 --- a/src/api/notifications/__tests__/notificationsRepository.test.ts +++ b/src/api/notifications/__tests__/notificationsRepository.test.ts @@ -28,7 +28,10 @@ describe('notificationRepository', () => { }); test('getUnreadNotificationsByRecipientId', async () => { - await notificationsRepository.getUnreadNotificationsByRecipientId(trx, 1); + await notificationsRepository.getUnreadNotificationsByRecipientId( + trx, + 1 + ); expect(trx.select).toBeCalledWith('*'); expect(trx.from).toBeCalledWith('notifications'); expect(trx.where).toBeCalledWith('recipientId', 1); diff --git a/src/api/notifications/notificationsRepository.ts b/src/api/notifications/notificationsRepository.ts index fd7bbbb..2d2234c 100644 --- a/src/api/notifications/notificationsRepository.ts +++ b/src/api/notifications/notificationsRepository.ts @@ -1,23 +1,53 @@ import { CreateNotificationDto, Notification } from './notificationsModel'; const notificationsRepository = { - createNotification: async (trx: any, notification: CreateNotificationDto): Promise => { + createNotification: async ( + trx: any, + notification: CreateNotificationDto + ): Promise => { // const ids = await trx('notifications').insert(notification); // const newNotification = await trx('notifications').where('id', ids[0]).first(); const ids = await trx.insert(notification).into('notifications'); - return await trx.select('*').from('notifications').where('id', ids[0]).first(); + return await trx + .select('*') + .from('notifications') + .where('id', ids[0]) + .first(); }, - getNotificationById: async (trx: any, id: number): Promise => { - return await trx.select('*').from('notifications').where('id', id).first(); + getNotificationById: async ( + trx: any, + id: number + ): Promise => { + return await trx + .select('*') + .from('notifications') + .where('id', id) + .first(); }, - getNotificationsByRecipientId: async (trx: any, recipientId: number): Promise => { - return await trx.select('*').from('notifications').where('recipientId', recipientId); + getNotificationsByRecipientId: async ( + trx: any, + recipientId: number + ): Promise => { + return await trx + .select('*') + .from('notifications') + .where('recipientId', recipientId); }, - getUnreadNotificationsByRecipientId: async (trx: any, recipientId: number): Promise => { - return await trx.select('*').from('notifications').where('recipientId', recipientId).andWhere('isRead', false); + getUnreadNotificationsByRecipientId: async ( + trx: any, + recipientId: number + ): Promise => { + return await trx + .select('*') + .from('notifications') + .where('recipientId', recipientId) + .andWhere('isRead', false); }, markAsRead: async (trx: any, id: number): Promise => { - return await trx.update({ isRead: true }).from('notifications').where('id', id); + return await trx + .update({ isRead: true }) + .from('notifications') + .where('id', id); }, deleteNotification: async (trx: any, id: number): Promise => { return await trx.delete().from('notifications').where('id', id); diff --git a/src/api/notifications/notificationsRoutes.ts b/src/api/notifications/notificationsRoutes.ts index a3845b3..7bdc700 100644 --- a/src/api/notifications/notificationsRoutes.ts +++ b/src/api/notifications/notificationsRoutes.ts @@ -24,7 +24,11 @@ export const notificationRouter: Router = (() => { responses: createApiResponse(notificationSchema, 'Success'), }); - router.get('/:id', validateRequest(notificationIdSchema), notificationsController.create); + router.get( + '/:id', + validateRequest(notificationIdSchema), + notificationsController.create + ); notificationsRegistry.registerPath({ method: 'patch', @@ -34,7 +38,11 @@ export const notificationRouter: Router = (() => { responses: createApiResponse(notificationSchema, 'Success'), }); - router.patch('/:id/read', validateRequest(notificationIdSchema), notificationsController.markAsRead); + router.patch( + '/:id/read', + validateRequest(notificationIdSchema), + notificationsController.markAsRead + ); notificationsRegistry.registerPath({ method: 'patch', @@ -44,7 +52,11 @@ export const notificationRouter: Router = (() => { responses: createApiResponse(notificationSchema, 'Success'), }); - router.patch('/:id/unread', validateRequest(notificationIdSchema), notificationsController.markAsRead); + router.patch( + '/:id/unread', + validateRequest(notificationIdSchema), + notificationsController.markAsRead + ); notificationsRegistry.registerPath({ method: 'delete', @@ -54,7 +66,11 @@ export const notificationRouter: Router = (() => { responses: createApiResponse(messageResponse, 'Success'), }); - router.delete('/', validateRequest(notificationIdSchema), notificationsController.delete); + router.delete( + '/', + validateRequest(notificationIdSchema), + notificationsController.delete + ); return router; })(); diff --git a/src/api/reactions/reactionRepository.ts b/src/api/reactions/reactionRepository.ts index 0285764..a9ee432 100644 --- a/src/api/reactions/reactionRepository.ts +++ b/src/api/reactions/reactionRepository.ts @@ -10,7 +10,10 @@ export const reactionRepository = { }, getReactionsByMessageId: async (messageId: number): Promise => { - return await db.select('*').from('reactions').where('messageId', messageId); + return await db + .select('*') + .from('reactions') + .where('messageId', messageId); }, deleteReaction: async (id: number): Promise => { diff --git a/src/api/reactions/reactionsRouter.ts b/src/api/reactions/reactionsRouter.ts index 976eb8b..de7e583 100644 --- a/src/api/reactions/reactionsRouter.ts +++ b/src/api/reactions/reactionsRouter.ts @@ -5,7 +5,11 @@ import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; import { messageResponse } from '@/common/utils/commonResponses'; import { validateRequest } from '../../common/utils/httpHandlers'; -import { CreateReactionSchema, DeleteReactionSchema, ReactionSchema } from './reactionModel'; +import { + CreateReactionSchema, + DeleteReactionSchema, + ReactionSchema, +} from './reactionModel'; import reactionsController from './reactionsController'; export const reactionsRegistry = new OpenAPIRegistry(); @@ -21,14 +25,20 @@ export const reactionsRouter: Router = (() => { request: { body: { content: { - 'application/json': { schema: CreateReactionSchema.shape.body }, + 'application/json': { + schema: CreateReactionSchema.shape.body, + }, }, }, }, responses: createApiResponse(ReactionSchema, 'Success'), }); - router.post('/add', validateRequest(CreateReactionSchema), reactionsController.add); + router.post( + '/add', + validateRequest(CreateReactionSchema), + reactionsController.add + ); reactionsRegistry.registerPath({ method: 'delete', @@ -37,14 +47,20 @@ export const reactionsRouter: Router = (() => { request: { body: { content: { - 'application/json': { schema: CreateReactionSchema.shape.body }, + 'application/json': { + schema: CreateReactionSchema.shape.body, + }, }, }, }, responses: createApiResponse(messageResponse, 'Success'), }); - router.delete('/delete', validateRequest(DeleteReactionSchema), reactionsController.delete); + router.delete( + '/delete', + validateRequest(DeleteReactionSchema), + reactionsController.delete + ); return router; })(); diff --git a/src/api/threads/__tests__/threadsRepository.test.ts b/src/api/threads/__tests__/threadsRepository.test.ts index ad22b9b..9c6f283 100644 --- a/src/api/threads/__tests__/threadsRepository.test.ts +++ b/src/api/threads/__tests__/threadsRepository.test.ts @@ -4,8 +4,14 @@ import threadsRepository from '../threadsRepository'; describe('threadsRepository', () => { const trx: any = mockedTxn; test('createThread', async () => { - await threadsRepository.createThread(trx, { participantId: 3, parentMessageId: 3 }); - expect(trx.insert).toBeCalledWith({ participantId: 3, parentMessageId: 3 }); + await threadsRepository.createThread(trx, { + participantId: 3, + parentMessageId: 3, + }); + expect(trx.insert).toBeCalledWith({ + participantId: 3, + parentMessageId: 3, + }); expect(trx.into).toBeCalledWith('threads'); }); diff --git a/src/api/threads/threadsRepository.ts b/src/api/threads/threadsRepository.ts index c14eef4..c4be2f0 100644 --- a/src/api/threads/threadsRepository.ts +++ b/src/api/threads/threadsRepository.ts @@ -1,14 +1,31 @@ import { CreateThreadDto, Thread } from './threadsModel'; const threadsRepository = { - createThread: async (trx: any, thread: CreateThreadDto): Promise => { + createThread: async ( + trx: any, + thread: CreateThreadDto + ): Promise => { const ids = await trx.insert(thread).into('threads'); - return await trx.select('*').from('threads').where('participantId', ids[0]).first(); + return await trx + .select('*') + .from('threads') + .where('participantId', ids[0]) + .first(); }, - getUserThreads: async (trx: any, participantId: number): Promise => { - return await trx.select('*').from('threads').where('participantId', participantId); + getUserThreads: async ( + trx: any, + participantId: number + ): Promise => { + return await trx + .select('*') + .from('threads') + .where('participantId', participantId); }, - deleteThread: async (trx: any, participantId: number, messageId: number): Promise => { + deleteThread: async ( + trx: any, + participantId: number, + messageId: number + ): Promise => { return await trx .delete() .from('threads') diff --git a/src/api/user/__tests__/userController.test.ts b/src/api/user/__tests__/userController.test.ts new file mode 100644 index 0000000..48c6fb8 --- /dev/null +++ b/src/api/user/__tests__/userController.test.ts @@ -0,0 +1,8 @@ +import { vi } from 'vitest'; + +describe('user controller', () => { + afterAll(async () => { + vi.clearAllMocks(); + }); + it('get user workspace', () => {}); +}); diff --git a/src/api/user/__tests__/userRepository.ts b/src/api/user/__tests__/userRepository.ts new file mode 100644 index 0000000..a1ae8f1 --- /dev/null +++ b/src/api/user/__tests__/userRepository.ts @@ -0,0 +1,21 @@ +import { vi } from 'vitest'; + +// import { mockedTxn } from '../../../common/__tests__/mocks'; + +describe('userRepository', () => { + // const trx: any = mockedTxn; + afterAll(async () => { + vi.clearAllMocks(); + }); + + test('create', async () => { + // const user = { + // bio: 'lul', + // password: 'pass', + // avatarUrl: 'haitchttp', + // status: 'offline', + // name: 'abdo', + // username: '7adidaz', + // }; + }); +}); diff --git a/src/api/user/userApi.ts b/src/api/user/userApi.ts new file mode 100644 index 0000000..7308606 --- /dev/null +++ b/src/api/user/userApi.ts @@ -0,0 +1,86 @@ +export const userRegistry = new OpenAPIRegistry(); +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import { StatusCodes } from 'http-status-codes'; + +import { + GetUserSchema, + UpdateUserSchema, + UserSchema, +} from '@/api/user/userModel'; +import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; +import { messageResponse } from '@/common/utils/commonResponses'; + +const bearerAuth = userRegistry.registerComponent( + 'securitySchemes', + 'bearerAuth', + { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + } +); + +userRegistry.register('User', UserSchema); + +userRegistry.registerPath({ + method: 'get', + path: '/users/{id}', + tags: ['User'], + security: [{ [bearerAuth.name]: [] }], + request: { params: GetUserSchema.shape.params }, + responses: createApiResponse(UserSchema, 'Success'), +}); + +userRegistry.registerPath({ + method: 'patch', + path: '/users/{id}', + tags: ['User'], + security: [{ [bearerAuth.name]: [] }], + request: { + params: GetUserSchema.shape.params, + body: { + content: { + 'application/json': { + schema: UpdateUserSchema.shape.body, + }, + }, + }, + }, + responses: { + [StatusCodes.OK]: { + description: 'Accepted Response', + content: { + 'application/json': { + schema: UserSchema, + }, + }, + }, + }, +}); + +userRegistry.registerPath({ + method: 'delete', + path: '/users/{id}', + tags: ['User'], + security: [{ [bearerAuth.name]: [] }], + request: { params: GetUserSchema.shape.params }, + responses: { + [StatusCodes.OK]: { + description: 'Accepted Response', + content: { + 'application/json': { + schema: messageResponse, + }, + }, + }, + }, +}); + +userRegistry.registerPath({ + method: 'get', + path: '/users/{id}/workspaces', + tags: ['User'], + security: [{ [bearerAuth.name]: [] }], + request: { params: GetUserSchema.shape.params }, + responses: createApiResponse(messageResponse, 'Success'), +}); diff --git a/src/api/user/userController.ts b/src/api/user/userController.ts index f5e7f2f..80ec422 100644 --- a/src/api/user/userController.ts +++ b/src/api/user/userController.ts @@ -1,31 +1,46 @@ import { Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; + +import { + asyncHandler, + handleServiceResponse, +} from '@/common/utils/httpHandlers'; import { UpdateUserDto } from './userModel'; +import userService from './userService'; const UserController = { - getById: async (req: Request, res: Response) => { - const id = parseInt(req.params.id); - res.json({ id }); - }, - updateUser: async (req: Request, res: Response) => { + getById: asyncHandler(async (req: Request, res: Response) => { const id = parseInt(req.params.id); - res.json({ id }); - }, - deleteUser: async (req: Request, res: Response) => { - const id = parseInt(req.params.id); - res.json({ id }); - }, - update: async (req: Request, res: Response) => { + const user = await userService.getById(id, res.trx); + if (!user) { + handleServiceResponse( + res, + null, + 'User not found', + StatusCodes.NOT_FOUND + ); + return; + } + handleServiceResponse(res, user, 'ok'); + }), + update: asyncHandler(async (req: Request, res: Response) => { const { name, email, bio, password } = req.body; const updateUserDto: UpdateUserDto = { name, email, bio, password }; - console.log(updateUserDto); - res.status(200).json({ message: 'User updated successfully' }); - }, - delete: async (req: Request, res: Response) => { const id = parseInt(req.params.id); - console.log(id); - res.status(200).json({ message: 'User deleted successfully' }); - }, + const user = await userService.updateUser(id, updateUserDto, res.trx); + handleServiceResponse(res, user, 'ok'); + }), + delete: asyncHandler(async (req: Request, res: Response) => { + const id = parseInt(req.params.id); + await userService.deleteUser(id, res.trx); + handleServiceResponse(res, null, 'ok'); + }), + getWorkspaces: asyncHandler(async (req: Request, res: Response) => { + const userId = parseInt(req.params.id); + const workspaces = await userService.getWorkspaces(userId, res.trx); + handleServiceResponse(res, workspaces, 'OK'); + }), }; export default UserController; diff --git a/src/api/user/userModel.ts b/src/api/user/userModel.ts index 60d4c34..4abf76a 100644 --- a/src/api/user/userModel.ts +++ b/src/api/user/userModel.ts @@ -35,6 +35,7 @@ export const GetUserSchema = z.object({ }); export const UpdateUserSchema = z.object({ + params: z.object({ id: commonValidations.id }), body: z.object({ name: z.string(), email: z.string(), diff --git a/src/api/user/userRepository.ts b/src/api/user/userRepository.ts index 6012680..35be803 100644 --- a/src/api/user/userRepository.ts +++ b/src/api/user/userRepository.ts @@ -1,27 +1,42 @@ -import { CreateUserDto, User } from '@/api/user/userModel'; +import { Knex } from 'knex'; -import db from '../../../db/db'; +import { CreateUserDto, UpdateUserDto, User } from '@/api/user/userModel'; export const userRepository = { - createUser: async (user: CreateUserDto): Promise => { - const ids = await db('users').insert(user); - const newUser = await db('users').where('id', ids[0]).first(); - return newUser; + create: async ( + user: CreateUserDto, + trx: Knex.Transaction + ): Promise => { + const ids = await trx.insert(user).into('users'); + return trx.select('*').from('users').where('id', ids[0]).first(); }, - - findAll: async (): Promise => { - return await db.select('*').from('users'); + getById: (id: number, trx: Knex.Transaction): Promise => { + return trx.select('*').from('users').where('id', id).first(); }, - - findById: async (id: number): Promise => { - return await db.select('*').from('users').where('id', id).first(); + getByEmail: ( + email: string, + trx: Knex.Transaction + ): Promise => { + return trx.select('*').from('users').where('email', email).first(); }, - - findByEmail: async (email: string): Promise => { - return await db.select('*').from('users').where('email', email).first(); + getByUsername: ( + username: string, + trx: Knex.Transaction + ): Promise => { + return trx + .select('*') + .from('users') + .where('username', username) + .first(); }, - - findByUsername: async (username: string): Promise => { - return await db.select('*').from('users').where('username', username).first(); + update: ( + id: number, + user: UpdateUserDto, + trx: Knex.Transaction + ): Promise => { + return trx('users').where('id', id).update(user); + }, + delete: (id: number, trx: Knex.Transaction): Promise => { + return trx('users').where('id', id).del(); }, }; diff --git a/src/api/user/userRouter.ts b/src/api/user/userRouter.ts index b7292cf..13dc2b6 100644 --- a/src/api/user/userRouter.ts +++ b/src/api/user/userRouter.ts @@ -1,64 +1,46 @@ -import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; import express, { Router } from 'express'; -import { GetUserSchema, UpdateUserSchema, UserSchema } from '@/api/user/userModel'; -import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; -import { messageResponse } from '@/common/utils/commonResponses'; -import { validateRequest } from '@/common/utils/httpHandlers'; +import { GetUserSchema, UpdateUserSchema } from '@/api/user/userModel'; +import { + checkIdExists, + RESOURCES, + validateRequest, +} from '@/common/utils/httpHandlers'; import AuthController from '../auth/authController'; import UserController from './userController'; -export const userRegistry = new OpenAPIRegistry(); - -const bearerAuth = userRegistry.registerComponent('securitySchemes', 'bearerAuth', { - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', -}); - -userRegistry.register('User', UserSchema); - export const userRouter: Router = (() => { const router = express.Router(); - userRegistry.registerPath({ - method: 'get', - path: '/users/{id}', - tags: ['User'], - security: [{ [bearerAuth.name]: [] }], - request: { params: GetUserSchema.shape.params }, - responses: createApiResponse(UserSchema, 'Success'), - }); - - router.get('/:id', [AuthController.authenticate, validateRequest(GetUserSchema)], UserController.getById); - userRegistry.registerPath({ - method: 'put', - path: '/users/{id}', - tags: ['User'], - security: [{ [bearerAuth.name]: [] }], - request: { - params: GetUserSchema.shape.params, - body: { - content: { - 'application/json': { - schema: UpdateUserSchema.shape.body, - }, - }, - }, - }, - responses: createApiResponse(UserSchema, 'Success'), - }); - router.put('/:id', [AuthController.authenticate, validateRequest(GetUserSchema)], UserController.updateUser); - userRegistry.registerPath({ - method: 'delete', - path: '/users/{id}', - tags: ['User'], - security: [{ [bearerAuth.name]: [] }], - request: { params: GetUserSchema.shape.params }, - responses: createApiResponse(messageResponse, 'Success'), - }); - router.delete('/:id', [AuthController.authenticate, validateRequest(GetUserSchema)], UserController.deleteUser); + router.get( + '/:id', + [ + AuthController.authenticate, + validateRequest(GetUserSchema), + checkIdExists('id', RESOURCES.USER), + ], + UserController.getById + ); + router.patch( + '/:id', + [ + AuthController.authenticate, + validateRequest(UpdateUserSchema), + checkIdExists('id', RESOURCES.USER), + ], + UserController.update + ); + router.delete( + '/:id', + [AuthController.authenticate, validateRequest(GetUserSchema)], + UserController.delete + ); + router.get( + '/:id/workspaces', + [AuthController.authenticate, validateRequest(GetUserSchema)], + UserController.getWorkspaces + ); return router; })(); diff --git a/src/api/user/userService.ts b/src/api/user/userService.ts index ef90b4d..b626d20 100644 --- a/src/api/user/userService.ts +++ b/src/api/user/userService.ts @@ -1,123 +1,64 @@ -import { StatusCodes } from 'http-status-codes'; +import { Knex } from 'knex'; -import { CreateUser, CreateUserDto, User } from '@/api/user/userModel'; +import { + CreateUser, + CreateUserDto, + UpdateUserDto, + User, +} from '@/api/user/userModel'; import { userRepository } from '@/api/user/userRepository'; -import { InternalServiceResponse, ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; -import { logger } from '@/server'; -export const userService = { - createUser: async (user: CreateUser): Promise> => { - try { - const userPayload: CreateUserDto = { - ...user, - bio: '', - avatarUrl: '', - status: 'offline', - }; - const newUser = await userRepository.createUser(userPayload); - if (!newUser) { - return new InternalServiceResponse(ResponseStatus.Failed, null); - } - return new InternalServiceResponse(ResponseStatus.Success, newUser); - } catch (ex) { - const errorMessage = `Error creating user: ${(ex as Error).message}`; - logger.error(errorMessage); - return new InternalServiceResponse(ResponseStatus.Failed, null); - } - }, +import coworkersService from '../coworkers/coworkersService'; +import workspaceService from '../workspace/workspaceService'; - checkEmailExists: async (email: string): Promise> => { - try { - const user = await userRepository.findByEmail(email); - if (!user) { - return new InternalServiceResponse(ResponseStatus.Failed, false); - } - return new InternalServiceResponse(ResponseStatus.Success, true); - } catch (ex) { - const errorMessage = `Error checking if email exists: ${(ex as Error).message}`; - logger.error(errorMessage); - return new InternalServiceResponse(ResponseStatus.Failed, false); - } +const userService = { + createUser: async ( + user: CreateUser, + trx: Knex.Transaction + ): Promise => { + const userPayload: CreateUserDto = { + ...user, + bio: '', + avatarUrl: '', + status: 'offline', + }; + return userRepository.create(userPayload, trx); }, - - usernameExists: async (username: string): Promise> => { - try { - const user = await userRepository.findByUsername(username); - if (!user) { - return new InternalServiceResponse(ResponseStatus.Failed, false); - } - return new InternalServiceResponse(ResponseStatus.Success, true); - } catch (ex) { - const errorMessage = `Error checking if username exists: ${(ex as Error).message}`; - logger.error(errorMessage); - return new InternalServiceResponse(ResponseStatus.Failed, false); - } + checkEmailExists: async ( + email: string, + trx: Knex.Transaction + ): Promise => { + return !!(await userRepository.getByEmail(email, trx)); }, - - // Retrieves all users from the database - findAll: async (): Promise> => { - try { - const users = await userRepository.findAll(); - if (!users) { - return new ServiceResponse(ResponseStatus.Failed, 'No Users found', null, StatusCodes.NOT_FOUND); - } - return new ServiceResponse(ResponseStatus.Success, 'Users found', users, StatusCodes.OK); - } catch (ex) { - const errorMessage = `Error finding all users: $${(ex as Error).message}`; - logger.error(errorMessage); - return new ServiceResponse(ResponseStatus.Failed, errorMessage, null, StatusCodes.INTERNAL_SERVER_ERROR); - } + usernameExists: async ( + username: string, + trx: Knex.Transaction + ): Promise => { + return !!(await userRepository.getByUsername(username, trx)); }, - - // Retrieves a single user by their ID - findById: async (id: number): Promise> => { - try { - const user = await userRepository.findById(id); - if (!user) { - return new ServiceResponse(ResponseStatus.Failed, 'User not found', null, StatusCodes.NOT_FOUND); - } - return new ServiceResponse(ResponseStatus.Success, 'User found', user, StatusCodes.OK); - } catch (ex) { - const errorMessage = `Error finding user with id ${id}:, ${(ex as Error).message}`; - logger.error(errorMessage); - return new ServiceResponse(ResponseStatus.Failed, errorMessage, null, StatusCodes.INTERNAL_SERVER_ERROR); - } + getById: (id: number, trx: Knex.Transaction): Promise => { + return userRepository.getById(id, trx); }, - - findByEmail: async (email: string): Promise> => { - try { - const user = await userRepository.findByEmail(email); - if (!user) { - return new InternalServiceResponse(ResponseStatus.Failed, null); - } - return new InternalServiceResponse(ResponseStatus.Success, user); - } catch (ex) { - const errorMessage = `Error finding user with email ${email}: ${(ex as Error).message}`; - logger.error(errorMessage); - return new InternalServiceResponse(ResponseStatus.Failed, null); - } + getByEmail: ( + email: string, + trx: Knex.Transaction + ): Promise => { + return userRepository.getByEmail(email, trx); + }, + getWorkspaces: async (userId: number, trx: Knex.Transaction) => { + const ids = await coworkersService.getUserWorkspaces(userId, trx); + return workspaceService.getByIds(ids, trx); + }, + updateUser: ( + userId: number, + user: UpdateUserDto, + trx: Knex.Transaction + ): Promise => { + return userRepository.update(userId, user, trx); + }, + deleteUser: (userId: number, trx: Knex.Transaction): Promise => { + return userRepository.delete(userId, trx); }, - // updateUser: async (id: number, user: UpdateUser): Promise> => { - // try { - // const updatedUser = await userRepository.updateUser(id, user); - // if (!updatedUser) { - // return new ServiceResponse(ResponseStatus.Failed, 'User not found', null, StatusCodes.NOT_FOUND); - // } - // return new ServiceResponse(ResponseStatus.Success, 'User updated', updatedUser, StatusCodes.OK); - // } catch (ex) { - // const errorMessage = `Error updating user with id ${id}: ${(ex as Error).message}`; - // logger.error(errorMessage); - // return new ServiceResponse(ResponseStatus.Failed, errorMessage, null, StatusCodes.INTERNAL_SERVER_ERROR); - // } - // }, - // deleteUser: async (id: number): Promise> => { - // try { - // await userRepository.deleteUser(id); - // return new ServiceResponse(ResponseStatus.Success, 'User deleted', null, StatusCodes.OK); - // } catch (ex) { - // const errorMessage = `Error deleting user with id ${id}: ${(ex as Error).message}`; - // logger.error(errorMessage); - // return new ServiceResponse(ResponseStatus.Failed, errorMessage, null, StatusCodes.INTERNAL_SERVER_ERROR); - // } - // }, }; + +export default userService; diff --git a/src/api/workspace/__tests__/workspaceRepository.test.ts b/src/api/workspace/__tests__/workspaceRepository.test.ts index 6470bfa..b28253b 100644 --- a/src/api/workspace/__tests__/workspaceRepository.test.ts +++ b/src/api/workspace/__tests__/workspaceRepository.test.ts @@ -15,18 +15,18 @@ describe('workspaceRepository', () => { description: 'description', avatarUrl: 'url', }; - await workspaceRepository.createWorkspace(trx, workspaceData); + await workspaceRepository.create(workspaceData, trx); expect(trx.insert).toBeCalledWith(workspaceData); expect(trx.into).toBeCalledWith('workspaces'); }); test('getAllUserWorkspaces', async () => { - await workspaceRepository.findAllUserWorkspaces(trx, 1); + await workspaceRepository.getByIds([1], trx); expect(trx.from).toBeCalledWith('workspaces'); - expect(trx.where).toBeCalledWith('ownerId', 1); + expect(trx.whereIn).toBeCalledWith('id', [1]); }); test('deleteWorkspace', async () => { - await workspaceRepository.deleteWorkspace(trx, 4); + await workspaceRepository.delete(4, trx); expect(trx.where).toBeCalledWith('id', 4); }); }); diff --git a/src/api/workspace/workspaceApi.ts b/src/api/workspace/workspaceApi.ts new file mode 100644 index 0000000..d2d8220 --- /dev/null +++ b/src/api/workspace/workspaceApi.ts @@ -0,0 +1,109 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; + +import * as Schemas from '@/api/workspace/workspaceModel'; +import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; +import { messageResponse } from '@/common/utils/commonResponses'; + +import { Channels } from '../channels/channelModel'; +import { Threads } from '../threads/threadsModel'; +import { Users } from '../user/userModel'; + +export const workspaceRegistry = new OpenAPIRegistry(); + +const bearerAuth = workspaceRegistry.registerComponent( + 'securitySchemes', + 'bearerAuth', + { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + } +); + +workspaceRegistry.register('Workspace', Schemas.WorkspaceSchema); + +workspaceRegistry.registerPath({ + method: 'post', + path: '/workspaces', + tags: ['Workspace'], + security: [{ [bearerAuth.name]: [] }], + request: { + body: { + content: { + 'application/json': { + schema: Schemas.CreateWorkspaceSchema.shape.body, + }, + }, + }, + }, + responses: createApiResponse(Schemas.WorkspaceSchema, 'Success'), +}); + +workspaceRegistry.registerPath({ + method: 'get', + path: '/workspaces/{id}', + tags: ['Workspace'], + security: [{ [bearerAuth.name]: [] }], + request: { params: Schemas.GetWorkspaceSchema.shape.params }, + responses: createApiResponse(Schemas.WorkspaceSchema, 'Success'), +}); + +workspaceRegistry.registerPath({ + method: 'patch', + path: '/workspaces/{id}', + tags: ['Workspace'], + security: [{ [bearerAuth.name]: [] }], + request: { + params: Schemas.UpdateWorkspaceSchema.shape.params, + body: { + content: { + 'application/json': { + schema: Schemas.UpdateWorkspaceSchema.shape.body, + }, + }, + }, + }, + responses: createApiResponse(Schemas.WorkspaceSchema, 'Success'), +}); + +workspaceRegistry.registerPath({ + method: 'delete', + path: '/workspaces/{id}', + tags: ['Workspace'], + security: [{ [bearerAuth.name]: [] }], + request: { params: Schemas.DeleteWorkspaceSchema.shape.params }, + responses: createApiResponse(messageResponse, 'Success'), +}); + +workspaceRegistry.registerPath({ + method: 'get', + path: '/workspaces/{id}/users', + tags: ['Workspace'], + security: [{ [bearerAuth.name]: [] }], + request: { params: Schemas.GetWorkspaceSchema.shape.params }, + responses: createApiResponse(Users, 'Success'), +}); + +workspaceRegistry.registerPath({ + method: 'get', + path: '/workspaces/{id}/channels', + tags: ['Workspace'], + security: [{ [bearerAuth.name]: [] }], + request: { + params: Schemas.GetWorkspaceSchema.shape.params, + query: Schemas.GetWorkspaceSchema.shape.query, + }, + responses: createApiResponse(Channels, 'Success'), +}); + +workspaceRegistry.registerPath({ + method: 'get', + path: '/workspaces/{id}/threads', + tags: ['Workspace'], + security: [{ [bearerAuth.name]: [] }], + request: { + params: Schemas.GetWorkspaceSchema.shape.params, + query: Schemas.GetWorkspaceSchema.shape.query, + }, + responses: createApiResponse(Threads, 'Success'), +}); diff --git a/src/api/workspace/workspaceController.ts b/src/api/workspace/workspaceController.ts index d3ea945..05a9eea 100644 --- a/src/api/workspace/workspaceController.ts +++ b/src/api/workspace/workspaceController.ts @@ -3,10 +3,6 @@ import { Request, Response } from 'express'; import { CreateWorkspace } from './workspaceModel'; const WorkspaceController = { - getWorkspaces: async (req: Request, res: Response) => { - const ownerId = res.locals.user.id; - res.json({ ownerId }); - }, getWorkspaceById: async (req: Request, res: Response) => { const id = parseInt(req.params.id); res.json({ id }); diff --git a/src/api/workspace/workspaceModel.ts b/src/api/workspace/workspaceModel.ts index a45f4dc..39f7fc9 100644 --- a/src/api/workspace/workspaceModel.ts +++ b/src/api/workspace/workspaceModel.ts @@ -43,6 +43,8 @@ export const UpdateWorkspaceSchema = z.object({ params: z.object({ id: commonValidations.id }), }); +export type UpdateWorkspace = z.infer; + export const DeleteWorkspaceSchema = z.object({ params: z.object({ id: commonValidations.id }), }); diff --git a/src/api/workspace/workspaceRepository.ts b/src/api/workspace/workspaceRepository.ts index 4eb5d3f..e5a1a9b 100644 --- a/src/api/workspace/workspaceRepository.ts +++ b/src/api/workspace/workspaceRepository.ts @@ -1,19 +1,39 @@ -import { CreateWorkspace, Workspace } from '@/api/workspace/workspaceModel'; +import { Knex } from 'knex'; + +import { + CreateWorkspace, + UpdateWorkspace, + Workspace, +} from '@/api/workspace/workspaceModel'; export const workspaceRepository = { - createWorkspace: async (trx: any, workspace: CreateWorkspace): Promise => { + create: async ( + workspace: CreateWorkspace, + trx: Knex.Transaction + ): Promise => { const ids = await trx.insert(workspace).into('workspaces'); - const newWorkspace = await trx.select('*').from('workspaces').where('id', ids[0]).first(); - return newWorkspace; + return trx.select('*').from('workspaces').where('id', ids[0]).first(); }, - findAllUserWorkspaces: async (trx: any, userId: number): Promise => { - return await trx.select('*').from('workspaces').where('ownerId', userId); + update: ( + workspaceId: number, + workspace: UpdateWorkspace, + trx: Knex.Transaction + ): Promise => { + return trx + .update(workspace) + .where('id', workspaceId) + .from('workspaces'); }, - - findById: async (trx: any, id: number): Promise => { - return await trx.select('*').from('workspaces').where('id', id).first(); + getByIds: ( + workspaceIds: number[], + trx: Knex.Transaction + ): Promise => { + return trx.select('*').from('workspaces').whereIn('id', workspaceIds); + }, + getById: (id: number, trx: Knex.Transaction): Promise => { + return trx.select('*').from('workspaces').where('id', id).first(); }, - deleteWorkspace: async (trx: any, id: number): Promise => { - return await trx.delete().from('workspaces').where('id', id); + delete: (id: number, trx: Knex.Transaction): Promise => { + return trx.delete().from('workspaces').where('id', id); }, }; diff --git a/src/api/workspace/workspaceRouter.ts b/src/api/workspace/workspaceRouter.ts index 0ecc04a..03fa1db 100644 --- a/src/api/workspace/workspaceRouter.ts +++ b/src/api/workspace/workspaceRouter.ts @@ -1,149 +1,76 @@ -import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; import express, { Router } from 'express'; import * as Schemas from '@/api/workspace/workspaceModel'; -import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; -import { messageResponse } from '@/common/utils/commonResponses'; import { validateRequest } from '@/common/utils/httpHandlers'; import AuthController from '../auth/authController'; import ChannelController from '../channels/channelController'; -import { Channels } from '../channels/channelModel'; import CoworkersController from '../coworkers/coworkersController'; -import { Threads } from '../threads/threadsModel'; -import { Users } from '../user/userModel'; import WorkspaceController from './workspaceController'; -export const workspaceRegistry = new OpenAPIRegistry(); - -const bearerAuth = workspaceRegistry.registerComponent('securitySchemes', 'bearerAuth', { - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', -}); - -workspaceRegistry.register('Workspace', Schemas.WorkspaceSchema); - export const workspaceRouter: Router = (() => { const router = express.Router(); - workspaceRegistry.registerPath({ - method: 'post', - path: '/workspaces', - tags: ['Workspace'], - security: [{ [bearerAuth.name]: [] }], - request: { - body: { - content: { - 'application/json': { - schema: Schemas.CreateWorkspaceSchema.shape.body, - }, - }, - }, - }, - responses: createApiResponse(Schemas.WorkspaceSchema, 'Success'), - }); - router.post( '/', - [AuthController.authenticate, validateRequest(Schemas.CreateWorkspaceSchema)], + [ + AuthController.authenticate, + validateRequest(Schemas.CreateWorkspaceSchema), + ], WorkspaceController.createWorkspace ); - workspaceRegistry.registerPath({ - method: 'get', - path: '/workspaces/{id}', - tags: ['Workspace'], - security: [{ [bearerAuth.name]: [] }], - request: { params: Schemas.GetWorkspaceSchema.shape.params }, - responses: createApiResponse(Schemas.WorkspaceSchema, 'Success'), - }); - router.get( '/:id', - [AuthController.authenticate, validateRequest(Schemas.GetWorkspaceSchema)], + [ + AuthController.authenticate, + validateRequest(Schemas.GetWorkspaceSchema), + ], WorkspaceController.getWorkspaceById ); - workspaceRegistry.registerPath({ - method: 'patch', - path: '/workspaces/{id}', - tags: ['Workspace'], - security: [{ [bearerAuth.name]: [] }], - request: { - params: Schemas.UpdateWorkspaceSchema.shape.params, - body: { - content: { - 'application/json': { - schema: Schemas.UpdateWorkspaceSchema.shape.body, - }, - }, - }, - }, - responses: createApiResponse(Schemas.WorkspaceSchema, 'Success'), - }); - router.patch( '/:id', - [AuthController.authenticate, validateRequest(Schemas.UpdateWorkspaceSchema)], + [ + AuthController.authenticate, + validateRequest(Schemas.UpdateWorkspaceSchema), + ], WorkspaceController.updateWorkspace ); - workspaceRegistry.registerPath({ - method: 'delete', - path: '/workspaces/{id}', - tags: ['Workspace'], - security: [{ [bearerAuth.name]: [] }], - request: { params: Schemas.DeleteWorkspaceSchema.shape.params }, - responses: createApiResponse(messageResponse, 'Success'), - }); router.delete( '/:id', - [AuthController.authenticate, validateRequest(Schemas.DeleteWorkspaceSchema)], + [ + AuthController.authenticate, + validateRequest(Schemas.DeleteWorkspaceSchema), + ], WorkspaceController.deleteWorkspace ); - workspaceRegistry.registerPath({ - method: 'get', - path: '/workspaces/{id}/users', - tags: ['Workspace'], - security: [{ [bearerAuth.name]: [] }], - request: { params: Schemas.GetWorkspaceSchema.shape.params }, - responses: createApiResponse(Users, 'Success'), - }); router.get( '/:id/users', - [AuthController.authenticate, validateRequest(Schemas.GetWorkspaceSchema)], + [ + AuthController.authenticate, + validateRequest(Schemas.GetWorkspaceSchema), + ], CoworkersController.getWorkspaceUsers ); - workspaceRegistry.registerPath({ - method: 'get', - path: '/workspaces/{id}/channels', - tags: ['Workspace'], - security: [{ [bearerAuth.name]: [] }], - request: { params: Schemas.GetWorkspaceSchema.shape.params, query: Schemas.GetWorkspaceSchema.shape.query }, - responses: createApiResponse(Channels, 'Success'), - }); - router.get( '/:id/channels', - [AuthController.authenticate, validateRequest(Schemas.GetWorkspaceSchema)], + [ + AuthController.authenticate, + validateRequest(Schemas.GetWorkspaceSchema), + ], ChannelController.getWorkspaceChannels ); - workspaceRegistry.registerPath({ - method: 'get', - path: '/workspaces/{id}/threads', - tags: ['Workspace'], - security: [{ [bearerAuth.name]: [] }], - request: { params: Schemas.GetWorkspaceSchema.shape.params, query: Schemas.GetWorkspaceSchema.shape.query }, - responses: createApiResponse(Threads, 'Success'), - }); - router.get( '/:id/threads', - [AuthController.authenticate, validateRequest(Schemas.GetWorkspaceSchema)], + [ + AuthController.authenticate, + validateRequest(Schemas.GetWorkspaceSchema), + ], ChannelController.getWorkspaceThreads ); diff --git a/src/api/workspace/workspaceService.ts b/src/api/workspace/workspaceService.ts new file mode 100644 index 0000000..37a7ceb --- /dev/null +++ b/src/api/workspace/workspaceService.ts @@ -0,0 +1,25 @@ +import { Knex } from 'knex'; + +import { CreateWorkspace, UpdateWorkspace } from './workspaceModel'; +import { workspaceRepository } from './workspaceRepository'; + +const workspaceService = { + create: (workspaceData: CreateWorkspace, trx: Knex.Transaction) => { + return workspaceRepository.create(workspaceData, trx); + }, + getByIds: (workspaceIds: number[], trx: Knex.Transaction) => { + return workspaceRepository.getByIds(workspaceIds, trx); + }, + update: ( + workspaceId: number, + workspaceData: UpdateWorkspace, + trx: Knex.Transaction + ) => { + return workspaceRepository.update(workspaceId, workspaceData, trx); + }, + delete: (workspaceId: number, trx: Knex.Transaction) => { + return workspaceRepository.delete(workspaceId, trx); + }, +}; + +export default workspaceService; diff --git a/src/common/__tests__/entityFactory.ts b/src/common/__tests__/entityFactory.ts index 0368857..5aea40e 100644 --- a/src/common/__tests__/entityFactory.ts +++ b/src/common/__tests__/entityFactory.ts @@ -7,7 +7,11 @@ import db from '../../../db/db'; import { Invite } from '../../api/invites/invitesModel'; import { Thread } from '../../api/threads/threadsModel'; class EntityFactory { - async createFile(id: number, uploadedBy: number, messageId: number): Promise { + async createFile( + id: number, + uploadedBy: number, + messageId: number + ): Promise { const file = { id: id, fileName: 'file1', @@ -16,7 +20,6 @@ class EntityFactory { content: 'content', messageId: messageId, uploadedBy: uploadedBy, - uploadAt: new Date(), }; return await db('files') .insert(file) @@ -41,8 +44,6 @@ class EntityFactory { senderId: senderId, inviteeId: inviteeId, workspaceId: workspaceId, - createdAt: new Date(), - updatedAt: new Date(), expiresAt: new Date(), status: status, }; @@ -69,7 +70,6 @@ class EntityFactory { // entityId: entityId, type: type, isRead: isRead, - createdAt: new Date(), }; return await db('notifications') .insert(notification) @@ -82,17 +82,25 @@ class EntityFactory { .del(); } - async createThread(participantId: number, parentMessageId: number): Promise { + async createThread( + participantId: number, + parentMessageId: number + ): Promise { const thread = { participantId: participantId, parentMessageId: parentMessageId, }; return await db('threads') .insert(thread) - .then(() => db('threads').where('participantId', participantId).first()); + .then(() => + db('threads').where('participantId', participantId).first() + ); } - async deleteThread(participantId: number, parentMessageId: number): Promise { + async deleteThread( + participantId: number, + parentMessageId: number + ): Promise { return await db('threads') .where('participantId', participantId) .andWhere('parentMessageId', parentMessageId) @@ -102,7 +110,6 @@ class EntityFactory { const member = { userId: userId, channelId: channelId, - createdAt: new Date(), }; return await db('members') .insert(member) @@ -114,15 +121,22 @@ class EntityFactory { .andWhere('channelId', channelId) .del(); } - async createCoworker(userId: number, workspaceId: number): Promise { + async createCoworker( + userId: number, + workspaceId: number + ): Promise { const coworker = { userId: userId, workspaceId: workspaceId, - createdAt: new Date(), }; return await db('coworkers') .insert(coworker) - .then(() => db('coworkers').where('userId', userId).andWhere('workspaceId', workspaceId).first()); + .then(() => + db('coworkers') + .where('userId', userId) + .andWhere('workspaceId', workspaceId) + .first() + ); } async deleteCoworkers(userId: number, workspaceId: number): Promise { return await db('coworkers') @@ -144,8 +158,6 @@ class EntityFactory { description: description, ownerId: ownerId, avatarUrl: avatarUrl, - createdAt: new Date(), - updatedAt: new Date(), }; return await db('workspaces') .insert(workspace) @@ -156,7 +168,12 @@ class EntityFactory { .where((b: any) => b.whereIn('id', id)) .del(); } - async createReaction(id: number, userId: number, messageId: number, emoji: string): Promise { + async createReaction( + id: number, + userId: number, + messageId: number, + emoji: string + ): Promise { const reaction = { id: id, messageId: messageId, @@ -176,7 +193,6 @@ class EntityFactory { id: number, senderId: number, channelId: number, - workspaceId: number, parentMessageId: number | null, content: string ): Promise { @@ -184,11 +200,8 @@ class EntityFactory { id: id, senderId: senderId, channelId: channelId, - workspaceId: workspaceId, parentMessageId: parentMessageId, content: content, - createdAt: new Date(), - updatedAt: new Date(), }; return db('messages') .insert(message) @@ -214,8 +227,6 @@ class EntityFactory { name: name, description: description, type: type, - createdAt: new Date(), - updatedAt: new Date(), }; return await db('channels') .insert(channel) @@ -226,7 +237,13 @@ class EntityFactory { .where((b: any) => b.whereIn('id', id)) .del(); } - async createUser(id: number, username: string, email: string, name: string, password: string): Promise { + async createUser( + id: number, + username: string, + email: string, + name: string, + password: string + ): Promise { const user = { id: id, username: username, @@ -235,8 +252,6 @@ class EntityFactory { password: password, avatarUrl: '', status: 'online', - createdAt: new Date(), - updatedAt: new Date(), }; return await db('users') .insert(user) diff --git a/src/common/__tests__/errorHandler.test.ts b/src/common/__tests__/errorHandler.test.ts index 3f142a3..56f071e 100644 --- a/src/common/__tests__/errorHandler.test.ts +++ b/src/common/__tests__/errorHandler.test.ts @@ -19,12 +19,16 @@ describe('Error Handler Middleware', () => { }); app.use(errorHandler()); - app.use('*', (req, res) => res.status(StatusCodes.NOT_FOUND).send('Not Found')); + app.use('*', (req, res) => + res.status(StatusCodes.NOT_FOUND).send('Not Found') + ); }); describe('Handling unknown routes', () => { it('returns 404 for unknown routes', async () => { - const response = await request(app).get('/this-route-does-not-exist'); + const response = await request(app).get( + '/this-route-does-not-exist' + ); expect(response.status).toBe(StatusCodes.NOT_FOUND); }); }); diff --git a/src/common/__tests__/mocks.ts b/src/common/__tests__/mocks.ts index 6e6dd99..c94b1ef 100644 --- a/src/common/__tests__/mocks.ts +++ b/src/common/__tests__/mocks.ts @@ -6,6 +6,7 @@ export const mockedTxn: any = { select: vi.fn().mockReturnThis(), from: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), + whereIn: vi.fn().mockReturnThis(), insert: vi.fn().mockReturnThis(), into: vi.fn().mockReturnThis(), first: vi.fn(), diff --git a/src/common/__tests__/requestLogger.test.ts b/src/common/__tests__/requestLogger.test.ts index 406dca1..d86d796 100644 --- a/src/common/__tests__/requestLogger.test.ts +++ b/src/common/__tests__/requestLogger.test.ts @@ -10,7 +10,9 @@ describe('Request Logger Middleware', () => { beforeAll(() => { app.use(requestLogger()); - app.get('/success', (req, res) => res.status(StatusCodes.OK).send('Success')); + app.get('/success', (req, res) => + res.status(StatusCodes.OK).send('Success') + ); app.get('/redirect', (req, res) => res.redirect('/success')); app.get('/error', () => { throw new Error('Test error'); @@ -26,7 +28,9 @@ describe('Request Logger Middleware', () => { it('checks existing request id', async () => { const requestId = 'test-request-id'; - const response = await request(app).get('/success').set('X-Request-Id', requestId); + const response = await request(app) + .get('/success') + .set('X-Request-Id', requestId); expect(response.status).toBe(StatusCodes.OK); }); }); diff --git a/src/common/middleware/errorHandler.ts b/src/common/middleware/errorHandler.ts index 95fcc4f..d857428 100644 --- a/src/common/middleware/errorHandler.ts +++ b/src/common/middleware/errorHandler.ts @@ -1,13 +1,53 @@ -import { ErrorRequestHandler, RequestHandler } from 'express'; -import { StatusCodes } from 'http-status-codes'; +import { + ErrorRequestHandler, + Request, + RequestHandler, + Response, +} from 'express'; +import { getReasonPhrase, StatusCodes } from 'http-status-codes'; + +import { handleServiceResponse } from '../utils/httpHandlers'; + +export class CustomError extends Error { + constructor( + public message: string, + public statusCode: StatusCodes, + public customProperty?: any + ) { + super(message); + this.statusCode = statusCode; + this.customProperty = customProperty; + } +} const unexpectedRequest: RequestHandler = (_req, res) => { res.sendStatus(StatusCodes.NOT_FOUND); }; -const addErrorToRequestLog: ErrorRequestHandler = (err, _req, res, next) => { - res.locals.err = err; - next(err); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const addErrorToRequestLog: ErrorRequestHandler = ( + err, + _req: Request, + res: Response +) => { + res.trx.rollback(); + if (err instanceof CustomError) { + handleServiceResponse( + res, + { err: err.customProperty }, + err.message, + err.statusCode + ); + return; + } + handleServiceResponse( + res, + { + status: getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), + err: err, + }, + StatusCodes.INTERNAL_SERVER_ERROR + ); }; export default () => [unexpectedRequest, addErrorToRequestLog]; diff --git a/src/common/middleware/requestLogger.ts b/src/common/middleware/requestLogger.ts index 61fac42..78d5224 100644 --- a/src/common/middleware/requestLogger.ts +++ b/src/common/middleware/requestLogger.ts @@ -33,7 +33,8 @@ const requestLogger = (options?: Options): RequestHandler[] => { customLogLevel, customSuccessMessage, customReceivedMessage: (req) => `request received: ${req.method}`, - customErrorMessage: (_req, res) => `request errored with status code: ${res.statusCode}`, + customErrorMessage: (_req, res) => + `request errored with status code: ${res.statusCode}`, customAttributeKeys, ...options, }; @@ -67,19 +68,31 @@ const responseBodyMiddleware: RequestHandler = (_req, res, next) => { next(); }; -const customLogLevel = (_req: IncomingMessage, res: ServerResponse, err?: Error): LevelWithSilent => { - if (err || res.statusCode >= StatusCodes.INTERNAL_SERVER_ERROR) return LogLevel.Error; +const customLogLevel = ( + _req: IncomingMessage, + res: ServerResponse, + err?: Error +): LevelWithSilent => { + if (err || res.statusCode >= StatusCodes.INTERNAL_SERVER_ERROR) + return LogLevel.Error; if (res.statusCode >= StatusCodes.BAD_REQUEST) return LogLevel.Warn; if (res.statusCode >= StatusCodes.MULTIPLE_CHOICES) return LogLevel.Silent; return LogLevel.Info; }; -const customSuccessMessage = (req: IncomingMessage, res: ServerResponse) => { - if (res.statusCode === StatusCodes.NOT_FOUND) return getReasonPhrase(StatusCodes.NOT_FOUND); +const customSuccessMessage = ( + req: IncomingMessage, + res: ServerResponse +) => { + if (res.statusCode === StatusCodes.NOT_FOUND) + return getReasonPhrase(StatusCodes.NOT_FOUND); return `${req.method} completed`; }; -const genReqId = (req: IncomingMessage, res: ServerResponse) => { +const genReqId = ( + req: IncomingMessage, + res: ServerResponse +) => { const existingID = req.id ?? req.headers['x-request-id']; if (existingID) return existingID; const id = randomUUID(); diff --git a/src/common/middleware/trxHandler.ts b/src/common/middleware/trxHandler.ts new file mode 100644 index 0000000..12eb6e7 --- /dev/null +++ b/src/common/middleware/trxHandler.ts @@ -0,0 +1,7 @@ +import db from 'db/db'; +import { NextFunction, Request, Response } from 'express'; + +export default async (_req: Request, res: Response, next: NextFunction) => { + res.trx = await db.transaction(); + next(); +}; diff --git a/src/common/models/serviceResponse.ts b/src/common/models/serviceResponse.ts index 66aa595..a5c8d53 100644 --- a/src/common/models/serviceResponse.ts +++ b/src/common/models/serviceResponse.ts @@ -11,7 +11,12 @@ export class ServiceResponse { responseObject: T | null; statusCode: number; - constructor(status: ResponseStatus, message: string, responseObject: T | null, statusCode: number) { + constructor( + status: ResponseStatus, + message: string, + responseObject: T | null, + statusCode: number + ) { this.success = status === ResponseStatus.Success; this.message = message; this.responseObject = responseObject; diff --git a/src/common/utils/httpHandlers.ts b/src/common/utils/httpHandlers.ts index 42f05d3..fe574a7 100644 --- a/src/common/utils/httpHandlers.ts +++ b/src/common/utils/httpHandlers.ts @@ -1,27 +1,97 @@ import { NextFunction, Request, Response } from 'express'; import { StatusCodes } from 'http-status-codes'; -import { ZodError, ZodSchema } from 'zod'; +import { ZodSchema } from 'zod'; -import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; +import userService from '../../api/user/userService'; -export const handleServiceResponse = (serviceResponse: ServiceResponse, response: Response) => { - return response.status(serviceResponse.statusCode).send(serviceResponse); +type AsyncFunction = ( + req: Request, + res: Response, + next: NextFunction +) => Promise; + +export const asyncHandler = (fn: AsyncFunction) => { + return async (req: Request, res: Response, next: NextFunction) => { + try { + await fn(req, res, next); + } catch (err) { + next(err); + } + }; +}; + +export const handleServiceResponse = ( + response: Response, + data: any, + message: any, + status: StatusCodes = StatusCodes.OK +) => { + if (!response.trx.isCompleted()) response.trx.commit(); + return response.status(status).json({ message: message, data: data }); }; -export const validateRequest = (schema: ZodSchema) => (req: Request, res: Response, next: NextFunction) => { - try { - const validatedReq = schema.parse({ - body: req.body, - query: req.query, - params: req.params, - }); - req.body = validatedReq.body; - req.query = validatedReq.query; - req.params = validatedReq.params; +export const validateRequest = + (schema: ZodSchema) => + (req: Request, res: Response, next: NextFunction) => { + try { + const validatedReq = schema.parse({ + body: req.body, + query: req.query, + params: req.params, + }); + req.body = validatedReq.body; + req.query = validatedReq.query; + req.params = validatedReq.params; + next(); + } catch (err) { + handleServiceResponse( + res, + { err }, + 'validation Error', + StatusCodes.BAD_REQUEST + ); + } + }; + +export enum RESOURCES { + USER, + WORKSPACE, + CHANNEL, + MESSAGE, + INVITE, + NOTIFICATION, + REACTION, +} +export const checkIdExists = (idString: string, resource: RESOURCES) => { + return async (req: Request, res: Response, next: NextFunction) => { + const id = parseInt(req.params[idString]); + if (isNaN(id)) { + handleServiceResponse( + res, + { err: 'Invalid Id' }, + 'Id should be a number', + StatusCodes.BAD_REQUEST + ); + } + + let notFound = false; + switch (resource) { + case RESOURCES.USER: + if (!(await userService.getById(id, res.trx))) { + notFound = true; + } + break; + // TODO: Add more cases for other resources as needed + } + + if (notFound) { + handleServiceResponse( + res, + { err: 'Not found' }, + 'Resource not found', + StatusCodes.NOT_FOUND + ); + } next(); - } catch (err) { - const errorMessage = `Invalid input: ${(err as ZodError).errors.map((e) => e.message).join(', ')}`; - const statusCode = StatusCodes.BAD_REQUEST; - res.status(statusCode).send(new ServiceResponse(ResponseStatus.Failed, errorMessage, null, statusCode)); - } + }; }; diff --git a/src/server.ts b/src/server.ts index ece073d..0a5cce9 100644 --- a/src/server.ts +++ b/src/server.ts @@ -19,6 +19,7 @@ import { filesRouter } from './api/files/filesRoutes'; import { messagesRouter } from './api/messages/messageRouter'; import { notificationRouter } from './api/notifications/notificationsRoutes'; import { reactionsRouter } from './api/reactions/reactionsRouter'; +import trxLoader from './common/middleware/trxHandler'; import { connectSocket } from './sockets/socket'; const logger = pino({ name: 'server start' }); @@ -27,19 +28,14 @@ const httpServer = createServer(app); connectSocket(httpServer); -// Set the application to trust the reverse proxy -app.set('trust proxy', true); - -// Middlewares app.use(cors({ origin: env.CORS_ORIGIN, credentials: true })); app.use(express.json()); app.use(helmet()); app.use(rateLimiter); -// Request logging app.use(requestLogger()); -// Routes +app.use(trxLoader); app.use('/health-check', healthCheckRouter); app.use('/auth', authRouter); app.use('/users', userRouter); @@ -50,10 +46,7 @@ app.use('/reactions', reactionsRouter); app.use('/files', filesRouter); app.use('/notifications', notificationRouter); -// Swagger UI app.use(openAPIRouter); - -// Error handlers app.use(errorHandler()); export { httpServer as app, logger }; diff --git a/src/sockets/__tests__/basicSocket.test.ts b/src/sockets/__tests__/basicSocket.test.ts index f4892b1..efda69b 100644 --- a/src/sockets/__tests__/basicSocket.test.ts +++ b/src/sockets/__tests__/basicSocket.test.ts @@ -1,6 +1,9 @@ import { type Socket as ClientSocket } from 'socket.io-client'; -import { SocketClient as ServerSocket, SocketServer as Server } from '../sockets.types'; +import { + SocketClient as ServerSocket, + SocketServer as Server, +} from '../sockets.types'; import { closeSocket, initSocket } from './setupSocketTesting'; describe('basic socket', () => { diff --git a/src/sockets/__tests__/setupSocketTesting.ts b/src/sockets/__tests__/setupSocketTesting.ts index fa1fe62..9f6666e 100644 --- a/src/sockets/__tests__/setupSocketTesting.ts +++ b/src/sockets/__tests__/setupSocketTesting.ts @@ -4,10 +4,19 @@ import { io as ioc, type Socket as ClientSocket } from 'socket.io-client'; import { app } from '../../server'; import { connectSocket } from '../socket'; -import { C2S, S2C, SocketClient as ServerSocket, SocketServer as Server } from '../sockets.types'; +import { + C2S, + S2C, + SocketClient as ServerSocket, + SocketServer as Server, +} from '../sockets.types'; export function initSocket() { - return new Promise<{ io: Server; serverSocket: ServerSocket; clientSocket: ClientSocket }>((done) => { + return new Promise<{ + io: Server; + serverSocket: ServerSocket; + clientSocket: ClientSocket; + }>((done) => { let serverSocket: ServerSocket, clientSocket: ClientSocket; const httpServer = createServer(app); diff --git a/src/sockets/socket.ts b/src/sockets/socket.ts index 260371d..ccc1ee6 100644 --- a/src/sockets/socket.ts +++ b/src/sockets/socket.ts @@ -6,7 +6,14 @@ import { User } from '../api/user/userModel'; import { userService } from '../api/user/userService'; import { env } from '../common/utils/envConfig'; import roomsEvents from './roomsEvents'; -import { C2S, data, S2C, ServerEvents, SocketClient, SocketServer } from './sockets.types'; +import { + C2S, + data, + S2C, + ServerEvents, + SocketClient, + SocketServer, +} from './sockets.types'; // refer to https://socket.io/docs/v4/server-application-structure#each-file-registers-its-own-event-handlers // to know more about the structure of this files. @@ -15,7 +22,9 @@ import { C2S, data, S2C, ServerEvents, SocketClient, SocketServer } from './sock // to know more about the types used in this file, and how to define them. export function connectSocket(server: HttpServer) { - const io = new Server(server, { cors: { origin: '*', methods: ['GET', 'POST'] } }); + const io = new Server(server, { + cors: { origin: '*', methods: ['GET', 'POST'] }, + }); if (env.NODE_ENV === 'development') { io.use( authorize({ diff --git a/src/sockets/sockets.types.ts b/src/sockets/sockets.types.ts index 4577838..3522d23 100644 --- a/src/sockets/sockets.types.ts +++ b/src/sockets/sockets.types.ts @@ -3,7 +3,7 @@ import { Server, Socket } from 'socket.io'; import { Channel, DeleteChannelData } from '../api/channels/channelModel'; import { Coworker, CoworkerData } from '../api/coworkers/coworkersModel'; import { MemberData } from '../api/members/memberModel'; -import { DeleteMessageData, Message } from '../api/messages/messageModel'; +import { deleteMessageData, Message } from '../api/messages/messageModel'; import { Reaction } from '../api/reactions/reactionModel'; import { User } from '../api/user/userModel'; import { Workspace } from '../api/workspace/workspaceModel'; @@ -12,12 +12,10 @@ import { Workspace } from '../api/workspace/workspaceModel'; export interface S2C { emitHi: (greeting: string) => void; // simple pings - //TODO: update this when repository layer is implemented. - // Messages newMessage: (message: Message) => void; updateMessage: (message: Message) => void; - deleteMessage: (message: DeleteMessageData) => void; + deleteMessage: (message: deleteMessageData) => void; // channels addChannel: (channel: Channel) => void; From d14c68bc60e8a533a87e3beabfcabe095864799d Mon Sep 17 00:00:00 2001 From: Abdallah Elhadad Date: Wed, 15 May 2024 06:13:48 +0300 Subject: [PATCH 2/2] revert: socket auth --- src/sockets/socket.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/sockets/socket.ts b/src/sockets/socket.ts index ccc1ee6..e2ace2c 100644 --- a/src/sockets/socket.ts +++ b/src/sockets/socket.ts @@ -2,8 +2,6 @@ import { authorize } from '@thream/socketio-jwt'; import { Server as HttpServer } from 'http'; import { Server } from 'socket.io'; -import { User } from '../api/user/userModel'; -import { userService } from '../api/user/userService'; import { env } from '../common/utils/envConfig'; import roomsEvents from './roomsEvents'; import { @@ -29,11 +27,12 @@ export function connectSocket(server: HttpServer) { io.use( authorize({ secret: env.JWT_SECRET, - onAuthentication: async (user: User) => { - const res = await userService.findById(user.id); - if (!res) throw new Error('User not found'); - return res.responseObject; - }, + //TODO: mmmm, need a trx?? + // onAuthentication: async (reqUser: User) => { + // const user = await userService.getById(reqUser.id); + // if (!user) throw new Error('User not found'); + // return user; + // }, }) ); }