mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-11-12 15:11:06 +08:00
rename friends to connections
This commit is contained in:
parent
a77186697e
commit
a65a81b464
21 changed files with 417 additions and 392 deletions
|
|
@ -9,61 +9,64 @@ import {
|
|||
} from "vitest";
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
import * as FriendsDal from "../../../src/dal/friends";
|
||||
import { createConnection as createFriend } from "../../__testData__/connections";
|
||||
import * as ConnectionsDal from "../../../src/dal/connections";
|
||||
import { createConnection } from "../../__testData__/connections";
|
||||
|
||||
describe("FriendsDal", () => {
|
||||
describe("ConnectionsDal", () => {
|
||||
beforeAll(async () => {
|
||||
await FriendsDal.createIndicies();
|
||||
await ConnectionsDal.createIndicies();
|
||||
});
|
||||
|
||||
describe("getRequests", () => {
|
||||
it("get by uid", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const initOne = await createFriend({ initiatorUid: uid });
|
||||
const initTwo = await createFriend({ initiatorUid: uid });
|
||||
const friendOne = await createFriend({ friendUid: uid });
|
||||
const _decoy = await createFriend({});
|
||||
const initOne = await createConnection({ initiatorUid: uid });
|
||||
const initTwo = await createConnection({ initiatorUid: uid });
|
||||
const friendOne = await createConnection({ friendUid: uid });
|
||||
const _decoy = await createConnection({});
|
||||
|
||||
//WHEN / THEM
|
||||
|
||||
expect(
|
||||
await FriendsDal.getRequests({ initiatorUid: uid, friendUid: uid })
|
||||
await ConnectionsDal.getConnections({
|
||||
initiatorUid: uid,
|
||||
friendUid: uid,
|
||||
})
|
||||
).toStrictEqual([initOne, initTwo, friendOne]);
|
||||
});
|
||||
|
||||
it("get by uid and status", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const initAccepted = await createFriend({
|
||||
const initAccepted = await createConnection({
|
||||
initiatorUid: uid,
|
||||
status: "accepted",
|
||||
});
|
||||
const _initPending = await createFriend({
|
||||
const _initPending = await createConnection({
|
||||
initiatorUid: uid,
|
||||
status: "pending",
|
||||
});
|
||||
const initBlocked = await createFriend({
|
||||
const initBlocked = await createConnection({
|
||||
initiatorUid: uid,
|
||||
status: "blocked",
|
||||
});
|
||||
|
||||
const friendAccepted = await createFriend({
|
||||
const friendAccepted = await createConnection({
|
||||
friendUid: uid,
|
||||
status: "accepted",
|
||||
});
|
||||
const _friendPending = await createFriend({
|
||||
const _friendPending = await createConnection({
|
||||
friendUid: uid,
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
const _decoy = await createFriend({ status: "accepted" });
|
||||
const _decoy = await createConnection({ status: "accepted" });
|
||||
|
||||
//WHEN / THEN
|
||||
|
||||
expect(
|
||||
await FriendsDal.getRequests({
|
||||
await ConnectionsDal.getConnections({
|
||||
initiatorUid: uid,
|
||||
friendUid: uid,
|
||||
status: ["accepted", "blocked"],
|
||||
|
|
@ -85,17 +88,17 @@ describe("FriendsDal", () => {
|
|||
it("should fail creating duplicates", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = await createFriend({
|
||||
const first = await createConnection({
|
||||
initiatorUid: uid,
|
||||
});
|
||||
|
||||
//WHEN/THEN
|
||||
await expect(
|
||||
createFriend({
|
||||
createConnection({
|
||||
initiatorUid: first.friendUid,
|
||||
friendUid: uid,
|
||||
})
|
||||
).rejects.toThrow("Duplicate friend or blocked");
|
||||
).rejects.toThrow("Duplicate connection or blocked");
|
||||
});
|
||||
|
||||
it("should create", async () => {
|
||||
|
|
@ -104,7 +107,7 @@ describe("FriendsDal", () => {
|
|||
const friendUid = new ObjectId().toHexString();
|
||||
|
||||
//WHEN
|
||||
const created = await FriendsDal.create(
|
||||
const created = await ConnectionsDal.create(
|
||||
{ uid, name: "Bob" },
|
||||
{ uid: friendUid, name: "Kevin" },
|
||||
2
|
||||
|
|
@ -123,15 +126,15 @@ describe("FriendsDal", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should fail if maximum friends are reached", async () => {
|
||||
it("should fail if maximum connections are reached", async () => {
|
||||
//GIVEN
|
||||
const initiatorUid = new ObjectId().toHexString();
|
||||
await createFriend({ initiatorUid });
|
||||
await createFriend({ initiatorUid });
|
||||
await createConnection({ initiatorUid });
|
||||
await createConnection({ initiatorUid });
|
||||
|
||||
//WHEN / THEM
|
||||
await expect(createFriend({ initiatorUid }, 2)).rejects.toThrow(
|
||||
"Maximum number of friends reached\nStack: create friend request"
|
||||
await expect(createConnection({ initiatorUid }, 2)).rejects.toThrow(
|
||||
"Maximum number of connections reached\nStack: create connection request"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -139,36 +142,44 @@ describe("FriendsDal", () => {
|
|||
it("should update the status", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = await createFriend({
|
||||
const first = await createConnection({
|
||||
friendUid: uid,
|
||||
});
|
||||
const second = await createFriend({
|
||||
const second = await createConnection({
|
||||
friendUid: uid,
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await FriendsDal.updateStatus(uid, first._id.toHexString(), "accepted");
|
||||
await ConnectionsDal.updateStatus(
|
||||
uid,
|
||||
first._id.toHexString(),
|
||||
"accepted"
|
||||
);
|
||||
|
||||
//THEN
|
||||
expect(await FriendsDal.getRequests({ friendUid: uid })).toEqual([
|
||||
expect(await ConnectionsDal.getConnections({ friendUid: uid })).toEqual([
|
||||
{ ...first, status: "accepted" },
|
||||
second,
|
||||
]);
|
||||
|
||||
//can update twice to the same status
|
||||
await FriendsDal.updateStatus(uid, first._id.toHexString(), "accepted");
|
||||
await ConnectionsDal.updateStatus(
|
||||
uid,
|
||||
first._id.toHexString(),
|
||||
"accepted"
|
||||
);
|
||||
});
|
||||
it("should fail if uid does not match the friendUid", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = await createFriend({
|
||||
const first = await createConnection({
|
||||
initiatorUid: uid,
|
||||
});
|
||||
|
||||
//WHEN / THEN
|
||||
await expect(
|
||||
FriendsDal.updateStatus(uid, first._id.toHexString(), "accepted")
|
||||
).rejects.toThrow("Friend not found");
|
||||
ConnectionsDal.updateStatus(uid, first._id.toHexString(), "accepted")
|
||||
).rejects.toThrow("Connection not found");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -176,81 +187,85 @@ describe("FriendsDal", () => {
|
|||
it("should delete by initiator", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = await createFriend({
|
||||
const first = await createConnection({
|
||||
initiatorUid: uid,
|
||||
});
|
||||
const second = await createFriend({
|
||||
const second = await createConnection({
|
||||
initiatorUid: uid,
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await FriendsDal.deleteById(uid, first._id.toHexString());
|
||||
await ConnectionsDal.deleteById(uid, first._id.toHexString());
|
||||
|
||||
//THEN
|
||||
expect(await FriendsDal.getRequests({ initiatorUid: uid })).toStrictEqual(
|
||||
[second]
|
||||
);
|
||||
expect(
|
||||
await ConnectionsDal.getConnections({ initiatorUid: uid })
|
||||
).toStrictEqual([second]);
|
||||
});
|
||||
|
||||
it("should delete by friend", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = await createFriend({
|
||||
const first = await createConnection({
|
||||
friendUid: uid,
|
||||
});
|
||||
const second = await createFriend({
|
||||
const second = await createConnection({
|
||||
friendUid: uid,
|
||||
status: "accepted",
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await FriendsDal.deleteById(uid, first._id.toHexString());
|
||||
await ConnectionsDal.deleteById(uid, first._id.toHexString());
|
||||
|
||||
//THEN
|
||||
expect(
|
||||
await FriendsDal.getRequests({ initiatorUid: second.initiatorUid })
|
||||
await ConnectionsDal.getConnections({
|
||||
initiatorUid: second.initiatorUid,
|
||||
})
|
||||
).toStrictEqual([second]);
|
||||
});
|
||||
|
||||
it("should fail if uid does not match", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = await createFriend({
|
||||
const first = await createConnection({
|
||||
initiatorUid: uid,
|
||||
});
|
||||
|
||||
//WHEN / THEN
|
||||
await expect(
|
||||
FriendsDal.deleteById("Bob", first._id.toHexString())
|
||||
ConnectionsDal.deleteById("Bob", first._id.toHexString())
|
||||
).rejects.toThrow("Cannot be deleted");
|
||||
});
|
||||
|
||||
it("should fail if initiator deletes blocked by friend", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const myRequestWasBlocked = await createFriend({
|
||||
const myRequestWasBlocked = await createConnection({
|
||||
initiatorName: uid,
|
||||
status: "blocked",
|
||||
});
|
||||
|
||||
//WHEN / THEN
|
||||
await expect(
|
||||
FriendsDal.deleteById(uid, myRequestWasBlocked._id.toHexString())
|
||||
ConnectionsDal.deleteById(uid, myRequestWasBlocked._id.toHexString())
|
||||
).rejects.toThrow("Cannot be deleted");
|
||||
});
|
||||
it("allow friend to delete blocked", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const myBlockedUser = await createFriend({
|
||||
const myBlockedUser = await createConnection({
|
||||
friendUid: uid,
|
||||
status: "blocked",
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await FriendsDal.deleteById(uid, myBlockedUser._id.toHexString());
|
||||
await ConnectionsDal.deleteById(uid, myBlockedUser._id.toHexString());
|
||||
|
||||
//THEN
|
||||
expect(await FriendsDal.getRequests({ friendUid: uid })).toEqual([]);
|
||||
expect(await ConnectionsDal.getConnections({ friendUid: uid })).toEqual(
|
||||
[]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -258,21 +273,26 @@ describe("FriendsDal", () => {
|
|||
it("should delete by uid", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const _initOne = await createFriend({ initiatorUid: uid });
|
||||
const _initTwo = await createFriend({ initiatorUid: uid });
|
||||
const _friendOne = await createFriend({ friendUid: uid });
|
||||
const decoy = await createFriend({});
|
||||
const _initOne = await createConnection({ initiatorUid: uid });
|
||||
const _initTwo = await createConnection({ initiatorUid: uid });
|
||||
const _friendOne = await createConnection({ friendUid: uid });
|
||||
const decoy = await createConnection({});
|
||||
|
||||
//WHEN
|
||||
await FriendsDal.deleteByUid(uid);
|
||||
await ConnectionsDal.deleteByUid(uid);
|
||||
|
||||
//THEN
|
||||
expect(
|
||||
await FriendsDal.getRequests({ initiatorUid: uid, friendUid: uid })
|
||||
await ConnectionsDal.getConnections({
|
||||
initiatorUid: uid,
|
||||
friendUid: uid,
|
||||
})
|
||||
).toEqual([]);
|
||||
|
||||
expect(
|
||||
await FriendsDal.getRequests({ initiatorUid: decoy.initiatorUid })
|
||||
await ConnectionsDal.getConnections({
|
||||
initiatorUid: decoy.initiatorUid,
|
||||
})
|
||||
).toEqual([decoy]);
|
||||
});
|
||||
});
|
||||
|
|
@ -280,26 +300,29 @@ describe("FriendsDal", () => {
|
|||
it("should update the name", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const initOne = await createFriend({
|
||||
const initOne = await createConnection({
|
||||
initiatorUid: uid,
|
||||
initiatorName: "Bob",
|
||||
});
|
||||
const initTwo = await createFriend({
|
||||
const initTwo = await createConnection({
|
||||
initiatorUid: uid,
|
||||
initiatorName: "Bob",
|
||||
});
|
||||
const friendOne = await createFriend({
|
||||
const friendOne = await createConnection({
|
||||
friendUid: uid,
|
||||
friendName: "Bob",
|
||||
});
|
||||
const decoy = await createFriend({});
|
||||
const decoy = await createConnection({});
|
||||
|
||||
//WHEN
|
||||
await FriendsDal.updateName(uid, "King Bob");
|
||||
await ConnectionsDal.updateName(uid, "King Bob");
|
||||
|
||||
//THEN
|
||||
expect(
|
||||
await FriendsDal.getRequests({ initiatorUid: uid, friendUid: uid })
|
||||
await ConnectionsDal.getConnections({
|
||||
initiatorUid: uid,
|
||||
friendUid: uid,
|
||||
})
|
||||
).toEqual([
|
||||
{ ...initOne, initiatorName: "King Bob" },
|
||||
{ ...initTwo, initiatorName: "King Bob" },
|
||||
|
|
@ -307,7 +330,9 @@ describe("FriendsDal", () => {
|
|||
]);
|
||||
|
||||
expect(
|
||||
await FriendsDal.getRequests({ initiatorUid: decoy.initiatorUid })
|
||||
await ConnectionsDal.getConnections({
|
||||
initiatorUid: decoy.initiatorUid,
|
||||
})
|
||||
).toEqual([decoy]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { ObjectId } from "mongodb";
|
||||
import * as FriendsDal from "../../src/dal/friends";
|
||||
import * as ConnectionsDal from "../../src/dal/connections";
|
||||
|
||||
export async function createConnection(
|
||||
data: Partial<FriendsDal.DBFriendRequest>,
|
||||
data: Partial<ConnectionsDal.DBConnection>,
|
||||
maxPerUser = 25
|
||||
): Promise<FriendsDal.DBFriendRequest> {
|
||||
const result = await FriendsDal.create(
|
||||
): Promise<ConnectionsDal.DBConnection> {
|
||||
const result = await ConnectionsDal.create(
|
||||
{
|
||||
uid: data.initiatorUid ?? new ObjectId().toHexString(),
|
||||
name: data.initiatorName ?? "user" + new ObjectId().toHexString(),
|
||||
|
|
@ -16,7 +16,7 @@ export async function createConnection(
|
|||
},
|
||||
maxPerUser
|
||||
);
|
||||
await FriendsDal.getCollection().updateOne(
|
||||
await ConnectionsDal.getCollection().updateOne(
|
||||
{ _id: result._id },
|
||||
{ $set: data }
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { mockBearerAuthentication } from "../../__testData__/auth";
|
|||
import * as Configuration from "../../../src/init/configuration";
|
||||
import { ObjectId } from "mongodb";
|
||||
import _ from "lodash";
|
||||
import * as FriendsDal from "../../../src/dal/friends";
|
||||
import * as ConnectionsDal from "../../../src/dal/connections";
|
||||
import * as UserDal from "../../../src/dal/user";
|
||||
|
||||
const mockApp = request(app);
|
||||
|
|
@ -13,24 +13,24 @@ const configuration = Configuration.getCachedConfiguration();
|
|||
const uid = new ObjectId().toHexString();
|
||||
const mockAuth = mockBearerAuthentication(uid);
|
||||
|
||||
describe("FriendsController", () => {
|
||||
describe("ConnectionsController", () => {
|
||||
beforeEach(async () => {
|
||||
await enableFriendsEndpoints(true);
|
||||
await enablleConnectionsEndpoints(true);
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(1000);
|
||||
mockAuth.beforeEach();
|
||||
});
|
||||
|
||||
describe("get friend requests", () => {
|
||||
const getFriendsMock = vi.spyOn(FriendsDal, "getRequests");
|
||||
describe("get connections", () => {
|
||||
const getConnectionsMock = vi.spyOn(ConnectionsDal, "getConnections");
|
||||
|
||||
beforeEach(() => {
|
||||
getFriendsMock.mockClear();
|
||||
getConnectionsMock.mockClear();
|
||||
});
|
||||
|
||||
it("should get for the current user", async () => {
|
||||
//GIVEN
|
||||
const friend: FriendsDal.DBFriendRequest = {
|
||||
const friend: ConnectionsDal.DBConnection = {
|
||||
_id: new ObjectId(),
|
||||
addedAt: 42,
|
||||
initiatorUid: new ObjectId().toHexString(),
|
||||
|
|
@ -41,11 +41,11 @@ describe("FriendsController", () => {
|
|||
key: "key",
|
||||
};
|
||||
|
||||
getFriendsMock.mockResolvedValue([friend]);
|
||||
getConnectionsMock.mockResolvedValue([friend]);
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
.get("/friends/requests")
|
||||
.get("/connections")
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
.expect(200);
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ describe("FriendsController", () => {
|
|||
expect(body.data).toEqual([
|
||||
{ ...friend, _id: friend._id.toHexString(), key: undefined },
|
||||
]);
|
||||
expect(getFriendsMock).toHaveBeenCalledWith({
|
||||
expect(getConnectionsMock).toHaveBeenCalledWith({
|
||||
initiatorUid: uid,
|
||||
friendUid: uid,
|
||||
});
|
||||
|
|
@ -61,17 +61,17 @@ describe("FriendsController", () => {
|
|||
|
||||
it("should filter by status", async () => {
|
||||
//GIVEN
|
||||
getFriendsMock.mockResolvedValue([]);
|
||||
getConnectionsMock.mockResolvedValue([]);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.get("/friends/requests")
|
||||
.get("/connections")
|
||||
.query({ status: "accepted" })
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(getFriendsMock).toHaveBeenCalledWith({
|
||||
expect(getConnectionsMock).toHaveBeenCalledWith({
|
||||
initiatorUid: uid,
|
||||
friendUid: uid,
|
||||
status: ["accepted"],
|
||||
|
|
@ -80,17 +80,17 @@ describe("FriendsController", () => {
|
|||
|
||||
it("should filter by multiple status", async () => {
|
||||
//GIVEN
|
||||
getFriendsMock.mockResolvedValue([]);
|
||||
getConnectionsMock.mockResolvedValue([]);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.get("/friends/requests")
|
||||
.get("/connections")
|
||||
.query({ status: ["accepted", "blocked"] })
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(getFriendsMock).toHaveBeenCalledWith({
|
||||
expect(getConnectionsMock).toHaveBeenCalledWith({
|
||||
initiatorUid: uid,
|
||||
friendUid: uid,
|
||||
status: ["accepted", "blocked"],
|
||||
|
|
@ -99,67 +99,67 @@ describe("FriendsController", () => {
|
|||
|
||||
it("should filter by type incoming", async () => {
|
||||
//GIVEN
|
||||
getFriendsMock.mockResolvedValue([]);
|
||||
getConnectionsMock.mockResolvedValue([]);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.get("/friends/requests")
|
||||
.get("/connections")
|
||||
.query({ type: "incoming" })
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(getFriendsMock).toHaveBeenCalledWith({
|
||||
expect(getConnectionsMock).toHaveBeenCalledWith({
|
||||
friendUid: uid,
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter by type outgoing", async () => {
|
||||
//GIVEN
|
||||
getFriendsMock.mockResolvedValue([]);
|
||||
getConnectionsMock.mockResolvedValue([]);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.get("/friends/requests")
|
||||
.get("/connections")
|
||||
.query({ type: "outgoing" })
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(getFriendsMock).toHaveBeenCalledWith({
|
||||
expect(getConnectionsMock).toHaveBeenCalledWith({
|
||||
initiatorUid: uid,
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter by multiple types", async () => {
|
||||
//GIVEN
|
||||
getFriendsMock.mockResolvedValue([]);
|
||||
getConnectionsMock.mockResolvedValue([]);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.get("/friends/requests")
|
||||
.get("/connections")
|
||||
.query({ type: ["incoming", "outgoing"] })
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(getFriendsMock).toHaveBeenCalledWith({
|
||||
expect(getConnectionsMock).toHaveBeenCalledWith({
|
||||
initiatorUid: uid,
|
||||
friendUid: uid,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail if friends endpoints are disabled", async () => {
|
||||
it("should fail if connections endpoints are disabled", async () => {
|
||||
await expectFailForDisabledEndpoint(
|
||||
mockApp.get("/friends/requests").set("Authorization", `Bearer ${uid}`)
|
||||
mockApp.get("/connections").set("Authorization", `Bearer ${uid}`)
|
||||
);
|
||||
});
|
||||
it("should fail without authentication", async () => {
|
||||
await mockApp.get("/friends/requests").expect(401);
|
||||
await mockApp.get("/connections").expect(401);
|
||||
});
|
||||
it("should fail for unknown query parameter", async () => {
|
||||
const { body } = await mockApp
|
||||
.get("/friends/requests")
|
||||
.get("/connections")
|
||||
.query({ extra: "yes" })
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
.expect(422);
|
||||
|
|
@ -174,7 +174,7 @@ describe("FriendsController", () => {
|
|||
describe("create friend request", () => {
|
||||
const getUserByNameMock = vi.spyOn(UserDal, "getUserByName");
|
||||
const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser");
|
||||
const createUserMock = vi.spyOn(FriendsDal, "create");
|
||||
const createUserMock = vi.spyOn(ConnectionsDal, "create");
|
||||
|
||||
beforeEach(() => {
|
||||
[getUserByNameMock, getPartialUserMock, createUserMock].forEach((it) =>
|
||||
|
|
@ -189,7 +189,7 @@ describe("FriendsController", () => {
|
|||
getUserByNameMock.mockResolvedValue(myFriend as any);
|
||||
getPartialUserMock.mockResolvedValue(me as any);
|
||||
|
||||
const result: FriendsDal.DBFriendRequest = {
|
||||
const result: ConnectionsDal.DBConnection = {
|
||||
_id: new ObjectId(),
|
||||
addedAt: 42,
|
||||
initiatorUid: me.uid,
|
||||
|
|
@ -203,7 +203,7 @@ describe("FriendsController", () => {
|
|||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
.post("/friends/requests")
|
||||
.post("/connections")
|
||||
.send({ friendName: "Kevin" })
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
.expect(200);
|
||||
|
|
@ -219,11 +219,15 @@ describe("FriendsController", () => {
|
|||
status: "pending",
|
||||
});
|
||||
|
||||
expect(getUserByNameMock).toHaveBeenCalledWith("Kevin", "create friend");
|
||||
expect(getPartialUserMock).toHaveBeenCalledWith(uid, "create friend", [
|
||||
"uid",
|
||||
"name",
|
||||
]);
|
||||
expect(getUserByNameMock).toHaveBeenCalledWith(
|
||||
"Kevin",
|
||||
"create connection"
|
||||
);
|
||||
expect(getPartialUserMock).toHaveBeenCalledWith(
|
||||
uid,
|
||||
"create connection",
|
||||
["uid", "name"]
|
||||
);
|
||||
expect(createUserMock).toHaveBeenCalledWith(me, myFriend, 100);
|
||||
});
|
||||
|
||||
|
|
@ -236,7 +240,7 @@ describe("FriendsController", () => {
|
|||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
.post("/friends/requests")
|
||||
.post("/connections")
|
||||
.send({ friendName: "Bob" })
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
.expect(400);
|
||||
|
|
@ -248,7 +252,7 @@ describe("FriendsController", () => {
|
|||
it("should fail without mandatory properties", async () => {
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
.post("/friends/requests")
|
||||
.post("/connections")
|
||||
.send({})
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
.expect(422);
|
||||
|
|
@ -262,7 +266,7 @@ describe("FriendsController", () => {
|
|||
it("should fail with extra properties", async () => {
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
.post("/friends/requests")
|
||||
.post("/connections")
|
||||
.send({ friendName: "1", extra: "value" })
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
.expect(422);
|
||||
|
|
@ -274,22 +278,22 @@ describe("FriendsController", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should fail if friends endpoints are disabled", async () => {
|
||||
it("should fail if connections endpoints are disabled", async () => {
|
||||
await expectFailForDisabledEndpoint(
|
||||
mockApp
|
||||
.post("/friends/requests")
|
||||
.post("/connections")
|
||||
.send({ friendName: "1" })
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail without authentication", async () => {
|
||||
await mockApp.post("/friends/requests").expect(401);
|
||||
await mockApp.post("/connections").expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete friend request", () => {
|
||||
const deleteByIdMock = vi.spyOn(FriendsDal, "deleteById");
|
||||
const deleteByIdMock = vi.spyOn(ConnectionsDal, "deleteById");
|
||||
|
||||
beforeEach(() => {
|
||||
deleteByIdMock.mockClear().mockResolvedValue();
|
||||
|
|
@ -298,28 +302,26 @@ describe("FriendsController", () => {
|
|||
it("should delete by id", async () => {
|
||||
//WHEN
|
||||
await mockApp
|
||||
.delete("/friends/requests/1")
|
||||
.delete("/connections/1")
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(deleteByIdMock).toHaveBeenCalledWith(uid, "1");
|
||||
});
|
||||
it("should fail if friends endpoints are disabled", async () => {
|
||||
it("should fail if connections endpoints are disabled", async () => {
|
||||
await expectFailForDisabledEndpoint(
|
||||
mockApp
|
||||
.delete("/friends/requests/1")
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
mockApp.delete("/connections/1").set("Authorization", `Bearer ${uid}`)
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail without authentication", async () => {
|
||||
await mockApp.delete("/friends/requests/1").expect(401);
|
||||
await mockApp.delete("/connections/1").expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("update friend request", () => {
|
||||
const updateStatusMock = vi.spyOn(FriendsDal, "updateStatus");
|
||||
const updateStatusMock = vi.spyOn(ConnectionsDal, "updateStatus");
|
||||
|
||||
beforeEach(() => {
|
||||
updateStatusMock.mockClear().mockResolvedValue();
|
||||
|
|
@ -328,7 +330,7 @@ describe("FriendsController", () => {
|
|||
it("should accept", async () => {
|
||||
//WHEN
|
||||
await mockApp
|
||||
.patch("/friends/requests/1")
|
||||
.patch("/connections/1")
|
||||
.send({ status: "accepted" })
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
.expect(200);
|
||||
|
|
@ -339,7 +341,7 @@ describe("FriendsController", () => {
|
|||
it("should block", async () => {
|
||||
//WHEN
|
||||
await mockApp
|
||||
.patch("/friends/requests/1")
|
||||
.patch("/connections/1")
|
||||
.send({ status: "blocked" })
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
.expect(200);
|
||||
|
|
@ -350,7 +352,7 @@ describe("FriendsController", () => {
|
|||
|
||||
it("should fail for invalid status", async () => {
|
||||
const { body } = await mockApp
|
||||
.patch("/friends/requests/1")
|
||||
.patch("/connections/1")
|
||||
.send({ status: "invalid" })
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
.expect(422);
|
||||
|
|
@ -362,10 +364,10 @@ describe("FriendsController", () => {
|
|||
],
|
||||
});
|
||||
});
|
||||
it("should fail if friends endpoints are disabled", async () => {
|
||||
it("should fail if connections endpoints are disabled", async () => {
|
||||
await expectFailForDisabledEndpoint(
|
||||
mockApp
|
||||
.patch("/friends/requests/1")
|
||||
.patch("/connections/1")
|
||||
.send({ status: "accepted" })
|
||||
.set("Authorization", `Bearer ${uid}`)
|
||||
);
|
||||
|
|
@ -373,14 +375,14 @@ describe("FriendsController", () => {
|
|||
|
||||
it("should fail without authentication", async () => {
|
||||
await mockApp
|
||||
.patch("/friends/requests/1")
|
||||
.patch("/connections/1")
|
||||
.send({ status: "accepted" })
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function enableFriendsEndpoints(enabled: boolean): Promise<void> {
|
||||
async function enablleConnectionsEndpoints(enabled: boolean): Promise<void> {
|
||||
const mockConfig = _.merge(await configuration, {
|
||||
connections: { enabled },
|
||||
});
|
||||
|
|
@ -390,7 +392,7 @@ async function enableFriendsEndpoints(enabled: boolean): Promise<void> {
|
|||
);
|
||||
}
|
||||
async function expectFailForDisabledEndpoint(call: SuperTest): Promise<void> {
|
||||
await enableFriendsEndpoints(false);
|
||||
await enablleConnectionsEndpoints(false);
|
||||
const { body } = await call.expect(503);
|
||||
expect(body.message).toEqual("Friends are not available at this time.");
|
||||
expect(body.message).toEqual("Connections are not available at this time.");
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ import { MonkeyMail, UserStreak } from "@monkeytype/schemas/users";
|
|||
import MonkeyError, { isFirebaseError } from "../../../src/utils/error";
|
||||
import { LeaderboardEntry } from "@monkeytype/schemas/leaderboards";
|
||||
import * as WeeklyXpLeaderboard from "../../../src/services/weekly-xp-leaderboard";
|
||||
import * as FriendsDal from "../../../src/dal/friends";
|
||||
import * as ConnectionsDal from "../../../src/dal/connections";
|
||||
import { pb } from "../../__testData__/users";
|
||||
import { SuperTest } from "supertest";
|
||||
|
||||
|
|
@ -626,7 +626,7 @@ describe("user controller test", () => {
|
|||
"purgeUserFromXpLeaderboards"
|
||||
);
|
||||
const blocklistAddMock = vi.spyOn(BlocklistDal, "add");
|
||||
const friendsDeletebyUidMock = vi.spyOn(FriendsDal, "deleteByUid");
|
||||
const friendsDeletebyUidMock = vi.spyOn(ConnectionsDal, "deleteByUid");
|
||||
const logsDeleteUserMock = vi.spyOn(LogDal, "deleteUserLogs");
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -989,7 +989,7 @@ describe("user controller test", () => {
|
|||
const blocklistContainsMock = vi.spyOn(BlocklistDal, "contains");
|
||||
const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser");
|
||||
const updateNameMock = vi.spyOn(UserDal, "updateName");
|
||||
const friendsUpdateNameMock = vi.spyOn(FriendsDal, "updateName");
|
||||
const friendsUpdateNameMock = vi.spyOn(ConnectionsDal, "updateName");
|
||||
const addImportantLogMock = vi.spyOn(LogDal, "addImportantLog");
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -4058,5 +4058,5 @@ async function enableFriendsEndpoints(enabled: boolean): Promise<void> {
|
|||
async function expectFailForDisabledEndpoint(call: SuperTest): Promise<void> {
|
||||
await enableFriendsEndpoints(false);
|
||||
const { body } = await call.expect(503);
|
||||
expect(body.message).toEqual("Friends are not available at this time.");
|
||||
expect(body.message).toEqual("Connections are not available at this time.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,9 +100,9 @@ export function getOpenApi(): OpenAPIObject {
|
|||
"x-displayName": "Leaderboards",
|
||||
},
|
||||
{
|
||||
name: "friends",
|
||||
description: "User friend requests and friends list.",
|
||||
"x-displayName": "Friends",
|
||||
name: "connections",
|
||||
description: "Connections between users.",
|
||||
"x-displayName": "Connections",
|
||||
"x-public": "no",
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
import {
|
||||
CreateFriendRequestRequest,
|
||||
CreateFriendRequestResponse,
|
||||
GetFriendRequestsQuery,
|
||||
GetFriendRequestsResponse,
|
||||
CreateConnectionRequest,
|
||||
CreateConnectionResponse,
|
||||
GetConnectionsQuery,
|
||||
GetConnectionsResponse,
|
||||
IdPathParams,
|
||||
UpdateFriendRequestsRequest,
|
||||
} from "@monkeytype/contracts/friends";
|
||||
UpdateConnectionRequest,
|
||||
} from "@monkeytype/contracts/connections";
|
||||
import { MonkeyRequest } from "../types";
|
||||
import { MonkeyResponse } from "../../utils/monkey-response";
|
||||
import * as FriendsDal from "../../dal/friends";
|
||||
import * as ConnectionsDal from "../../dal/connections";
|
||||
import * as UserDal from "../../dal/user";
|
||||
import { replaceObjectId } from "../../utils/misc";
|
||||
import MonkeyError from "../../utils/error";
|
||||
import { omit } from "lodash";
|
||||
import { FriendRequest } from "@monkeytype/schemas/friends";
|
||||
import { Connection } from "@monkeytype/schemas/connections";
|
||||
|
||||
function convert(db: FriendsDal.DBFriendRequest): FriendRequest {
|
||||
function convert(db: ConnectionsDal.DBConnection): Connection {
|
||||
return replaceObjectId(omit(db, "key"));
|
||||
}
|
||||
export async function getRequests(
|
||||
req: MonkeyRequest<GetFriendRequestsQuery>
|
||||
): Promise<GetFriendRequestsResponse> {
|
||||
req: MonkeyRequest<GetConnectionsQuery>
|
||||
): Promise<GetConnectionsResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const { status, type } = req.query;
|
||||
|
||||
const results = await FriendsDal.getRequests({
|
||||
const results = await ConnectionsDal.getConnections({
|
||||
initiatorUid:
|
||||
type === undefined || type.includes("outgoing") ? uid : undefined,
|
||||
friendUid:
|
||||
|
|
@ -32,30 +32,30 @@ export async function getRequests(
|
|||
status: status,
|
||||
});
|
||||
|
||||
return new MonkeyResponse("Friend requests retrieved", results.map(convert));
|
||||
return new MonkeyResponse("Connections retrieved", results.map(convert));
|
||||
}
|
||||
|
||||
export async function createRequest(
|
||||
req: MonkeyRequest<undefined, CreateFriendRequestRequest>
|
||||
): Promise<CreateFriendRequestResponse> {
|
||||
req: MonkeyRequest<undefined, CreateConnectionRequest>
|
||||
): Promise<CreateConnectionResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const { friendName } = req.body;
|
||||
const { maxPerUser } = req.ctx.configuration.connections;
|
||||
|
||||
const friend = await UserDal.getUserByName(friendName, "create friend");
|
||||
const friend = await UserDal.getUserByName(friendName, "create connection");
|
||||
|
||||
if (uid === friend.uid) {
|
||||
throw new MonkeyError(400, "You cannot be your own friend, sorry.");
|
||||
}
|
||||
|
||||
const initiator = await UserDal.getPartialUser(uid, "create friend", [
|
||||
const initiator = await UserDal.getPartialUser(uid, "create connection", [
|
||||
"uid",
|
||||
"name",
|
||||
]);
|
||||
|
||||
const result = await FriendsDal.create(initiator, friend, maxPerUser);
|
||||
const result = await ConnectionsDal.create(initiator, friend, maxPerUser);
|
||||
|
||||
return new MonkeyResponse("Friend created", convert(result));
|
||||
return new MonkeyResponse("Connection created", convert(result));
|
||||
}
|
||||
|
||||
export async function deleteRequest(
|
||||
|
|
@ -64,19 +64,19 @@ export async function deleteRequest(
|
|||
const { uid } = req.ctx.decodedToken;
|
||||
const { id } = req.params;
|
||||
|
||||
await FriendsDal.deleteById(uid, id);
|
||||
await ConnectionsDal.deleteById(uid, id);
|
||||
|
||||
return new MonkeyResponse("Friend deleted", null);
|
||||
return new MonkeyResponse("Connection deleted", null);
|
||||
}
|
||||
|
||||
export async function updateRequest(
|
||||
req: MonkeyRequest<undefined, UpdateFriendRequestsRequest, IdPathParams>
|
||||
req: MonkeyRequest<undefined, UpdateConnectionRequest, IdPathParams>
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const { id } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
await FriendsDal.updateStatus(uid, id, status);
|
||||
await ConnectionsDal.updateStatus(uid, id, status);
|
||||
|
||||
return new MonkeyResponse("Friend updated", null);
|
||||
return new MonkeyResponse("Connection updated", null);
|
||||
}
|
||||
|
|
@ -90,7 +90,7 @@ import {
|
|||
import { MILLISECONDS_IN_DAY } from "@monkeytype/util/date-and-time";
|
||||
import { MonkeyRequest } from "../types";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
import * as FriendsDal from "../../dal/friends";
|
||||
import * as ConnectionsDal from "../../dal/connections";
|
||||
|
||||
async function verifyCaptcha(captcha: string): Promise<void> {
|
||||
const { data: verified, error } = await tryCatch(verify(captcha));
|
||||
|
|
@ -294,7 +294,7 @@ export async function deleteUser(req: MonkeyRequest): Promise<MonkeyResponse> {
|
|||
uid,
|
||||
req.ctx.configuration.leaderboards.weeklyXp
|
||||
),
|
||||
FriendsDal.deleteByUid(uid),
|
||||
ConnectionsDal.deleteByUid(uid),
|
||||
]);
|
||||
|
||||
try {
|
||||
|
|
@ -386,7 +386,7 @@ export async function updateName(
|
|||
|
||||
await UserDAL.updateName(uid, name, user.name);
|
||||
|
||||
await FriendsDal.updateName(uid, name);
|
||||
await ConnectionsDal.updateName(uid, name);
|
||||
void addImportantLog(
|
||||
"user_name_updated",
|
||||
`changed name from ${user.name} to ${name}`,
|
||||
|
|
|
|||
24
backend/src/api/routes/connections.ts
Normal file
24
backend/src/api/routes/connections.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { connectionsContract } from "@monkeytype/contracts/connections";
|
||||
import { initServer } from "@ts-rest/express";
|
||||
import { callController } from "../ts-rest-adapter";
|
||||
|
||||
import * as ConnectionsController from "../controllers/connections";
|
||||
|
||||
const s = initServer();
|
||||
export default s.router(connectionsContract, {
|
||||
get: {
|
||||
handler: async (r) => callController(ConnectionsController.getRequests)(r),
|
||||
},
|
||||
create: {
|
||||
handler: async (r) =>
|
||||
callController(ConnectionsController.createRequest)(r),
|
||||
},
|
||||
delete: {
|
||||
handler: async (r) =>
|
||||
callController(ConnectionsController.deleteRequest)(r),
|
||||
},
|
||||
update: {
|
||||
handler: async (r) =>
|
||||
callController(ConnectionsController.updateRequest)(r),
|
||||
},
|
||||
});
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { friendsContract } from "@monkeytype/contracts/friends";
|
||||
import { initServer } from "@ts-rest/express";
|
||||
import { callController } from "../ts-rest-adapter";
|
||||
|
||||
import * as FriendsController from "../controllers/friends";
|
||||
|
||||
const s = initServer();
|
||||
export default s.router(friendsContract, {
|
||||
getRequests: {
|
||||
handler: async (r) => callController(FriendsController.getRequests)(r),
|
||||
},
|
||||
createRequest: {
|
||||
handler: async (r) => callController(FriendsController.createRequest)(r),
|
||||
},
|
||||
deleteRequest: {
|
||||
handler: async (r) => callController(FriendsController.deleteRequest)(r),
|
||||
},
|
||||
updateRequest: {
|
||||
handler: async (r) => callController(FriendsController.updateRequest)(r),
|
||||
},
|
||||
});
|
||||
|
|
@ -16,7 +16,7 @@ import configs from "./configs";
|
|||
import configuration from "./configuration";
|
||||
import { version } from "../../version";
|
||||
import leaderboards from "./leaderboards";
|
||||
import friends from "./friends";
|
||||
import connections from "./connections";
|
||||
import addSwaggerMiddlewares from "./swagger";
|
||||
import { MonkeyResponse } from "../../utils/monkey-response";
|
||||
import {
|
||||
|
|
@ -62,7 +62,7 @@ const router = s.router(contract, {
|
|||
users,
|
||||
quotes,
|
||||
webhooks,
|
||||
friends,
|
||||
connections,
|
||||
});
|
||||
|
||||
export function addApiRoutes(app: Application): void {
|
||||
|
|
|
|||
|
|
@ -1,33 +1,30 @@
|
|||
import { Collection, Filter, ObjectId } from "mongodb";
|
||||
import * as db from "../init/db";
|
||||
import {
|
||||
FriendRequest,
|
||||
FriendRequestStatus,
|
||||
} from "@monkeytype/schemas/friends";
|
||||
import { Connection, ConnectionStatus } from "@monkeytype/schemas/connections";
|
||||
import MonkeyError from "../utils/error";
|
||||
import { WithObjectId } from "../utils/misc";
|
||||
|
||||
export type DBFriendRequest = WithObjectId<
|
||||
FriendRequest & {
|
||||
export type DBConnection = WithObjectId<
|
||||
Connection & {
|
||||
key: string; //sorted uid
|
||||
}
|
||||
>;
|
||||
|
||||
// Export for use in tests
|
||||
export const getCollection = (): Collection<DBFriendRequest> =>
|
||||
db.collection("friends");
|
||||
export const getCollection = (): Collection<DBConnection> =>
|
||||
db.collection("connections");
|
||||
|
||||
export async function getRequests(options: {
|
||||
export async function getConnections(options: {
|
||||
initiatorUid?: string;
|
||||
friendUid?: string;
|
||||
status?: FriendRequestStatus[];
|
||||
}): Promise<DBFriendRequest[]> {
|
||||
status?: ConnectionStatus[];
|
||||
}): Promise<DBConnection[]> {
|
||||
const { initiatorUid, friendUid, status } = options;
|
||||
|
||||
if (initiatorUid === undefined && friendUid === undefined)
|
||||
throw new Error("no filter provided");
|
||||
|
||||
let filter: Filter<DBFriendRequest> = { $or: [] };
|
||||
let filter: Filter<DBConnection> = { $or: [] };
|
||||
|
||||
if (initiatorUid !== undefined) {
|
||||
filter.$or?.push({ initiatorUid });
|
||||
|
|
@ -48,7 +45,7 @@ export async function create(
|
|||
initiator: { uid: string; name: string },
|
||||
friend: { uid: string; name: string },
|
||||
maxPerUser: number
|
||||
): Promise<DBFriendRequest> {
|
||||
): Promise<DBConnection> {
|
||||
const count = await getCollection().countDocuments({
|
||||
initiatorUid: initiator.uid,
|
||||
});
|
||||
|
|
@ -56,12 +53,12 @@ export async function create(
|
|||
if (count >= maxPerUser) {
|
||||
throw new MonkeyError(
|
||||
409,
|
||||
"Maximum number of friends reached",
|
||||
"create friend request"
|
||||
"Maximum number of connections reached",
|
||||
"create connection request"
|
||||
);
|
||||
}
|
||||
try {
|
||||
const created: DBFriendRequest = {
|
||||
const created: DBConnection = {
|
||||
_id: new ObjectId(),
|
||||
key: getKey(initiator.uid, friend.uid),
|
||||
initiatorUid: initiator.uid,
|
||||
|
|
@ -78,7 +75,7 @@ export async function create(
|
|||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (e.name === "MongoServerError" && e.code === 11000) {
|
||||
throw new MonkeyError(409, "Duplicate friend or blocked");
|
||||
throw new MonkeyError(409, "Duplicate connection or blocked");
|
||||
}
|
||||
|
||||
throw e;
|
||||
|
|
@ -86,16 +83,16 @@ export async function create(
|
|||
}
|
||||
|
||||
/**
|
||||
*Update the status of a friend by id
|
||||
*Update the status of a connection by id
|
||||
* @param friendUid
|
||||
* @param id
|
||||
* @param status
|
||||
* @throws MonkeyError if the friend id is unknown or the friendUid does not match
|
||||
* @throws MonkeyError if the connection id is unknown or the friendUid does not match
|
||||
*/
|
||||
export async function updateStatus(
|
||||
friendUid: string,
|
||||
id: string,
|
||||
status: FriendRequestStatus
|
||||
status: ConnectionStatus
|
||||
): Promise<void> {
|
||||
const updateResult = await getCollection().updateOne(
|
||||
{
|
||||
|
|
@ -106,15 +103,15 @@ export async function updateStatus(
|
|||
);
|
||||
|
||||
if (updateResult.matchedCount === 0) {
|
||||
throw new MonkeyError(404, "Friend not found");
|
||||
throw new MonkeyError(404, "Connection not found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* delete a friend by the id.
|
||||
* delete a connection by the id.
|
||||
* @param uid
|
||||
* @param id
|
||||
* @throws MonkeyError if the friend id is unknown or uid does not match
|
||||
* @throws MonkeyError if the connection id is unknown or uid does not match
|
||||
*/
|
||||
export async function deleteById(uid: string, id: string): Promise<void> {
|
||||
const deletionResult = await getCollection().deleteOne({
|
||||
|
|
@ -137,7 +134,7 @@ export async function deleteById(uid: string, id: string): Promise<void> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Update all friends for the uid (initiator or friend) with the given name.
|
||||
* Update all connections for the uid (initiator or friend) with the given name.
|
||||
* @param uid
|
||||
* @param newName
|
||||
*/
|
||||
|
|
@ -159,7 +156,7 @@ export async function updateName(uid: string, newName: string): Promise<void> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Remove all friends containing the uid as initiatorUid or friendUid
|
||||
* Remove all connections containing the uid as initiatorUid or friendUid
|
||||
* @param uid
|
||||
*/
|
||||
export async function deleteByUid(uid: string): Promise<void> {
|
||||
|
|
@ -179,6 +176,6 @@ export async function createIndicies(): Promise<void> {
|
|||
await getCollection().createIndex({ initiatorUid: 1 });
|
||||
await getCollection().createIndex({ friendUid: 1 });
|
||||
|
||||
//make sure there is only one friend entry for each friend/creator pair
|
||||
//make sure there is only one connection for each friend/creator pair
|
||||
await getCollection().createIndex({ key: 1 }, { unique: true });
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ import { Result as ResultType } from "@monkeytype/schemas/results";
|
|||
import { Configuration } from "@monkeytype/schemas/configuration";
|
||||
import { isToday, isYesterday } from "@monkeytype/util/date-and-time";
|
||||
import GeorgeQueue from "../queues/george-queue";
|
||||
import { getCollection as getConnectionCollection } from "./friends";
|
||||
import { getCollection as getConnectionCollection } from "./connections";
|
||||
|
||||
export type DBUserTag = WithObjectId<UserTag>;
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import * as EmailClient from "./init/email-client";
|
|||
import { init as initFirebaseAdmin } from "./init/firebase-admin";
|
||||
import { createIndicies as leaderboardDbSetup } from "./dal/leaderboards";
|
||||
import { createIndicies as blocklistDbSetup } from "./dal/blocklist";
|
||||
import { createIndicies as friendsDbSetup } from "./dal/friends";
|
||||
import { createIndicies as connectionsDbSetup } from "./dal/connections";
|
||||
import { getErrorMessage } from "./utils/error";
|
||||
|
||||
async function bootServer(port: number): Promise<Server> {
|
||||
|
|
@ -77,8 +77,8 @@ async function bootServer(port: number): Promise<Server> {
|
|||
Logger.info("Setting up blocklist indicies...");
|
||||
await blocklistDbSetup();
|
||||
|
||||
Logger.info("Setting up friends indicies...");
|
||||
await friendsDbSetup();
|
||||
Logger.info("Setting up connections indicies...");
|
||||
await connectionsDbSetup();
|
||||
|
||||
recordServerVersion(version);
|
||||
} catch (error) {
|
||||
|
|
|
|||
136
packages/contracts/src/connections.ts
Normal file
136
packages/contracts/src/connections.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { initContract } from "@ts-rest/core";
|
||||
|
||||
import {
|
||||
ConnectionSchema,
|
||||
ConnectionStatusSchema,
|
||||
ConnectionTypeSchema,
|
||||
} from "@monkeytype/schemas/connections";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CommonResponses,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./util/api";
|
||||
import { IdSchema } from "@monkeytype/schemas/util";
|
||||
|
||||
const c = initContract();
|
||||
|
||||
export const GetConnectionsResponseSchema = responseWithData(
|
||||
z.array(ConnectionSchema)
|
||||
);
|
||||
export type GetConnectionsResponse = z.infer<
|
||||
typeof GetConnectionsResponseSchema
|
||||
>;
|
||||
|
||||
export const GetConnectionsQuerySchema = z.object({
|
||||
status: z
|
||||
.array(ConnectionStatusSchema)
|
||||
.or(ConnectionStatusSchema.transform((it) => [it]))
|
||||
.optional(),
|
||||
type: z
|
||||
.array(ConnectionTypeSchema)
|
||||
.or(ConnectionTypeSchema.transform((it) => [it]))
|
||||
.optional(),
|
||||
});
|
||||
export type GetConnectionsQuery = z.infer<typeof GetConnectionsQuerySchema>;
|
||||
|
||||
export const CreateConnectionRequestSchema = ConnectionSchema.pick({
|
||||
friendName: true,
|
||||
});
|
||||
export type CreateConnectionRequest = z.infer<
|
||||
typeof CreateConnectionRequestSchema
|
||||
>;
|
||||
|
||||
export const CreateConnectionResponseSchema =
|
||||
responseWithData(ConnectionSchema);
|
||||
export type CreateConnectionResponse = z.infer<
|
||||
typeof CreateConnectionResponseSchema
|
||||
>;
|
||||
|
||||
export const IdPathParamsSchema = z.object({
|
||||
id: IdSchema,
|
||||
});
|
||||
export type IdPathParams = z.infer<typeof IdPathParamsSchema>;
|
||||
|
||||
export const UpdateConnectionRequestSchema = z.object({
|
||||
status: ConnectionStatusSchema.exclude(["pending"]),
|
||||
});
|
||||
export type UpdateConnectionRequest = z.infer<
|
||||
typeof UpdateConnectionRequestSchema
|
||||
>;
|
||||
|
||||
export const connectionsContract = c.router(
|
||||
{
|
||||
get: {
|
||||
summary: "get connections",
|
||||
description: "Get connections of the current user",
|
||||
method: "GET",
|
||||
path: "/",
|
||||
query: GetConnectionsQuerySchema.strict(),
|
||||
responses: {
|
||||
200: GetConnectionsResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "connectionGet",
|
||||
}),
|
||||
},
|
||||
create: {
|
||||
summary: "create connection",
|
||||
description: "Request a connection to a user ",
|
||||
method: "POST",
|
||||
path: "/",
|
||||
body: CreateConnectionRequestSchema.strict(),
|
||||
responses: {
|
||||
200: CreateConnectionResponseSchema,
|
||||
404: MonkeyResponseSchema.describe("FriendUid unknown"),
|
||||
409: MonkeyResponseSchema.describe(
|
||||
"Duplicate connection, blocked or max connections reached"
|
||||
),
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "connectionCreate",
|
||||
}),
|
||||
},
|
||||
delete: {
|
||||
summary: "delete connection",
|
||||
description: "Delete a connection",
|
||||
method: "DELETE",
|
||||
path: "/:id",
|
||||
pathParams: IdPathParamsSchema.strict(),
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "connectionDelete",
|
||||
}),
|
||||
},
|
||||
update: {
|
||||
summary: "update connection",
|
||||
description: "Update a connection status",
|
||||
method: "PATCH",
|
||||
path: "/:id",
|
||||
pathParams: IdPathParamsSchema.strict(),
|
||||
body: UpdateConnectionRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "connectionUpdate",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/connections",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "connections",
|
||||
requireConfiguration: {
|
||||
path: "connections.enabled",
|
||||
invalidMessage: "Connections are not available at this time.",
|
||||
},
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
);
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
import { initContract } from "@ts-rest/core";
|
||||
|
||||
import {
|
||||
FriendRequestSchema,
|
||||
FriendRequestStatusSchema,
|
||||
FriendRequestTypeSchema,
|
||||
} from "@monkeytype/schemas/friends";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CommonResponses,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./util/api";
|
||||
import { IdSchema } from "@monkeytype/schemas/util";
|
||||
|
||||
const c = initContract();
|
||||
|
||||
export const GetFriendRequestsResponseSchema = responseWithData(
|
||||
z.array(FriendRequestSchema)
|
||||
);
|
||||
export type GetFriendRequestsResponse = z.infer<
|
||||
typeof GetFriendRequestsResponseSchema
|
||||
>;
|
||||
|
||||
export const GetFriendRequestsQuerySchema = z.object({
|
||||
status: z
|
||||
.array(FriendRequestStatusSchema)
|
||||
.or(FriendRequestStatusSchema.transform((it) => [it]))
|
||||
.optional(),
|
||||
type: z
|
||||
.array(FriendRequestTypeSchema)
|
||||
.or(FriendRequestTypeSchema.transform((it) => [it]))
|
||||
.optional(),
|
||||
});
|
||||
export type GetFriendRequestsQuery = z.infer<
|
||||
typeof GetFriendRequestsQuerySchema
|
||||
>;
|
||||
|
||||
export const CreateFriendRequestRequestSchema = FriendRequestSchema.pick({
|
||||
friendName: true,
|
||||
});
|
||||
export type CreateFriendRequestRequest = z.infer<
|
||||
typeof CreateFriendRequestRequestSchema
|
||||
>;
|
||||
|
||||
export const CreateFriendRequestResponseSchema =
|
||||
responseWithData(FriendRequestSchema);
|
||||
export type CreateFriendRequestResponse = z.infer<
|
||||
typeof CreateFriendRequestResponseSchema
|
||||
>;
|
||||
|
||||
export const IdPathParamsSchema = z.object({
|
||||
id: IdSchema,
|
||||
});
|
||||
export type IdPathParams = z.infer<typeof IdPathParamsSchema>;
|
||||
|
||||
export const UpdateFriendRequestsRequestSchema = z.object({
|
||||
status: FriendRequestStatusSchema.exclude(["pending"]),
|
||||
});
|
||||
export type UpdateFriendRequestsRequest = z.infer<
|
||||
typeof UpdateFriendRequestsRequestSchema
|
||||
>;
|
||||
|
||||
export const friendsContract = c.router(
|
||||
{
|
||||
getRequests: {
|
||||
summary: "get friend requests",
|
||||
description: "Get friend requests of the current user",
|
||||
method: "GET",
|
||||
path: "/requests",
|
||||
query: GetFriendRequestsQuerySchema.strict(),
|
||||
responses: {
|
||||
200: GetFriendRequestsResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "friendRequestsGet",
|
||||
}),
|
||||
},
|
||||
createRequest: {
|
||||
summary: "create friend request",
|
||||
description: "Request a user to become a friend",
|
||||
method: "POST",
|
||||
path: "/requests",
|
||||
body: CreateFriendRequestRequestSchema.strict(),
|
||||
responses: {
|
||||
200: CreateFriendRequestResponseSchema,
|
||||
404: MonkeyResponseSchema.describe("FriendUid unknown"),
|
||||
409: MonkeyResponseSchema.describe(
|
||||
"Duplicate friend, blocked or max friends reached"
|
||||
),
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "friendRequestsCreate",
|
||||
}),
|
||||
},
|
||||
deleteRequest: {
|
||||
summary: "delete friend request",
|
||||
description: "Delete a friend request",
|
||||
method: "DELETE",
|
||||
path: "/requests/:id",
|
||||
pathParams: IdPathParamsSchema.strict(),
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "friendRequestsDelete",
|
||||
}),
|
||||
},
|
||||
updateRequest: {
|
||||
summary: "update friend request",
|
||||
description: "Update a friend request status",
|
||||
method: "PATCH",
|
||||
path: "/requests/:id",
|
||||
pathParams: IdPathParamsSchema.strict(),
|
||||
body: UpdateFriendRequestsRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "friendRequestsUpdate",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/friends",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "friends",
|
||||
requireConfiguration: {
|
||||
path: "connections.enabled",
|
||||
invalidMessage: "Friends are not available at this time.",
|
||||
},
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
);
|
||||
|
|
@ -12,7 +12,7 @@ import { devContract } from "./dev";
|
|||
import { usersContract } from "./users";
|
||||
import { quotesContract } from "./quotes";
|
||||
import { webhooksContract } from "./webhooks";
|
||||
import { friendsContract } from "./friends";
|
||||
import { connectionsContract } from "./connections";
|
||||
|
||||
const c = initContract();
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ export const contract = c.router({
|
|||
users: usersContract,
|
||||
quotes: quotesContract,
|
||||
webhooks: webhooksContract,
|
||||
friends: friendsContract,
|
||||
connections: connectionsContract,
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -346,6 +346,11 @@ export const limits = {
|
|||
max: 60,
|
||||
},
|
||||
|
||||
userFriendGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
// ApeKeys Routing
|
||||
apeKeysGet: {
|
||||
window: "hour",
|
||||
|
|
@ -362,27 +367,22 @@ export const limits = {
|
|||
max: 1,
|
||||
},
|
||||
|
||||
friendRequestsGet: {
|
||||
connectionGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
friendRequestsCreate: {
|
||||
connectionCreate: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
friendRequestsDelete: {
|
||||
connectionDelete: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
friendRequestsUpdate: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
friendGet: {
|
||||
connectionUpdate: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -939,10 +939,10 @@ export const usersContract = c.router(
|
|||
200: GetFriendsResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "friendGet",
|
||||
rateLimit: "userFriendGet",
|
||||
requireConfiguration: {
|
||||
path: "connections.enabled",
|
||||
invalidMessage: "Friends are not available at this time.",
|
||||
invalidMessage: "Connections are not available at this time.",
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export type OpenApiTag =
|
|||
| "users"
|
||||
| "quotes"
|
||||
| "webhooks"
|
||||
| "friends";
|
||||
| "connections";
|
||||
|
||||
export type PermissionId =
|
||||
| "quoteMod"
|
||||
|
|
|
|||
24
packages/schemas/src/connections.ts
Normal file
24
packages/schemas/src/connections.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { z } from "zod";
|
||||
import { IdSchema } from "./util";
|
||||
|
||||
export const ConnectionStatusSchema = z.enum([
|
||||
"pending",
|
||||
"accepted",
|
||||
"blocked",
|
||||
]);
|
||||
export type ConnectionStatus = z.infer<typeof ConnectionStatusSchema>;
|
||||
|
||||
export const ConnectionTypeSchema = z.enum(["incoming", "outgoing"]);
|
||||
export type ConnectionType = z.infer<typeof ConnectionTypeSchema>;
|
||||
|
||||
export const ConnectionSchema = z.object({
|
||||
_id: IdSchema,
|
||||
initiatorUid: IdSchema,
|
||||
initiatorName: z.string(),
|
||||
friendUid: IdSchema,
|
||||
friendName: z.string(),
|
||||
addedAt: z.number().int().nonnegative(),
|
||||
status: ConnectionStatusSchema,
|
||||
});
|
||||
|
||||
export type Connection = z.infer<typeof ConnectionSchema>;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { z } from "zod";
|
||||
import { IdSchema } from "./util";
|
||||
|
||||
export const FriendRequestStatusSchema = z.enum([
|
||||
"pending",
|
||||
"accepted",
|
||||
"blocked",
|
||||
]);
|
||||
export type FriendRequestStatus = z.infer<typeof FriendRequestStatusSchema>;
|
||||
|
||||
export const FriendRequestTypeSchema = z.enum(["incoming", "outgoing"]);
|
||||
export type FriendRequestType = z.infer<typeof FriendRequestTypeSchema>;
|
||||
|
||||
export const FriendRequestSchema = z.object({
|
||||
_id: IdSchema,
|
||||
initiatorUid: IdSchema,
|
||||
initiatorName: z.string(),
|
||||
friendUid: IdSchema,
|
||||
friendName: z.string(),
|
||||
addedAt: z.number().int().nonnegative(),
|
||||
status: FriendRequestStatusSchema,
|
||||
});
|
||||
|
||||
export type FriendRequest = z.infer<typeof FriendRequestSchema>;
|
||||
Loading…
Add table
Reference in a new issue