Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/api/v1/chats/chats.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ chatsRouter.get('/', Middlewares.authValidator, async (req, res) => {

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

Expand Down
88 changes: 44 additions & 44 deletions src/api/v1/chats/chats.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import db from '@/lib/db';

const createPaginationArgs = (
filters: Types.BasePaginationFilters & { orderBy: 'createdAt' | 'updatedAt' },
limit = 10
limit = 10,
) => {
return {
...(filters.cursor ? { cursor: { id: filters.cursor }, skip: 1 } : {}),
Expand All @@ -36,7 +36,7 @@ const createChatAggregation = () => {

const createCurrentProfileChatArgs = (
currentProfile: Prisma.ProfileGetPayload<{ include: { user: true } }>,
now: Date
now: Date,
) => {
return {
profileName: currentProfile.user.username,
Expand All @@ -48,7 +48,7 @@ const createCurrentProfileChatArgs = (

const prepareChat = (
chat: Prisma.ChatGetPayload<{ include: ReturnType<typeof createChatAggregation> }>,
currentProfile: Profile
currentProfile: Profile,
) => {
if (currentProfile.tangible) {
chat.profiles = chat.profiles.map((p) => {
Expand All @@ -59,7 +59,7 @@ const prepareChat = (
});
} else {
chat.profiles = chat.profiles.map((p) =>
p.profileId === currentProfile.id ? p : { ...p, lastSeenAt: null }
p.profileId === currentProfile.id ? p : { ...p, lastSeenAt: null },
);
}
return chat;
Expand All @@ -69,23 +69,22 @@ export const createChat = async (
user: Types.PublicUser,
data: Schema.ValidChat,
imageData?: Types.ImageFullData,
uploadedImage?: Storage.UploadedImageData
uploadedImage?: Storage.UploadedImageData,
) => {
return await Utils.handleDBKnownErrors(
db.$transaction(async (tx) => {
// Get current user's profile
const currentProfile = await tx.profile.findUnique({
where: { userId: user.id },
const allProfiles = await tx.profile.findMany({
where: { OR: [{ id: { in: data.profiles } }, { userId: user.id }] },
include: { user: true },
distinct: 'id',
});
const otherProfiles: typeof allProfiles = [];
const currentProfile = allProfiles.reduce<(typeof allProfiles)[number] | null>(
(acc, curr) => (curr.user.id === user.id ? curr : (otherProfiles.push(curr), acc)),
null,
);
if (!currentProfile) throw new AppNotFoundError('Profile not found');
const currentProfileChatArgs = createCurrentProfileChatArgs(currentProfile, new Date());
// Get all the other profiles
const otherProfiles = await tx.profile.findMany({
where: { id: { in: data.profiles } },
include: { user: true },
distinct: 'id',
});
const chatAggregation = createChatAggregation();
const messageArgs = {
profileName: currentProfile.user.username,
Expand All @@ -101,19 +100,15 @@ export const createChat = async (
profileId: currentProfile.id,
};
// Find chats with the same owner and same group of profiles (there should be at most one)
const existentChats = await tx.chat.findMany({
where: {
managers: { some: { profileId: currentProfile.id, role: 'OWNER' } },
profiles: {
every: {
profileName: {
in: [currentProfile.user.username, ...otherProfiles.map((p) => p.user.username)],
},
},
const existentChats = (
await tx.chat.findMany({
where: {
managers: { some: { profileId: currentProfile.id, role: 'OWNER' } },
profiles: { every: { profileName: { in: allProfiles.map((p) => p.user.username) } } },
},
},
include: chatAggregation,
});
include: { ...chatAggregation, _count: { select: { profiles: true } } },
})
).filter((c) => c._count.profiles === allProfiles.length); // Filter out the profiles with a mismatched count.
// Upsert a chat
let chat: Prisma.ChatGetPayload<{ include: typeof chatAggregation }>;
if (existentChats.length > 0) {
Expand Down Expand Up @@ -163,7 +158,7 @@ export const createChat = async (
});
}
return prepareChat(chat, currentProfile);
})
}),
);
};

Expand All @@ -187,13 +182,13 @@ export const deleteChat = async (userId: User['id'], chatId: Chat['id']) => {
}
}
}
})
}),
);
};

export const getUserChats = async (
userId: User['id'],
filters: Types.BasePaginationFilters = {}
filters: Types.BasePaginationFilters = {},
) => {
return await Utils.handleDBKnownErrors(
db.$transaction(async (tx) => {
Expand All @@ -214,7 +209,7 @@ export const getUserChats = async (
include: createChatAggregation(),
});
return chats.map((chat) => prepareChat(chat, currentProfile));
})
}),
);
};

