Skip to content

Commit 9598adf

Browse files
Merge PR: Fix chats creation & allow get by username
2 parents 9811e06 + 546619c commit 9598adf

File tree

3 files changed

+97
-47
lines changed

3 files changed

+97
-47
lines changed

src/api/v1/chats/chats.router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ chatsRouter.get('/', Middlewares.authValidator, async (req, res) => {
3333

3434
chatsRouter.get('/members/:profileId', Middlewares.authValidator, async (req, res) => {
3535
const userId = Utils.getCurrentUserIdFromReq(req)!;
36-
const chats = await Service.getUserChatsByMemberProfileId(userId, req.params.profileId);
36+
const chats = await Service.getUserChatsByMember(userId, req.params.profileId);
3737
res.json(chats);
3838
});
3939

src/api/v1/chats/chats.service.ts

Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import db from '@/lib/db';
1010

1111
const createPaginationArgs = (
1212
filters: Types.BasePaginationFilters & { orderBy: 'createdAt' | 'updatedAt' },
13-
limit = 10
13+
limit = 10,
1414
) => {
1515
return {
1616
...(filters.cursor ? { cursor: { id: filters.cursor }, skip: 1 } : {}),
@@ -36,7 +36,7 @@ const createChatAggregation = () => {
3636

3737
const createCurrentProfileChatArgs = (
3838
currentProfile: Prisma.ProfileGetPayload<{ include: { user: true } }>,
39-
now: Date
39+
now: Date,
4040
) => {
4141
return {
4242
profileName: currentProfile.user.username,
@@ -48,7 +48,7 @@ const createCurrentProfileChatArgs = (
4848

4949
const prepareChat = (
5050
chat: Prisma.ChatGetPayload<{ include: ReturnType<typeof createChatAggregation> }>,
51-
currentProfile: Profile
51+
currentProfile: Profile,
5252
) => {
5353
if (currentProfile.tangible) {
5454
chat.profiles = chat.profiles.map((p) => {
@@ -59,7 +59,7 @@ const prepareChat = (
5959
});
6060
} else {
6161
chat.profiles = chat.profiles.map((p) =>
62-
p.profileId === currentProfile.id ? p : { ...p, lastSeenAt: null }
62+
p.profileId === currentProfile.id ? p : { ...p, lastSeenAt: null },
6363
);
6464
}
6565
return chat;
@@ -69,23 +69,22 @@ export const createChat = async (
6969
user: Types.PublicUser,
7070
data: Schema.ValidChat,
7171
imageData?: Types.ImageFullData,
72-
uploadedImage?: Storage.UploadedImageData
72+
uploadedImage?: Storage.UploadedImageData,
7373
) => {
7474
return await Utils.handleDBKnownErrors(
7575
db.$transaction(async (tx) => {
76-
// Get current user's profile
77-
const currentProfile = await tx.profile.findUnique({
78-
where: { userId: user.id },
76+
const allProfiles = await tx.profile.findMany({
77+
where: { OR: [{ id: { in: data.profiles } }, { userId: user.id }] },
7978
include: { user: true },
79+
distinct: 'id',
8080
});
81+
const otherProfiles: typeof allProfiles = [];
82+
const currentProfile = allProfiles.reduce<(typeof allProfiles)[number] | null>(
83+
(acc, curr) => (curr.user.id === user.id ? curr : (otherProfiles.push(curr), acc)),
84+
null,
85+
);
8186
if (!currentProfile) throw new AppNotFoundError('Profile not found');
8287
const currentProfileChatArgs = createCurrentProfileChatArgs(currentProfile, new Date());
83-
// Get all the other profiles
84-
const otherProfiles = await tx.profile.findMany({
85-
where: { id: { in: data.profiles } },
86-
include: { user: true },
87-
distinct: 'id',
88-
});
8988
const chatAggregation = createChatAggregation();
9089
const messageArgs = {
9190
profileName: currentProfile.user.username,
@@ -101,19 +100,15 @@ export const createChat = async (
101100
profileId: currentProfile.id,
102101
};
103102
// Find chats with the same owner and same group of profiles (there should be at most one)
104-
const existentChats = await tx.chat.findMany({
105-
where: {
106-
managers: { some: { profileId: currentProfile.id, role: 'OWNER' } },
107-
profiles: {
108-
every: {
109-
profileName: {
110-
in: [currentProfile.user.username, ...otherProfiles.map((p) => p.user.username)],
111-
},
112-
},
103+
const existentChats = (
104+
await tx.chat.findMany({
105+
where: {
106+
managers: { some: { profileId: currentProfile.id, role: 'OWNER' } },
107+
profiles: { every: { profileName: { in: allProfiles.map((p) => p.user.username) } } },
113108
},
114-
},
115-
include: chatAggregation,
116-
});
109+
include: { ...chatAggregation, _count: { select: { profiles: true } } },
110+
})
111+
).filter((c) => c._count.profiles === allProfiles.length); // Filter out the profiles with a mismatched count.
117112
// Upsert a chat
118113
let chat: Prisma.ChatGetPayload<{ include: typeof chatAggregation }>;
119114
if (existentChats.length > 0) {
@@ -163,7 +158,7 @@ export const createChat = async (
163158
});
164159
}
165160
return prepareChat(chat, currentProfile);
166-
})
161+
}),
167162
);
168163
};
169164

@@ -187,13 +182,13 @@ export const deleteChat = async (userId: User['id'], chatId: Chat['id']) => {
187182
}
188183
}
189184
}
190-
})
185+
}),
191186
);
192187
};
193188

194189
export const getUserChats = async (
195190
userId: User['id'],
196-
filters: Types.BasePaginationFilters = {}
191+
filters: Types.BasePaginationFilters = {},
197192
) => {
198193
return await Utils.handleDBKnownErrors(
199194
db.$transaction(async (tx) => {
@@ -214,7 +209,7 @@ export const getUserChats = async (
214209
include: createChatAggregation(),
215210
});
216211
return chats.map((chat) => prepareChat(chat, currentProfile));
217-
})
212+
}),
218213
);
219214
};
220215

