From a65a81b464c4d1d61935ad0dc46d74f2f73bc3be Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 10 Sep 2025 13:56:29 +0200 Subject: [PATCH] rename friends to connections --- .../{friends.spec.ts => connections.spec.ts} | 153 ++++++++++-------- backend/__tests__/__testData__/connections.ts | 10 +- .../{friends.spec.ts => connections.spec.ts} | 122 +++++++------- .../__tests__/api/controllers/user.spec.ts | 8 +- backend/scripts/openapi.ts | 6 +- .../{friends.ts => connections.ts} | 48 +++--- backend/src/api/controllers/user.ts | 6 +- backend/src/api/routes/connections.ts | 24 +++ backend/src/api/routes/friends.ts | 21 --- backend/src/api/routes/index.ts | 4 +- .../src/dal/{friends.ts => connections.ts} | 49 +++--- backend/src/dal/user.ts | 2 +- backend/src/server.ts | 6 +- packages/contracts/src/connections.ts | 136 ++++++++++++++++ packages/contracts/src/friends.ts | 138 ---------------- packages/contracts/src/index.ts | 4 +- packages/contracts/src/rate-limit/index.ts | 18 +-- packages/contracts/src/users.ts | 4 +- packages/contracts/src/util/api.ts | 2 +- packages/schemas/src/connections.ts | 24 +++ packages/schemas/src/friends.ts | 24 --- 21 files changed, 417 insertions(+), 392 deletions(-) rename backend/__tests__/__integration__/dal/{friends.spec.ts => connections.spec.ts} (55%) rename backend/__tests__/api/controllers/{friends.spec.ts => connections.spec.ts} (75%) rename backend/src/api/controllers/{friends.ts => connections.ts} (54%) create mode 100644 backend/src/api/routes/connections.ts delete mode 100644 backend/src/api/routes/friends.ts rename backend/src/dal/{friends.ts => connections.ts} (74%) create mode 100644 packages/contracts/src/connections.ts delete mode 100644 packages/contracts/src/friends.ts create mode 100644 packages/schemas/src/connections.ts delete mode 100644 packages/schemas/src/friends.ts diff --git a/backend/__tests__/__integration__/dal/friends.spec.ts b/backend/__tests__/__integration__/dal/connections.spec.ts similarity index 55% rename from backend/__tests__/__integration__/dal/friends.spec.ts rename to backend/__tests__/__integration__/dal/connections.spec.ts index c0593b486..fea09fa95 100644 --- a/backend/__tests__/__integration__/dal/friends.spec.ts +++ b/backend/__tests__/__integration__/dal/connections.spec.ts @@ -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]); }); }); diff --git a/backend/__tests__/__testData__/connections.ts b/backend/__tests__/__testData__/connections.ts index 9cb2d5c97..2283f451e 100644 --- a/backend/__tests__/__testData__/connections.ts +++ b/backend/__tests__/__testData__/connections.ts @@ -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, + data: Partial, maxPerUser = 25 -): Promise { - const result = await FriendsDal.create( +): Promise { + 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 } ); diff --git a/backend/__tests__/api/controllers/friends.spec.ts b/backend/__tests__/api/controllers/connections.spec.ts similarity index 75% rename from backend/__tests__/api/controllers/friends.spec.ts rename to backend/__tests__/api/controllers/connections.spec.ts index 8f8a72937..9483b10b5 100644 --- a/backend/__tests__/api/controllers/friends.spec.ts +++ b/backend/__tests__/api/controllers/connections.spec.ts @@ -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 { +async function enablleConnectionsEndpoints(enabled: boolean): Promise { const mockConfig = _.merge(await configuration, { connections: { enabled }, }); @@ -390,7 +392,7 @@ async function enableFriendsEndpoints(enabled: boolean): Promise { ); } async function expectFailForDisabledEndpoint(call: SuperTest): Promise { - 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."); } diff --git a/backend/__tests__/api/controllers/user.spec.ts b/backend/__tests__/api/controllers/user.spec.ts index 852bdd639..695f0f765 100644 --- a/backend/__tests__/api/controllers/user.spec.ts +++ b/backend/__tests__/api/controllers/user.spec.ts @@ -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 { async function expectFailForDisabledEndpoint(call: SuperTest): Promise { 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."); } diff --git a/backend/scripts/openapi.ts b/backend/scripts/openapi.ts index 1ae15b452..53eaf52eb 100644 --- a/backend/scripts/openapi.ts +++ b/backend/scripts/openapi.ts @@ -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", }, { diff --git a/backend/src/api/controllers/friends.ts b/backend/src/api/controllers/connections.ts similarity index 54% rename from backend/src/api/controllers/friends.ts rename to backend/src/api/controllers/connections.ts index 19430f3ce..09dc07883 100644 --- a/backend/src/api/controllers/friends.ts +++ b/backend/src/api/controllers/connections.ts @@ -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 -): Promise { + req: MonkeyRequest +): Promise { 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 -): Promise { + req: MonkeyRequest +): Promise { 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 + req: MonkeyRequest ): Promise { 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); } diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 9a8f3309e..b93110516 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -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 { const { data: verified, error } = await tryCatch(verify(captcha)); @@ -294,7 +294,7 @@ export async function deleteUser(req: MonkeyRequest): Promise { 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}`, diff --git a/backend/src/api/routes/connections.ts b/backend/src/api/routes/connections.ts new file mode 100644 index 000000000..a67a4974d --- /dev/null +++ b/backend/src/api/routes/connections.ts @@ -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), + }, +}); diff --git a/backend/src/api/routes/friends.ts b/backend/src/api/routes/friends.ts deleted file mode 100644 index fdd5c7189..000000000 --- a/backend/src/api/routes/friends.ts +++ /dev/null @@ -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), - }, -}); diff --git a/backend/src/api/routes/index.ts b/backend/src/api/routes/index.ts index c0b07a53d..aed51685e 100644 --- a/backend/src/api/routes/index.ts +++ b/backend/src/api/routes/index.ts @@ -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 { diff --git a/backend/src/dal/friends.ts b/backend/src/dal/connections.ts similarity index 74% rename from backend/src/dal/friends.ts rename to backend/src/dal/connections.ts index 3fb9d0908..6467cf242 100644 --- a/backend/src/dal/friends.ts +++ b/backend/src/dal/connections.ts @@ -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 => - db.collection("friends"); +export const getCollection = (): Collection => + db.collection("connections"); -export async function getRequests(options: { +export async function getConnections(options: { initiatorUid?: string; friendUid?: string; - status?: FriendRequestStatus[]; -}): Promise { + status?: ConnectionStatus[]; +}): Promise { const { initiatorUid, friendUid, status } = options; if (initiatorUid === undefined && friendUid === undefined) throw new Error("no filter provided"); - let filter: Filter = { $or: [] }; + let filter: Filter = { $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 { +): Promise { 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 { 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 { const deletionResult = await getCollection().deleteOne({ @@ -137,7 +134,7 @@ export async function deleteById(uid: string, id: string): Promise { } /** - * 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 { } /** - * 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 { @@ -179,6 +176,6 @@ export async function createIndicies(): Promise { 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 }); } diff --git a/backend/src/dal/user.ts b/backend/src/dal/user.ts index 466427655..d125f2cee 100644 --- a/backend/src/dal/user.ts +++ b/backend/src/dal/user.ts @@ -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; diff --git a/backend/src/server.ts b/backend/src/server.ts index d23e0ac87..68c7ca3e5 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -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 { @@ -77,8 +77,8 @@ async function bootServer(port: number): Promise { 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) { diff --git a/packages/contracts/src/connections.ts b/packages/contracts/src/connections.ts new file mode 100644 index 000000000..5e55bc85d --- /dev/null +++ b/packages/contracts/src/connections.ts @@ -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; + +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; + +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, + } +); diff --git a/packages/contracts/src/friends.ts b/packages/contracts/src/friends.ts deleted file mode 100644 index 7d6ce2df8..000000000 --- a/packages/contracts/src/friends.ts +++ /dev/null @@ -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; - -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, - } -); diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index d5ccfaa8f..6a54be9f5 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -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, }); /** diff --git a/packages/contracts/src/rate-limit/index.ts b/packages/contracts/src/rate-limit/index.ts index 0701b80ff..f93b532b0 100644 --- a/packages/contracts/src/rate-limit/index.ts +++ b/packages/contracts/src/rate-limit/index.ts @@ -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, }, diff --git a/packages/contracts/src/users.ts b/packages/contracts/src/users.ts index 3cc47ea45..548d3000d 100644 --- a/packages/contracts/src/users.ts +++ b/packages/contracts/src/users.ts @@ -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.", }, }), }, diff --git a/packages/contracts/src/util/api.ts b/packages/contracts/src/util/api.ts index 1d42ab160..0587aa1f2 100644 --- a/packages/contracts/src/util/api.ts +++ b/packages/contracts/src/util/api.ts @@ -16,7 +16,7 @@ export type OpenApiTag = | "users" | "quotes" | "webhooks" - | "friends"; + | "connections"; export type PermissionId = | "quoteMod" diff --git a/packages/schemas/src/connections.ts b/packages/schemas/src/connections.ts new file mode 100644 index 000000000..da634c384 --- /dev/null +++ b/packages/schemas/src/connections.ts @@ -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; + +export const ConnectionTypeSchema = z.enum(["incoming", "outgoing"]); +export type ConnectionType = z.infer; + +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; diff --git a/packages/schemas/src/friends.ts b/packages/schemas/src/friends.ts deleted file mode 100644 index fbbc00f4f..000000000 --- a/packages/schemas/src/friends.ts +++ /dev/null @@ -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; - -export const FriendRequestTypeSchema = z.enum(["incoming", "outgoing"]); -export type FriendRequestType = z.infer; - -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;