Expand Down Expand Up @@ -242,19 +237,24 @@ export const getUserChatById = async (userId: User['id'], chatId: Chat['id']) =>
throw new AppNotFoundError('Chat not found');
}
return prepareChat(chat, currentProfile);
})
}),
);
};

export const getUserChatsByMemberProfileId = async (
userId: User['id'],
profileId: Profile['id']
) => {
export const getUserChatsByMember = async (userId: User['id'], memberIdOrUsername: string) => {
return await Utils.handleDBKnownErrors(
db.$transaction(async (tx) => {
let profileId: Profile['id'];
try {
const memberProfile = await tx.profile.findUnique({ where: { id: profileId } });
const memberUser = await tx.user.findUnique({
where: { username: memberIdOrUsername },
include: { profile: true },
});
const memberProfile =
memberUser?.profile ??
(await tx.profile.findUnique({ where: { id: memberIdOrUsername } }));
if (!memberProfile) throw new AppNotFoundError('Chat member profile not found');
profileId = memberProfile.id;
} catch {
throw new AppNotFoundError('Chat member profile not found');
}
Expand All @@ -281,14 +281,14 @@ export const getUserChatsByMemberProfileId = async (
});
if (!currentProfileWithChats) throw new AppNotFoundError('Profile not found');
return currentProfileWithChats.chats.map((c) => prepareChat(c.chat, currentProfileWithChats));
})
}),
);
};

export const getUserChatMessages = async (
userId: User['id'],
chatId: Chat['id'],
filters: Types.BasePaginationFilters = {}
filters: Types.BasePaginationFilters = {},
) => {
return await Utils.handleDBKnownErrors(
db.$transaction(async (tx) => {
Expand All @@ -315,14 +315,14 @@ export const getUserChatMessages = async (
where: { chat: { id: chatId, profiles: { some: { profileId } } } },
include: createMessageAggregation(),
});
})
}),
);
};

export const getUserChatMessageById = async (
userId: User['id'],
chatId: Chat['id'],
msgId: Message['id']
msgId: Message['id'],
) => {
return await Utils.handleDBKnownErrors(
db.$transaction(async (tx) => {
Expand All @@ -343,7 +343,7 @@ export const getUserChatMessageById = async (
});
if (!message) throw new AppNotFoundError('Message not found');
return message;
})
}),
);
};

Expand All @@ -352,7 +352,7 @@ export const createUserChatMessage = async (
chatId: Chat['id'],
{ body }: Schema.ValidMessage,
imageData?: Types.ImageFullData,
uploadedImage?: Storage.UploadedImageData
uploadedImage?: Storage.UploadedImageData,
) => {
return await Utils.handleDBKnownErrors(
db.$transaction(async (tx) => {
Expand Down Expand Up @@ -393,7 +393,7 @@ export const createUserChatMessage = async (
},
include: createMessageAggregation(),
});
})
}),
);
};

Expand All @@ -411,7 +411,7 @@ export const updateProfileChatLastSeenDate = async (userId: User['id'], chatId:
where: { profileName_chatId: { profileName, chatId } },
data: { lastSeenAt: now },
});
})
}),
);
return now;
};
54 changes: 52 additions & 2 deletions src/tests/api/v1/chats.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,37 @@ describe('Chats endpoints', async () => {
}
assertReceivedDateUpdated(resChat1, dbChat1, userOneData.username);
assertReceivedDateUpdated(resChat2, dbChat2, userOneData.username);
await db.chat.delete({ where: { id: dbChat2.id } });
});