@@ -242,19 +237,24 @@ export const getUserChatById = async (userId: User['id'], chatId: Chat['id']) =>
242237
throw new AppNotFoundError('Chat not found');
243238
}
244239
return prepareChat(chat, currentProfile);
245-
})
240+
}),
246241
);
247242
};
248243

249-
export const getUserChatsByMemberProfileId = async (
250-
userId: User['id'],
251-
profileId: Profile['id']
252-
) => {
244+
export const getUserChatsByMember = async (userId: User['id'], memberIdOrUsername: string) => {
253245
return await Utils.handleDBKnownErrors(
254246
db.$transaction(async (tx) => {
247+
let profileId: Profile['id'];
255248
try {
256-
const memberProfile = await tx.profile.findUnique({ where: { id: profileId } });
249+
const memberUser = await tx.user.findUnique({
250+
where: { username: memberIdOrUsername },
251+
include: { profile: true },
252+
});
253+
const memberProfile =
254+
memberUser?.profile ??
255+
(await tx.profile.findUnique({ where: { id: memberIdOrUsername } }));
257256
if (!memberProfile) throw new AppNotFoundError('Chat member profile not found');
257+
profileId = memberProfile.id;
258258
} catch {
259259
throw new AppNotFoundError('Chat member profile not found');
260260
}
@@ -281,14 +281,14 @@ export const getUserChatsByMemberProfileId = async (
281281
});
282282
if (!currentProfileWithChats) throw new AppNotFoundError('Profile not found');
283283
return currentProfileWithChats.chats.map((c) => prepareChat(c.chat, currentProfileWithChats));
284-
})
284+
}),
285285
);
286286
};
287287

288288
export const getUserChatMessages = async (
289289
userId: User['id'],
290290
chatId: Chat['id'],
291-
filters: Types.BasePaginationFilters = {}
291+
filters: Types.BasePaginationFilters = {},
292292
) => {
293293
return await Utils.handleDBKnownErrors(
294294
db.$transaction(async (tx) => {
@@ -315,14 +315,14 @@ export const getUserChatMessages = async (
315315
where: { chat: { id: chatId, profiles: { some: { profileId } } } },
316316
include: createMessageAggregation(),
317317
});
318-
})
318+
}),
319319
);
320320
};
321321

322322
export const getUserChatMessageById = async (
323323
userId: User['id'],
324324
chatId: Chat['id'],
325-
msgId: Message['id']
325+
msgId: Message['id'],
326326
) => {
327327
return await Utils.handleDBKnownErrors(
328328
db.$transaction(async (tx) => {
@@ -343,7 +343,7 @@ export const getUserChatMessageById = async (
343343
});
344344
if (!message) throw new AppNotFoundError('Message not found');
345345
return message;
346-
})
346+
}),
347347
);
348348
};
349349

@@ -352,7 +352,7 @@ export const createUserChatMessage = async (
352352
chatId: Chat['id'],
353353
{ body }: Schema.ValidMessage,
354354
imageData?: Types.ImageFullData,
355-
uploadedImage?: Storage.UploadedImageData
355+
uploadedImage?: Storage.UploadedImageData,
356356
) => {
357357
return await Utils.handleDBKnownErrors(
358358
db.$transaction(async (tx) => {
@@ -393,7 +393,7 @@ export const createUserChatMessage = async (
393393
},
394394
include: createMessageAggregation(),
395395
});
396-
})
396+
}),
397397
);
398398
};
399399

@@ -411,7 +411,7 @@ export const updateProfileChatLastSeenDate = async (userId: User['id'], chatId:
411411
where: { profileName_chatId: { profileName, chatId } },
412412
data: { lastSeenAt: now },
413413
});
414-
})
414+
}),
415415
);
416416
return now;
417417
};

