diff --git a/prisma/migrations/20211229101953_init_4/migration.sql b/prisma/migrations/20211229101953_init_4/migration.sql new file mode 100644 index 0000000..5c695a0 --- /dev/null +++ b/prisma/migrations/20211229101953_init_4/migration.sql @@ -0,0 +1,5 @@ +-- DropIndex +DROP INDEX "OAuth_userId_key"; + +-- DropIndex +DROP INDEX "RefreshToken_userId_key"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b9338eb..ce7669b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -44,7 +44,7 @@ model RefreshToken { token String @db.Text createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt - userId Int @unique + userId Int user User? @relation(fields: [userId], references: [id]) } @@ -54,7 +54,7 @@ model OAuth { provider String @db.VarChar(20) createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt - userId Int @unique + userId Int user User? @relation(fields: [userId], references: [id]) } diff --git a/src/models/Message.ts b/src/models/Message.ts index fbe6d59..952744b 100644 --- a/src/models/Message.ts +++ b/src/models/Message.ts @@ -1,3 +1,4 @@ +import { Message } from '@prisma/client' import { Type } from '@sinclair/typebox' import { date, id } from './utils.js' @@ -21,3 +22,14 @@ export const messageSchema = { memberId: id, channelId: id } + +export const messageExample: Message = { + id: 1, + value: 'Hello, world!', + type: 'text', + mimetype: 'text/plain', + createdAt: new Date(), + updatedAt: new Date(), + memberId: 1, + channelId: 1 +} diff --git a/src/services/channels/[channelId]/messages/__test__/get.test.ts b/src/services/channels/[channelId]/messages/__test__/get.test.ts new file mode 100644 index 0000000..dfd6335 --- /dev/null +++ b/src/services/channels/[channelId]/messages/__test__/get.test.ts @@ -0,0 +1,76 @@ +import { application } from '../../../../../application.js' +import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js' +import { prismaMock } from '../../../../../__test__/setup.js' +import { channelExample } from '../../../../../models/Channel.js' +import { userExample } from '../../../../../models/User.js' +import { memberExample } from '../../../../../models/Member.js' +import { messageExample } from '../../../../../models/Message.js' + +describe('GET /channels/[channelId]/messages', () => { + it('succeeds', async () => { + prismaMock.channel.findUnique.mockResolvedValue(channelExample) + prismaMock.member.findFirst.mockResolvedValue({ + ...memberExample, + user: userExample + } as any) + prismaMock.message.findMany.mockResolvedValue([messageExample]) + const { accessToken } = await authenticateUserTest() + const response = await application.inject({ + method: 'GET', + url: `/channels/${channelExample.id}/messages`, + headers: { + authorization: `Bearer ${accessToken}` + } + }) + const responseJson = response.json() + expect(response.statusCode).toEqual(200) + expect(responseJson.length).toEqual(1) + expect(responseJson[0].id).toEqual(messageExample.id) + expect(responseJson[0].value).toEqual(messageExample.value) + expect(responseJson[0].type).toEqual(messageExample.type) + expect(responseJson[0].mimetype).toEqual(messageExample.mimetype) + expect(responseJson[0].member.id).toEqual(memberExample.id) + expect(responseJson[0].member.isOwner).toEqual(memberExample.isOwner) + expect(responseJson[0].member.user.id).toEqual(userExample.id) + expect(responseJson[0].member.user.name).toEqual(userExample.name) + }) + + it('fails with not found channel', async () => { + prismaMock.channel.findUnique.mockResolvedValue(null) + const { accessToken } = await authenticateUserTest() + const response = await application.inject({ + method: 'GET', + url: `/channels/${channelExample.id}/messages`, + headers: { + authorization: `Bearer ${accessToken}` + } + }) + const responseJson = response.json() + expect(response.statusCode).toEqual(404) + expect(responseJson.message).toEqual('Channel not found') + }) + + it('fails with not found member', async () => { + prismaMock.channel.findUnique.mockResolvedValue(channelExample) + prismaMock.member.findFirst.mockResolvedValue(null) + const { accessToken } = await authenticateUserTest() + const response = await application.inject({ + method: 'GET', + url: `/channels/${channelExample.id}/messages`, + headers: { + authorization: `Bearer ${accessToken}` + } + }) + const responseJson = response.json() + expect(response.statusCode).toEqual(404) + expect(responseJson.message).toEqual('Channel not found') + }) + + it('fails with unauthenticated user', async () => { + const response = await application.inject({ + method: 'GET', + url: '/channels/1/messages' + }) + expect(response.statusCode).toEqual(401) + }) +}) diff --git a/src/services/channels/[channelId]/messages/get.ts b/src/services/channels/[channelId]/messages/get.ts new file mode 100644 index 0000000..76ab81e --- /dev/null +++ b/src/services/channels/[channelId]/messages/get.ts @@ -0,0 +1,121 @@ +import { Static, Type } from '@sinclair/typebox' +import { FastifyPluginAsync, FastifySchema } from 'fastify' + +import prisma from '../../../../tools/database/prisma.js' +import { fastifyErrors } from '../../../../models/utils.js' +import authenticateUser from '../../../../tools/plugins/authenticateUser.js' +import { messageSchema } from '../../../../models/Message.js' +import { memberSchema } from '../../../../models/Member.js' +import { userPublicWithoutSettingsSchema } from '../../../../models/User.js' +import { + getPaginationOptions, + queryPaginationObjectSchema +} from '../../../../tools/database/pagination.js' +import { channelSchema } from '../../../../models/Channel.js' + +type QuerySchemaType = Static + +const parametersSchema = Type.Object({ + channelId: channelSchema.id +}) + +type Parameters = Static + +const getServiceSchema: FastifySchema = { + description: 'GET all the messages of a channel by its id.', + tags: ['messages'] as string[], + security: [ + { + bearerAuth: [] + } + ] as Array<{ [key: string]: [] }>, + params: parametersSchema, + querystring: queryPaginationObjectSchema, + response: { + 200: Type.Array( + Type.Object({ + ...messageSchema, + member: Type.Object({ + ...memberSchema, + user: Type.Object(userPublicWithoutSettingsSchema) + }) + }) + ), + 400: fastifyErrors[400], + 401: fastifyErrors[401], + 403: fastifyErrors[403], + 404: fastifyErrors[404], + 500: fastifyErrors[500] + } +} as const + +export const getMessagesByChannelIdService: FastifyPluginAsync = async ( + fastify +) => { + await fastify.register(authenticateUser) + + fastify.route<{ + Params: Parameters + Querystring: QuerySchemaType + }>({ + method: 'GET', + url: '/channels/:channelId/messages', + schema: getServiceSchema, + handler: async (request, reply) => { + if (request.user == null) { + throw fastify.httpErrors.forbidden() + } + const { channelId } = request.params + const channel = await prisma.channel.findUnique({ + where: { id: channelId } + }) + if (channel == null) { + throw fastify.httpErrors.notFound('Channel not found') + } + const memberCheck = await prisma.member.findFirst({ + where: { guildId: channel.guildId, userId: request.user.current.id } + }) + if (memberCheck == null) { + throw fastify.httpErrors.notFound('Channel not found') + } + const messagesRequest = await prisma.message.findMany({ + ...getPaginationOptions(request.query), + orderBy: { createdAt: 'asc' }, + where: { channelId } + }) + const messages = await Promise.all( + messagesRequest.map(async (message) => { + const member = await prisma.member.findFirst({ + where: { id: message.memberId }, + include: { + user: { + select: { + id: true, + name: true, + logo: true, + status: true, + biography: true, + website: true, + createdAt: true, + updatedAt: true + } + } + } + }) + return { + ...message, + member: { + ...member, + user: { + ...member?.user, + email: null + } + } + } + }) + ) + reply.statusCode = 200 + return messages + } + }) +} diff --git a/src/services/channels/index.ts b/src/services/channels/index.ts index 75e903a..cee77fb 100644 --- a/src/services/channels/index.ts +++ b/src/services/channels/index.ts @@ -1,7 +1,9 @@ import { FastifyPluginAsync } from 'fastify' import { getChannelByIdService } from './[channelId]/get' +import { getMessagesByChannelIdService } from './[channelId]/messages/get' export const channelsService: FastifyPluginAsync = async (fastify) => { await fastify.register(getChannelByIdService) + await fastify.register(getMessagesByChannelIdService) }