it('should respond all the current user chats that include the given member`s', async () => {
const memberUsername = dbUserTwo.username;
const dbChatId1 = dbChats.find((c) =>
c.profiles.some((p) => p.profileName === memberUsername),
)!.id;
const dbChat1 = (await db.chat.findUnique({
where: { id: dbChatId1 },
include: { profiles: true },
}))!;
const dbChat2 = await createChat('Hello #2', [dbUserOne, dbUserTwo, dbXUser]);
const { authorizedApi } = await prepForAuthorizedTest(userOneData);
const res = await authorizedApi.get(`${CHATS_URL}/members/${memberUsername}`);
const resBody = res.body as ChatFullData[];
const resChat1 = resBody.find((c) => c.id === dbChat1.id)!;
const resChat2 = resBody.find((c) => c.id === dbChat2.id)!;
expect(res.statusCode).toBe(200);
expect(res.type).toMatch(/json/);
expect(resBody).toBeInstanceOf(Array);
expect(resBody).toHaveLength(2);
expect(resBody.every((c) => c.id === dbChat1.id || c.id === dbChat2.id)).toBe(true);
for (const c of resBody) {
if (c.id === dbChat2.id) assertChat(c, c.id, 1, 3);
else assertChat(c, c.id);
assertChatMembersTangibility(c);
}
assertReceivedDateUpdated(resChat1, dbChat1, userOneData.username);
assertReceivedDateUpdated(resChat2, dbChat2, userOneData.username);
await db.chat.delete({ where: { id: dbChat2.id } });
});
});

Expand Down Expand Up @@ -799,7 +830,8 @@ describe('Chats endpoints', async () => {
const oldChat = await createChat('', chatMembers);
await createMessage(oldChat.id, dbUserOne);
const chatData = {
profiles: chatMembers.map((u) => u.profile!.id),
// Remove current user from profile ids
profiles: chatMembers.filter((cm) => cm.id !== dbUserOne.id).map((u) => u.profile!.id),
message: { body: 'Whats up?' },
};
const { authorizedApi } = await prepForAuthorizedTest(userOneData);
Expand All @@ -824,7 +856,8 @@ describe('Chats endpoints', async () => {
const oldChat = await createChat('', chatMembers);
await createMessage(oldChat.id, dbUserOne);
const chatData = {
profiles: chatMembers.map((u) => u.profile!.id),
// Remove current user from profile ids
profiles: chatMembers.filter((cm) => cm.id !== intangibleUser.id).map((u) => u.profile!.id),
message: { body: 'Whats up?' },
};
const { authorizedApi } = await prepForAuthorizedTest(intangibleUserData);
Expand Down Expand Up @@ -866,6 +899,23 @@ describe('Chats endpoints', async () => {
expect(dbMsgs[0].chatId).toBe(dbChats[0].id);
});

it('should not use an already exist self-chat, and start new chat with the given member profile id', async () => {
const dbSelfChat = await createChat('Hello, Me!', [dbUserOne], false);
const chatData = { profiles: [dbUserTwo.profile!.id], message: { body: 'Hello!' } };
const { authorizedApi } = await prepForAuthorizedTest(userOneData);
const res = await authorizedApi.post(CHATS_URL).send(chatData);
const dbMsgs = await db.message.findMany({});
const dbChats = await db.chat.findMany({});
const chat = res.body as ChatFullData;
expect(res.statusCode).toBe(201);
expect(res.type).toMatch(/json/);
expect(dbChats).toHaveLength(2);
expect(dbMsgs).toHaveLength(2);
assertChat(chat, dbChats.find((c) => c.id !== dbSelfChat.id)!.id, 1, 2);
expect(dbMsgs.find((m) => m.chatId === dbChats[0].id)).toBeTruthy();
expect(dbMsgs.find((m) => m.chatId === dbChats[1].id)).toBeTruthy();
});

it('should create new chat with a non-image message, and ignore `imagedata` field without an image file', async () => {
const profileId = dbUserTwo.profile!.id;
const chatData = { profiles: [profileId], message: { body: 'Hello!' }, imagedata };
Expand Down