src/tests/api/v1/chats.int.test.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,37 @@ describe('Chats endpoints', async () => {
322322
}
323323
assertReceivedDateUpdated(resChat1, dbChat1, userOneData.username);
324324
assertReceivedDateUpdated(resChat2, dbChat2, userOneData.username);
325+
await db.chat.delete({ where: { id: dbChat2.id } });
326+
});
327+
328+
it('should respond all the current user chats that include the given member`s', async () => {
329+
const memberUsername = dbUserTwo.username;
330+
const dbChatId1 = dbChats.find((c) =>
331+
c.profiles.some((p) => p.profileName === memberUsername),
332+
)!.id;
333+
const dbChat1 = (await db.chat.findUnique({
334+
where: { id: dbChatId1 },
335+
include: { profiles: true },
336+
}))!;
337+
const dbChat2 = await createChat('Hello #2', [dbUserOne, dbUserTwo, dbXUser]);
338+
const { authorizedApi } = await prepForAuthorizedTest(userOneData);
339+
const res = await authorizedApi.get(`${CHATS_URL}/members/${memberUsername}`);
340+
const resBody = res.body as ChatFullData[];
341+
const resChat1 = resBody.find((c) => c.id === dbChat1.id)!;
342+
const resChat2 = resBody.find((c) => c.id === dbChat2.id)!;
343+
expect(res.statusCode).toBe(200);
344+
expect(res.type).toMatch(/json/);
345+
expect(resBody).toBeInstanceOf(Array);
346+
expect(resBody).toHaveLength(2);
347+
expect(resBody.every((c) => c.id === dbChat1.id || c.id === dbChat2.id)).toBe(true);
348+
for (const c of resBody) {
349+
if (c.id === dbChat2.id) assertChat(c, c.id, 1, 3);
350+
else assertChat(c, c.id);
351+
assertChatMembersTangibility(c);
352+
}
353+
assertReceivedDateUpdated(resChat1, dbChat1, userOneData.username);
354+
assertReceivedDateUpdated(resChat2, dbChat2, userOneData.username);
355+
await db.chat.delete({ where: { id: dbChat2.id } });
325356
});
326357
});
327358

@@ -799,7 +830,8 @@ describe('Chats endpoints', async () => {
799830
const oldChat = await createChat('', chatMembers);
800831
await createMessage(oldChat.id, dbUserOne);
801832
const chatData = {
802-
profiles: chatMembers.map((u) => u.profile!.id),
833+
// Remove current user from profile ids
834+
profiles: chatMembers.filter((cm) => cm.id !== dbUserOne.id).map((u) => u.profile!.id),
803835
message: { body: 'Whats up?' },
804836
};
805837
const { authorizedApi } = await prepForAuthorizedTest(userOneData);
@@ -824,7 +856,8 @@ describe('Chats endpoints', async () => {
824856
const oldChat = await createChat('', chatMembers);
825857
await createMessage(oldChat.id, dbUserOne);
826858
const chatData = {
827-
profiles: chatMembers.map((u) => u.profile!.id),
859+
// Remove current user from profile ids
860+
profiles: chatMembers.filter((cm) => cm.id !== intangibleUser.id).map((u) => u.profile!.id),
828861
message: { body: 'Whats up?' },
829862
};
830863
const { authorizedApi } = await prepForAuthorizedTest(intangibleUserData);
@@ -866,6 +899,23 @@ describe('Chats endpoints', async () => {
866899
expect(dbMsgs[0].chatId).toBe(dbChats[0].id);
867900
});
868901

902+
it('should not use an already exist self-chat, and start new chat with the given member profile id', async () => {
903+
const dbSelfChat = await createChat('Hello, Me!', [dbUserOne], false);
904+
const chatData = { profiles: [dbUserTwo.profile!.id], message: { body: 'Hello!' } };
905+
const { authorizedApi } = await prepForAuthorizedTest(userOneData);
906+
const res = await authorizedApi.post(CHATS_URL).send(chatData);
907+
const dbMsgs = await db.message.findMany({});
908+
const dbChats = await db.chat.findMany({});
909+
const chat = res.body as ChatFullData;
910+
expect(res.statusCode).toBe(201);
911+
expect(res.type).toMatch(/json/);
912+
expect(dbChats).toHaveLength(2);
913+
expect(dbMsgs).toHaveLength(2);
914+
assertChat(chat, dbChats.find((c) => c.id !== dbSelfChat.id)!.id, 1, 2);
915+
expect(dbMsgs.find((m) => m.chatId === dbChats[0].id)).toBeTruthy();
916+
expect(dbMsgs.find((m) => m.chatId === dbChats[1].id)).toBeTruthy();
917+
});
918+
869919
it('should create new chat with a non-image message, and ignore `imagedata` field without an image file', async () => {
870920
const profileId = dbUserTwo.profile!.id;
871921
const chatData = { profiles: [profileId], message: { body: 'Hello!' }, imagedata };

0 commit comments

Comments
 (0)