diff --git a/apps/server/env.d.ts b/apps/server/env.d.ts index d16e5d5..44ca325 100644 --- a/apps/server/env.d.ts +++ b/apps/server/env.d.ts @@ -1,6 +1,6 @@ declare namespace NodeJS { export interface ProcessEnv { - NODE_ENV: "development" | "production"; + NODE_ENV: "development" | "production" | "test"; PORT?: string; WS_PORT?: string; DATABASE_URL: string; diff --git a/apps/server/package.json b/apps/server/package.json index 5ec58b6..0d814f8 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -6,6 +6,8 @@ "build": "pnpm generate:schema && tsup src/index.ts", "start": "node dist/index.js", "dev": "pnpm generate:schema && cp ./schema/schema.graphql ../web/data && tsx watch --require dotenv/config src/index.ts", + "test": "vitest run", + "test:watch": "vitest", "format": "pnpm biome format ./src --write", "lint": "pnpm biome lint ./src", "check": "pnpm biome check --apply ./src", @@ -16,6 +18,7 @@ "dependencies": { "@entria/graphql-mongo-helpers": "^1.1.2", "@entria/graphql-mongoose-loader": "^4.4.0", + "@faker-js/faker": "^8.4.1", "@koa/bodyparser": "^5.1.1", "@koa/cors": "^5.0.0", "@koa/router": "^12.0.1", @@ -37,14 +40,15 @@ "koa": "^2.15.3", "koa-logger": "^3.2.1", "koa-mount": "^4.0.0", + "mongodb-memory-server": "^10.0.0", "mongoose": "^8.4.3", "regex-escape": "^3.4.10", + "supertest": "^7.0.0", "tsup": "^8.1.0", "ws": "^8.18.0" }, "devDependencies": { "@biomejs/biome": "^1.8.1", - "@types/ws": "^8.5.11", "@types/bcrypt": "^5.0.2", "@types/jsonwebtoken": "^9.0.6", "@types/koa": "^2.15.0", @@ -52,7 +56,11 @@ "@types/koa__cors": "^5.0.0", "@types/koa__router": "^12.0.4", "@types/node": "^20.14.6", + "@types/supertest": "^6.0.2", + "@types/ws": "^8.5.11", "tsx": "^4.15.6", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "vite-tsconfig-paths": "^5.0.1", + "vitest": "^2.0.5" } } diff --git a/apps/server/schema/schema.graphql b/apps/server/schema/schema.graphql index 32bddd6..14b0cfa 100644 --- a/apps/server/schema/schema.graphql +++ b/apps/server/schema/schema.graphql @@ -147,6 +147,7 @@ type Message implements Node { """mongoose_id""" _id: String! + localId: Int! from: User! delivered: Boolean! deliveredAt: DateTime @@ -336,8 +337,7 @@ input LoginMutationInput { scalar NonEmptyString type SendMessagePayload { - message: Message - chat: ChatEdge + message: MessageEdge clientMutationId: String } @@ -345,7 +345,10 @@ input SendMessageInput { content: NonEmptyString """The recipient id, a user or a chat""" - to: String! + toId: String! + + """A int that identifies the message of a user in chat locally""" + localId: Int! clientMutationId: String } diff --git a/apps/server/scripts/generateSchema.ts b/apps/server/scripts/generateSchema.ts index ca1f5df..9012455 100644 --- a/apps/server/scripts/generateSchema.ts +++ b/apps/server/scripts/generateSchema.ts @@ -1,10 +1,12 @@ -import { schema } from "@/schemas"; import { printSchema } from "graphql"; import fs from "node:fs/promises"; import path from "node:path"; +process.env.SCHEMA_GENERATION = "true"; + (async () => { const filePath = path.resolve(__dirname, "..", "schema", "schema.graphql"); + const { schema } = await import("@/schemas"); await fs.writeFile(filePath, printSchema(schema)); })(); diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index bcd42cf..d1e9443 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -12,7 +12,7 @@ import { useServer } from "graphql-ws/lib/use/ws"; import Koa from "koa"; import logger from "koa-logger"; import mount from "koa-mount"; -import * as ws from "ws"; +import { WebSocketServer } from "ws"; import { buildContext } from "./context"; import { connectDb } from "./database"; import { schema } from "./schemas"; @@ -64,9 +64,11 @@ app.use(async (ctx) => { const port = process.env.PORT ?? "4000"; const server = app.listen(port, async () => { - await connectDb(); + if (process.env.NODE_ENV !== "test") { + await connectDb(); + } - const wsServer = new ws.Server({ + const wsServer = new WebSocketServer({ server, path: "/graphql", }); diff --git a/apps/server/src/modules/chat/fixture.ts b/apps/server/src/modules/chat/fixture.ts new file mode 100644 index 0000000..cef52fd --- /dev/null +++ b/apps/server/src/modules/chat/fixture.ts @@ -0,0 +1,8 @@ +import { type Chat, ChatModel } from "./ChatModel"; + +export const createChat = async (data: Partial | undefined = {}) => { + const chat = await ChatModel.create(data); + await chat.save(); + + return chat; +}; diff --git a/apps/server/src/modules/chat/subscription/TypingStatusSubscription.ts b/apps/server/src/modules/chat/subscription/TypingStatusSubscription.ts index 10719e4..3802c17 100644 --- a/apps/server/src/modules/chat/subscription/TypingStatusSubscription.ts +++ b/apps/server/src/modules/chat/subscription/TypingStatusSubscription.ts @@ -3,6 +3,7 @@ import { UserLoader } from "@/modules/user/UserLoader"; import { UserType } from "@/modules/user/UserType"; import { events, pubSub } from "@/pubsub"; import { GraphQLBoolean, GraphQLNonNull, GraphQLString } from "graphql"; +import { fromGlobalId } from "graphql-relay"; import { subscriptionWithClientId } from "graphql-relay-subscription"; import { withFilter } from "graphql-subscriptions"; @@ -39,7 +40,7 @@ export const OnTypeSubscription = subscriptionWithClientId< () => pubSub.asyncIterator(events.chat.typing), (payload: TypingStatusPayload, input: TypingStatusInput) => { if (input.chatId) { - return input.chatId === payload.chatId; + return fromGlobalId(input.chatId).id === payload.chatId; } return false; }, diff --git a/apps/server/src/modules/chat/util/getChat.ts b/apps/server/src/modules/chat/util/getChat.ts index 5f9733b..35ed710 100644 --- a/apps/server/src/modules/chat/util/getChat.ts +++ b/apps/server/src/modules/chat/util/getChat.ts @@ -1,10 +1,7 @@ import type { Context } from "@/context"; import { ChatModel } from "../ChatModel"; -export async function getChat( - ctx: Context | { userId: string }, - args: { chatId: string }, -) { +export async function getChat(ctx: Context, args: { chatId: string }) { const currentUserId = "userId" in ctx ? ctx.userId : ctx.user?.id.toString(); const id = args.chatId; diff --git a/apps/server/src/modules/message/MessageModel.ts b/apps/server/src/modules/message/MessageModel.ts index d850dbb..105ef30 100644 --- a/apps/server/src/modules/message/MessageModel.ts +++ b/apps/server/src/modules/message/MessageModel.ts @@ -11,6 +11,7 @@ export interface Message extends Document { createdAt: Date; updatedAt: Date; chat: Types.ObjectId; + localId: number; } const MessageSchema = new Schema( @@ -22,6 +23,7 @@ const MessageSchema = new Schema( seenAt: Date, content: { type: String, required: true }, chat: { type: Schema.Types.ObjectId, ref: "Chat", required: true }, + localId: { type: Number, required: true }, }, { timestamps: true, @@ -30,5 +32,6 @@ const MessageSchema = new Schema( ); MessageSchema.index({ createdAt: -1 }); +MessageSchema.index({ from: 1, localId: 1, chat: 1 }, { unique: true }); export const MessageModel = model("Message", MessageSchema); diff --git a/apps/server/src/modules/message/MessageType.ts b/apps/server/src/modules/message/MessageType.ts index 6fefd45..3898863 100644 --- a/apps/server/src/modules/message/MessageType.ts +++ b/apps/server/src/modules/message/MessageType.ts @@ -2,6 +2,7 @@ import type { Context } from "@/context"; import { connectionDefinitions } from "@entria/graphql-mongo-helpers"; import { GraphQLBoolean, + GraphQLInt, GraphQLNonNull, GraphQLObjectType, GraphQLString, @@ -25,6 +26,9 @@ export const MessageType = new GraphQLObjectType({ description: "mongoose_id", resolve: (msg) => msg.id, }, + localId: { + type: new GraphQLNonNull(GraphQLInt), + }, from: { type: new GraphQLNonNull(UserType), resolve: (msg, _args, ctx) => UserLoader.load(ctx, msg.from), diff --git a/apps/server/src/modules/message/mutations/SendMessageMutation.ts b/apps/server/src/modules/message/mutations/SendMessageMutation.ts index fd43407..0de5065 100644 --- a/apps/server/src/modules/message/mutations/SendMessageMutation.ts +++ b/apps/server/src/modules/message/mutations/SendMessageMutation.ts @@ -1,34 +1,23 @@ import type { Context } from "@/context"; -import { ChatLoader } from "@/modules/chat/ChatLoader"; -import { type Chat, ChatModel } from "@/modules/chat/ChatModel"; -import { ChatConnection } from "@/modules/chat/ChatType"; -import { getChat } from "@/modules/chat/util/getChat"; -import { UserModel } from "@/modules/user/UserModel"; -import { events, pubSub } from "@/pubsub"; -import { GraphQLNonNull, GraphQLString } from "graphql"; +import { GraphQLInt, GraphQLNonNull, GraphQLString } from "graphql"; import { fromGlobalId, mutationWithClientMutationId, toGlobalId, } from "graphql-relay"; import { GraphQLNonEmptyString } from "graphql-scalars"; -import { startSession } from "mongoose"; import { MessageLoader } from "../MessageLoader"; -import { MessageModel } from "../MessageModel"; -import { MessageType } from "../MessageType"; -import type { MessageSubscription } from "../subscription/OnMessage"; +import type { Message } from "../MessageModel"; +import { MessageConnection } from "../MessageType"; +import { sendMessage } from "./lib/sendMessage"; -type SendMessageInput = { - to: string; +export type SendMessageInput = { + toId: string; content: string; + localId: number; }; -type SendMessageOutput = { - content: string; - createdAt: Date; - id: string; - chat: Chat | null; -}; +type SendMessageOutput = { message: Message }; export const SendMessage = mutationWithClientMutationId< SendMessageInput, @@ -38,84 +27,44 @@ export const SendMessage = mutationWithClientMutationId< name: "SendMessage", inputFields: { content: { type: GraphQLNonEmptyString }, - to: { + toId: { type: new GraphQLNonNull(GraphQLString), description: "The recipient id, a user or a chat", }, + localId: { + type: new GraphQLNonNull(GraphQLInt), + description: + "A int that identifies the message of a user in chat locally", + }, }, outputFields: { message: { - type: MessageType, - resolve: async (out, _, ctx) => MessageLoader.load(ctx, (await out).id), - }, - chat: { - type: ChatConnection.edgeType, - resolve: async (res, _, ctx) => { - const { chat } = await res; - const node = await ChatLoader.load(ctx, chat?.id); - - if (!node) { - return null; - } - - return { cursor: toGlobalId("Chat", node.id), node }; + type: MessageConnection.edgeType, + resolve: async (out, _, ctx) => { + const node = await MessageLoader.load( + ctx, + (await out).message._id as string, + ); + if (!node) return null; + + return { + cursor: toGlobalId("Message", node.id), + node, + }; }, }, }, - mutateAndGetPayload: async ({ content, to }, ctx) => { + mutateAndGetPayload: async ({ toId, ...data }, ctx) => { if (!ctx.user) { throw new Error("Sender not specified"); } - const toId = fromGlobalId(to).id; - const selfMessage = toId === ctx.user.id.toString(); - - let chat = await getChat(ctx, { chatId: toId }); - - const recipient = await UserModel.findById(toId); - - if (!chat && !recipient) { - throw new Error("Recipient not specified"); - } - - let newChat = false; - - if (!chat) { - newChat = true; - chat = new ChatModel({ - users: selfMessage ? [ctx.user.id] : [recipient?.id, ctx.user.id], - }); - } - - const message = new MessageModel({ - content, - from: ctx.user, + const { message } = await sendMessage({ + ...data, + toId: fromGlobalId(toId).id, + ctx, }); - const session = await startSession(); - - try { - chat.lastMessage = message.id; - message.chat = chat.id; - - await message.save({ session }); - await chat.save({ session }); - } finally { - await session.endSession(); - } - - await pubSub.publish(events.message.new, { - topic: events.message.new, - newMessageId: message.id, - chatId: chat.id, - newChat, - } satisfies MessageSubscription); - - return { - id: toGlobalId("Message", message.id), - content: message.content, - createdAt: message.createdAt, - chat, - }; + return { message }; }, }); diff --git a/apps/server/src/modules/message/mutations/__tests__/SendMessage.spec.ts b/apps/server/src/modules/message/mutations/__tests__/SendMessage.spec.ts new file mode 100644 index 0000000..1a9ced3 --- /dev/null +++ b/apps/server/src/modules/message/mutations/__tests__/SendMessage.spec.ts @@ -0,0 +1,123 @@ +import { generateToken } from "@/auth"; +import type { Chat } from "@/modules/chat/ChatModel"; +import { createChat } from "@/modules/chat/fixture"; +import { createUser } from "@/modules/user/fixture"; +import { executeQuery } from "@/test/utils"; +import { fromGlobalId, toGlobalId } from "graphql-relay"; +import { setTimeout } from "node:timers/promises"; +import { MessageModel } from "../../MessageModel"; + +const sendMessageMutation = ({ + content, + localId, + toId, +}: { content: string; toId: string; localId: number }) => ({ + query: `mutation sendMessageMutation($input: SendMessageInput!) { + sendMessage (input: $input) { + message { + node { + id + } + } + } + } +`, + variables: { + input: { + content, + localId, + toId, + }, + }, +}); + +describe("message/mutations/SendMessage", () => { + it("should be able to send a message to a chat", async () => { + const user = await createUser(); + const chat = await createChat({ users: [user.id] }); + + const token = generateToken(user); + const mutation = sendMessageMutation({ + content: "hello", + toId: toGlobalId("Chat", chat.id), + localId: 0, + }); + + const res = await executeQuery(mutation, token); + + const messageId = fromGlobalId( + res.body.data.sendMessage.message.node.id, + ).id; + const message = await MessageModel.findById(messageId).populate<{ + chat: Chat; + }>("chat"); + + expect(message).toBeDefined(); + expect(message?.content).toEqual("hello"); + expect(message?.from.toString()).toEqual(user.id); + expect(message?.chat.id).toEqual(chat.id); + expect(message?.chat.lastMessage?.toString()).toEqual(message?.id); + }); + + it("should be able send a message to a user", async () => { + const userA = await createUser(); + const userB = await createUser(); + + const token = generateToken(userA); + const mutation = sendMessageMutation({ + content: "hello", + toId: toGlobalId("User", userB.id), + localId: 0, + }); + + const res = await executeQuery(mutation, token); + const messageId = fromGlobalId( + res.body.data.sendMessage.message.node.id, + ).id; + const message = await MessageModel.findById(messageId).populate<{ + chat: Chat; + }>("chat"); + + expect(message?.chat).toBeDefined(); + expect(message?.content).toEqual("hello"); + expect(message?.chat.lastMessage?.toString()).toEqual(messageId); + expect(message?.from.toString()).toEqual(userA.id); + + expect(message?.chat.users.map((id) => id.toString())).toEqual( + expect.arrayContaining([userA.id, userB.id]), + ); + }); + + it("should be able store messages in order", async () => { + const userA = await createUser(); + const chat = await createChat({ users: [userA.id] }); + + const token = generateToken(userA); + + const mutations = Array.from({ length: 3 }).map((_, i) => + sendMessageMutation({ + content: `hello:${i}`, + toId: toGlobalId("User", chat.id), + localId: i, + }), + ); + + await Promise.all( + mutations.map(async (data, i, arr) => { + // force time delay between messages so that messages arrive in reverse order + await setTimeout(1 * (arr.length - i)); + return executeQuery(data, token); + }), + ); + + const messages = await MessageModel.find({ from: userA.id }).sort({ + createdAt: -1, + }); + + expect(messages).toEqual([ + expect.objectContaining({ localId: 2 }), + expect.objectContaining({ localId: 1 }), + expect.objectContaining({ localId: 0 }), + ]); + }); +}); diff --git a/apps/server/src/modules/message/mutations/lib/sendMessage.ts b/apps/server/src/modules/message/mutations/lib/sendMessage.ts new file mode 100644 index 0000000..401486e --- /dev/null +++ b/apps/server/src/modules/message/mutations/lib/sendMessage.ts @@ -0,0 +1,119 @@ +import type { Context } from "@/context"; +import { ChatModel } from "@/modules/chat/ChatModel"; +import { getChat } from "@/modules/chat/util/getChat"; +import { UserModel } from "@/modules/user/UserModel"; +import { events, pubSub } from "@/pubsub"; +import { startSession } from "mongoose"; +import { setInterval } from "node:timers/promises"; +import { MessageModel } from "../../MessageModel"; +import type { MessageSubscription } from "../../subscription/OnMessage"; +import type { SendMessageInput } from "../SendMessageMutation"; + +type SendMessageArgs = { ctx: Context } & SendMessageInput; + +type GetUserLastMessageArgs = { + chatId: string; + senderId: string; +}; + +type CheckLocalIdArgs = GetUserLastMessageArgs & { + localId: number; +}; + +const INTERVAL_TIME = 100; +const MAX_TIMEOUT = 2000; + +const getUserLastMessage = ({ chatId, senderId }: GetUserLastMessageArgs) => + MessageModel.findOne({ + chat: chatId, + from: senderId, + }) + .sort({ createdAt: -1 }) + .exec(); + +const checkLocalId = async ({ + chatId, + localId, + senderId, +}: CheckLocalIdArgs) => { + for await (const startTime of setInterval(INTERVAL_TIME, Date.now())) { + const now = Date.now(); + const senderLastMessage = await getUserLastMessage({ + chatId, + senderId, + }); + + /** We can only assure message ordering if all messages get + to the server within the waiting time, otherwise, instead of + discarding the data, it gets processed right away + **/ + if ( + (!senderLastMessage && localId === 0) || + (senderLastMessage && senderLastMessage.localId + 1 === localId) || + now - startTime > MAX_TIMEOUT + ) { + break; + } + } +}; + +export const sendMessage = async ({ + content, + toId, + localId, + ctx, +}: SendMessageArgs) => { + const fromId = ctx.user?.id; + + const selfMessage = toId === fromId; + + let chat = await getChat(ctx, { chatId: toId }); + + if (chat) { + await checkLocalId({ chatId: chat?.id, senderId: fromId, localId }); + } + + const recipient = await UserModel.findById(toId); + + if (!chat && !recipient) { + throw new Error("Recipient not specified"); + } + + let newChat = false; + + if (!chat) { + newChat = true; + chat = new ChatModel({ + users: selfMessage ? [fromId] : [recipient?.id, fromId], + }); + } + + const message = new MessageModel({ + content, + from: fromId, + localId, + }); + + const session = await startSession(); + + try { + chat.lastMessage = message.id; + message.chat = chat.id; + + await message.save({ session }); + await chat.save({ session }); + } catch (e) { + throw new Error(`unexpected error: \n${e}`); + } finally { + await session.endSession(); + } + + await pubSub.publish(events.message.new, { + topic: events.message.new, + newMessageId: message.id, + chatId: chat.id, + newChat, + } satisfies MessageSubscription); + + return { message, chat, newChat }; +}; diff --git a/apps/server/src/modules/user/fixture.ts b/apps/server/src/modules/user/fixture.ts new file mode 100644 index 0000000..181562d --- /dev/null +++ b/apps/server/src/modules/user/fixture.ts @@ -0,0 +1,22 @@ +import { faker } from "@faker-js/faker"; +import { genSaltSync, hashSync } from "bcrypt"; +import { type User, UserModel } from "./UserModel"; + +export const createUser = async ({ + password, + email, + username, +}: Partial | undefined = {}) => { + const salt = genSaltSync(); + + const hashedPassword = hashSync(password ?? faker.internet.password(), salt); + + const user = await UserModel.create({ + email: email ?? faker.internet.email(), + password: hashedPassword, + username: username ?? faker.internet.userName(), + }); + await user.save(); + + return user; +}; diff --git a/apps/server/src/test/db.ts b/apps/server/src/test/db.ts new file mode 100644 index 0000000..18b1d70 --- /dev/null +++ b/apps/server/src/test/db.ts @@ -0,0 +1,18 @@ +import type { MongoMemoryServer } from "mongodb-memory-server"; +import mongoose from "mongoose"; + +export const connect = async (db: MongoMemoryServer) => { + const uri = db.getUri(); + + return mongoose.connect(uri); +}; + +export const disconnect = async (db: MongoMemoryServer) => { + await mongoose.connection.dropDatabase(); + await mongoose.connection.close(); + return db.stop(); +}; + +export const clear = () => { + return mongoose.connection.dropDatabase(); +}; diff --git a/apps/server/src/test/index.ts b/apps/server/src/test/index.ts new file mode 100644 index 0000000..d1b3c7b --- /dev/null +++ b/apps/server/src/test/index.ts @@ -0,0 +1,18 @@ +import { MongoMemoryServer } from "mongodb-memory-server"; +import { afterAll, afterEach, beforeAll } from "vitest"; +import { clear, connect, disconnect } from "./db"; + +let db: MongoMemoryServer; + +beforeAll(async () => { + db = await MongoMemoryServer.create(); + await connect(db); +}); + +afterEach(async () => { + await clear(); +}); + +afterAll(() => { + disconnect(db); +}); diff --git a/apps/server/src/test/utils.ts b/apps/server/src/test/utils.ts new file mode 100644 index 0000000..fd18871 --- /dev/null +++ b/apps/server/src/test/utils.ts @@ -0,0 +1,23 @@ +import request from "supertest"; +import { app } from ".."; + +type Query = { + query: string; + variables?: object; +}; + +export function executeQuery(query: Query, token?: string) { + const headers: Record = { + Accept: "application/json", + "Content-Type": "application/json", + }; + + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + return request(app.callback()) + .post("/graphql") + .set(headers) + .send(JSON.stringify(query)); +} diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 87322a4..db478f9 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -13,8 +13,8 @@ "sourceMap": true, "noEmit": true, "strict": true, - "types": ["node"] + "types": ["node", "vitest/globals"] }, "exclude": ["node_modules"], - "include": ["env.d.ts", "./**/*.ts"] + "include": ["env.d.ts", "./**/*.ts", "vitest.config.mts"] } diff --git a/apps/server/vitest.config.mts b/apps/server/vitest.config.mts new file mode 100644 index 0000000..e2ec5a1 --- /dev/null +++ b/apps/server/vitest.config.mts @@ -0,0 +1,8 @@ +import tsConfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [tsConfigPaths()], + test: { globals: true, setupFiles: ["dotenv/config", "src/test/index.ts"] }, + resolve: { alias: { graphql: "graphql/index.js" } }, +}); diff --git a/apps/web/__generated__/chatComposerMutation.graphql.ts b/apps/web/__generated__/chatComposerMutation.graphql.ts index c19237c..b6e19bd 100644 --- a/apps/web/__generated__/chatComposerMutation.graphql.ts +++ b/apps/web/__generated__/chatComposerMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<044397bd0e49826331bef9a610ec0757>> + * @generated SignedSource<<323d629a94a6b057b2395a3e0a52042b>> * @lightSyntaxTransform * @nogrep */ @@ -12,14 +12,37 @@ import { ConcreteRequest, Mutation } from 'relay-runtime'; export type SendMessageInput = { clientMutationId?: string | null | undefined; content?: any | null | undefined; - to: string; + localId: number; + toId: string; }; export type chatComposerMutation$variables = { + connections: ReadonlyArray; input: SendMessageInput; }; export type chatComposerMutation$data = { readonly sendMessage: { - readonly clientMutationId: string | null | undefined; + readonly message: { + readonly node: { + readonly chat: { + readonly node: { + readonly group: boolean | null | undefined; + } | null | undefined; + } | null | undefined; + readonly content: string; + readonly createdAt: any | null | undefined; + readonly delivered: boolean; + readonly deliveredAt: any | null | undefined; + readonly from: { + readonly avatar: string | null | undefined; + readonly id: string; + readonly username: string; + }; + readonly id: string; + readonly localId: number; + readonly seen: boolean; + readonly seenAt: any | null | undefined; + } | null | undefined; + } | null | undefined; } | null | undefined; }; export type chatComposerMutation = { @@ -28,67 +51,295 @@ export type chatComposerMutation = { }; const node: ConcreteRequest = (function(){ -var v0 = [ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ { - "defaultValue": null, - "kind": "LocalArgument", - "name": "input" + "kind": "Variable", + "name": "input", + "variableName": "input" } ], -v1 = [ - { - "alias": null, - "args": [ - { - "kind": "Variable", - "name": "input", - "variableName": "input" - } +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "from", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "username", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "avatar", + "storageKey": null + } + ], + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "group", + "storageKey": null +}, +v6 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "localId", + "storageKey": null +}, +v7 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "content", + "storageKey": null +}, +v8 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "seen", + "storageKey": null +}, +v9 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null +}, +v10 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "seenAt", + "storageKey": null +}, +v11 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "delivered", + "storageKey": null +}, +v12 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deliveredAt", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) ], - "concreteType": "SendMessagePayload", - "kind": "LinkedField", - "name": "sendMessage", - "plural": false, + "kind": "Fragment", + "metadata": null, + "name": "chatComposerMutation", "selections": [ { "alias": null, - "args": null, - "kind": "ScalarField", - "name": "clientMutationId", + "args": (v2/*: any*/), + "concreteType": "SendMessagePayload", + "kind": "LinkedField", + "name": "sendMessage", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MessageEdge", + "kind": "LinkedField", + "name": "message", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Message", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "ChatEdge", + "kind": "LinkedField", + "name": "chat", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Chat", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v5/*: any*/) + ], + "storageKey": null + } + ], + "storageKey": null + }, + (v6/*: any*/), + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + (v11/*: any*/), + (v12/*: any*/) + ], + "storageKey": null + } + ], + "storageKey": null + } + ], "storageKey": null } ], - "storageKey": null - } -]; -return { - "fragment": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Fragment", - "metadata": null, - "name": "chatComposerMutation", - "selections": (v1/*: any*/), "type": "mutation", "abstractKey": null }, "kind": "Request", "operation": { - "argumentDefinitions": (v0/*: any*/), + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], "kind": "Operation", "name": "chatComposerMutation", - "selections": (v1/*: any*/) + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "SendMessagePayload", + "kind": "LinkedField", + "name": "sendMessage", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MessageEdge", + "kind": "LinkedField", + "name": "message", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Message", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "ChatEdge", + "kind": "LinkedField", + "name": "chat", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Chat", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v5/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + } + ], + "storageKey": null + }, + (v6/*: any*/), + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + (v11/*: any*/), + (v12/*: any*/) + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "message", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] }, "params": { - "cacheID": "1ca479e1bc361f1a3c9e5e1a853c58cc", + "cacheID": "50b4a47e32619cd9a835377a07e637e6", "id": null, "metadata": {}, "name": "chatComposerMutation", "operationKind": "mutation", - "text": "mutation chatComposerMutation(\n $input: SendMessageInput!\n) {\n sendMessage(input: $input) {\n clientMutationId\n }\n}\n" + "text": "mutation chatComposerMutation(\n $input: SendMessageInput!\n) {\n sendMessage(input: $input) {\n message {\n node {\n id\n from {\n id\n username\n avatar\n }\n chat {\n node {\n group\n id\n }\n }\n localId\n content\n seen\n createdAt\n seenAt\n delivered\n deliveredAt\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "efbc4d12ac37b09e427dedfb7e1f6052"; +(node as any).hash = "ca708d10398ec266de57bea04299cd39"; export default node; diff --git a/apps/web/__generated__/chatLayoutQuery.graphql.ts b/apps/web/__generated__/chatLayoutQuery.graphql.ts index 0175cc4..dd494e6 100644 --- a/apps/web/__generated__/chatLayoutQuery.graphql.ts +++ b/apps/web/__generated__/chatLayoutQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -231,6 +231,13 @@ return { "plural": false, "selections": [ (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "localId", + "storageKey": null + }, { "alias": null, "args": null, @@ -369,12 +376,12 @@ return { ] }, "params": { - "cacheID": "243ef7a71126e734ab42b8dbe599ac17", + "cacheID": "732b408bd983bd89b09419583d18043b", "id": null, "metadata": {}, "name": "chatLayoutQuery", "operationKind": "query", - "text": "query chatLayoutQuery(\n $chatId: String!\n) {\n user(userId: $chatId) {\n id\n username\n avatar\n }\n chat(id: $chatId) {\n id\n }\n ...chatHeaderFragment_1IzeVY\n ...chatMessagesFragment_1IzeVY\n}\n\nfragment chatHeaderFragment_1IzeVY on query {\n chat(id: $chatId) {\n name\n group\n users {\n edges {\n node {\n id\n avatar\n username\n }\n }\n }\n id\n }\n user(userId: $chatId) {\n id\n username\n }\n}\n\nfragment chatMessageFragment on Message {\n id\n from {\n id\n username\n avatar\n }\n chat {\n node {\n group\n id\n }\n }\n content\n seen\n createdAt\n seenAt\n delivered\n deliveredAt\n}\n\nfragment chatMessagesFragment_1IzeVY on query {\n messages(chatId: $chatId, first: 50) {\n edges {\n node {\n id\n from {\n id\n }\n ...chatMessageFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n" + "text": "query chatLayoutQuery(\n $chatId: String!\n) {\n user(userId: $chatId) {\n id\n username\n avatar\n }\n chat(id: $chatId) {\n id\n }\n ...chatHeaderFragment_1IzeVY\n ...chatMessagesFragment_1IzeVY\n}\n\nfragment chatHeaderFragment_1IzeVY on query {\n chat(id: $chatId) {\n name\n group\n users {\n edges {\n node {\n id\n avatar\n username\n }\n }\n }\n id\n }\n user(userId: $chatId) {\n id\n username\n }\n}\n\nfragment chatMessageFragment on Message {\n id\n from {\n id\n username\n avatar\n }\n chat {\n node {\n group\n id\n }\n }\n localId\n content\n seen\n createdAt\n seenAt\n delivered\n deliveredAt\n}\n\nfragment chatMessagesFragment_1IzeVY on query {\n messages(chatId: $chatId, first: 50) {\n edges {\n node {\n id\n localId\n from {\n id\n }\n ...chatMessageFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n" } }; })(); diff --git a/apps/web/__generated__/chatMessageFragment.graphql.ts b/apps/web/__generated__/chatMessageFragment.graphql.ts index c08454f..cba5f4b 100644 --- a/apps/web/__generated__/chatMessageFragment.graphql.ts +++ b/apps/web/__generated__/chatMessageFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<0724794ea3682bff7d8c96b25a6f2755>> * @lightSyntaxTransform * @nogrep */ @@ -26,6 +26,7 @@ export type chatMessageFragment$data = { readonly username: string; }; readonly id: string; + readonly localId: number; readonly seen: boolean; readonly seenAt: any | null | undefined; readonly " $fragmentType": "chatMessageFragment"; @@ -105,6 +106,13 @@ return { ], "storageKey": null }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "localId", + "storageKey": null + }, { "alias": null, "args": null, @@ -153,6 +161,6 @@ return { }; })(); -(node as any).hash = "69ac8b9d382c8b2a570c8da9a53a8248"; +(node as any).hash = "c1b896fd64e8959c31efc7d064a7cf53"; export default node; diff --git a/apps/web/__generated__/chatMessagesFragment.graphql.ts b/apps/web/__generated__/chatMessagesFragment.graphql.ts index ef99fc3..6579602 100644 --- a/apps/web/__generated__/chatMessagesFragment.graphql.ts +++ b/apps/web/__generated__/chatMessagesFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<53b53e2c2e6bdb0d53d77b0278e546af>> + * @generated SignedSource<<9451c17cceca026773b44b9272fd51aa>> * @lightSyntaxTransform * @nogrep */ @@ -18,6 +18,7 @@ export type chatMessagesFragment$data = { readonly id: string; }; readonly id: string; + readonly localId: number; readonly " $fragmentSpreads": FragmentRefs<"chatMessageFragment">; } | null | undefined; } | null | undefined>; @@ -114,6 +115,13 @@ return { "plural": false, "selections": [ (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "localId", + "storageKey": null + }, { "alias": null, "args": null, @@ -185,6 +193,6 @@ return { }; })(); -(node as any).hash = "d4d208719c9620430a8297fb07ec51fc"; +(node as any).hash = "d5f48072beb9a3c1787fc8c39521da39"; export default node; diff --git a/apps/web/__generated__/chatMessagesRefetchQuery.graphql.ts b/apps/web/__generated__/chatMessagesRefetchQuery.graphql.ts index a89eb5b..a293e70 100644 --- a/apps/web/__generated__/chatMessagesRefetchQuery.graphql.ts +++ b/apps/web/__generated__/chatMessagesRefetchQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -125,6 +125,13 @@ return { "plural": false, "selections": [ (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "localId", + "storageKey": null + }, { "alias": null, "args": null, @@ -285,16 +292,16 @@ return { ] }, "params": { - "cacheID": "5e08ff1ad4d6e8ac4d30c6d1f2bb699c", + "cacheID": "ef7127718b5ecc58b5b3885885b69c49", "id": null, "metadata": {}, "name": "chatMessagesRefetchQuery", "operationKind": "query", - "text": "query chatMessagesRefetchQuery(\n $chatId: String!\n $count: Int = 50\n $cursor: String\n) {\n ...chatMessagesFragment_2v0QF4\n}\n\nfragment chatMessageFragment on Message {\n id\n from {\n id\n username\n avatar\n }\n chat {\n node {\n group\n id\n }\n }\n content\n seen\n createdAt\n seenAt\n delivered\n deliveredAt\n}\n\nfragment chatMessagesFragment_2v0QF4 on query {\n messages(chatId: $chatId, after: $cursor, first: $count) {\n edges {\n node {\n id\n from {\n id\n }\n ...chatMessageFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n" + "text": "query chatMessagesRefetchQuery(\n $chatId: String!\n $count: Int = 50\n $cursor: String\n) {\n ...chatMessagesFragment_2v0QF4\n}\n\nfragment chatMessageFragment on Message {\n id\n from {\n id\n username\n avatar\n }\n chat {\n node {\n group\n id\n }\n }\n localId\n content\n seen\n createdAt\n seenAt\n delivered\n deliveredAt\n}\n\nfragment chatMessagesFragment_2v0QF4 on query {\n messages(chatId: $chatId, after: $cursor, first: $count) {\n edges {\n node {\n id\n localId\n from {\n id\n }\n ...chatMessageFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n" } }; })(); -(node as any).hash = "d4d208719c9620430a8297fb07ec51fc"; +(node as any).hash = "d5f48072beb9a3c1787fc8c39521da39"; export default node; diff --git a/apps/web/__generated__/useInboxSubscription.graphql.ts b/apps/web/__generated__/useInboxSubscription.graphql.ts index f3c3156..b1ec47c 100644 --- a/apps/web/__generated__/useInboxSubscription.graphql.ts +++ b/apps/web/__generated__/useInboxSubscription.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<94f6d63ce001ec51e6f93931350998d2>> + * @generated SignedSource<<33a4a2cea7c5f0af22d5d69172d69158>> * @lightSyntaxTransform * @nogrep */ @@ -384,6 +384,13 @@ return { ], "storageKey": null }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "localId", + "storageKey": null + }, (v12/*: any*/), { "alias": null, @@ -426,12 +433,12 @@ return { ] }, "params": { - "cacheID": "b24ba7a7dbc3c8b71766e36ee69bd637", + "cacheID": "b836ee4584f4f8d13f0707c8f81fd43a", "id": null, "metadata": {}, "name": "useInboxSubscription", "operationKind": "subscription", - "text": "subscription useInboxSubscription(\n $input: MessageInput!\n) {\n onMessage(input: $input) {\n newChat\n deletedMessages\n deletedChat\n chat {\n id\n group\n user {\n id\n }\n updatedAt\n ...chatItemFragment\n }\n newMessage {\n node {\n id\n from {\n id\n }\n ...chatMessageFragment\n }\n }\n }\n}\n\nfragment chatItemFragment on Chat {\n id\n name\n group\n user {\n id\n username\n avatar\n }\n lastMessage {\n node {\n id\n createdAt\n content\n }\n }\n}\n\nfragment chatMessageFragment on Message {\n id\n from {\n id\n username\n avatar\n }\n chat {\n node {\n group\n id\n }\n }\n content\n seen\n createdAt\n seenAt\n delivered\n deliveredAt\n}\n" + "text": "subscription useInboxSubscription(\n $input: MessageInput!\n) {\n onMessage(input: $input) {\n newChat\n deletedMessages\n deletedChat\n chat {\n id\n group\n user {\n id\n }\n updatedAt\n ...chatItemFragment\n }\n newMessage {\n node {\n id\n from {\n id\n }\n ...chatMessageFragment\n }\n }\n }\n}\n\nfragment chatItemFragment on Chat {\n id\n name\n group\n user {\n id\n username\n avatar\n }\n lastMessage {\n node {\n id\n createdAt\n content\n }\n }\n}\n\nfragment chatMessageFragment on Message {\n id\n from {\n id\n username\n avatar\n }\n chat {\n node {\n group\n id\n }\n }\n localId\n content\n seen\n createdAt\n seenAt\n delivered\n deliveredAt\n}\n" } }; })(); diff --git a/apps/web/data/schema.graphql b/apps/web/data/schema.graphql index 32bddd6..14b0cfa 100644 --- a/apps/web/data/schema.graphql +++ b/apps/web/data/schema.graphql @@ -147,6 +147,7 @@ type Message implements Node { """mongoose_id""" _id: String! + localId: Int! from: User! delivered: Boolean! deliveredAt: DateTime @@ -336,8 +337,7 @@ input LoginMutationInput { scalar NonEmptyString type SendMessagePayload { - message: Message - chat: ChatEdge + message: MessageEdge clientMutationId: String } @@ -345,7 +345,10 @@ input SendMessageInput { content: NonEmptyString """The recipient id, a user or a chat""" - to: String! + toId: String! + + """A int that identifies the message of a user in chat locally""" + localId: Int! clientMutationId: String } diff --git a/apps/web/src/components/chat/chat-composer.tsx b/apps/web/src/components/chat/chat-composer.tsx index 723f2ba..2fa5be2 100644 --- a/apps/web/src/components/chat/chat-composer.tsx +++ b/apps/web/src/components/chat/chat-composer.tsx @@ -1,15 +1,20 @@ import { useUser } from "@/hooks/useUser"; +import { getNextLocalId, setNextLocalId } from "@/utils/localId"; import { Button } from "@ui/components"; import { SendHorizonal, Trash, X } from "lucide-react"; import { useEffect, useRef, useState } from "react"; -import { graphql, useMutation } from "react-relay"; +import { ConnectionHandler, graphql, useMutation } from "react-relay"; import type { chatComposerMutation } from "../../../__generated__/chatComposerMutation.graphql"; import type { chatComposerSendTypingStatusMutation } from "../../../__generated__/chatComposerSendTypingStatusMutation.graphql"; const SendMessageMutation = graphql` - mutation chatComposerMutation($input: SendMessageInput!) { + mutation chatComposerMutation($input: SendMessageInput!, $connections: [ID!]!) { sendMessage (input: $input){ - clientMutationId + message @prependEdge(connections: $connections){ + node { + ...chatMessageFragment @relay(mask: false) + } + } } } `; @@ -23,7 +28,8 @@ const SendTypingStatusMutation = graphql` `; type ChatComposerProps = { - chatId: string; + recipientId: string; + chatId?: string; selectable?: boolean; onCancelSelection?: () => void; onDelete?: () => void; @@ -33,6 +39,7 @@ export function ChatComposer({ onDelete, selectable, onCancelSelection, + recipientId, chatId, }: ChatComposerProps) { const currUser = useUser(); @@ -50,21 +57,63 @@ export function ChatComposer({ ); const onSendTypingStatus = (typing: boolean) => { - if (chatId && currUser) { + if (recipientId && currUser) { sendTypingStatus({ - variables: { input: { chatId, typing } }, + variables: { input: { chatId: recipientId, typing } }, }); } }; - const onSendMessage = () => { + const onSendMessage = async () => { if (textbox.current?.innerText.trim()) { + const content = textbox.current.innerText.trim(); + const localId = chatId ? getNextLocalId(chatId) : 0; + sendMessage({ + updater: (store) => { + if (!chatId) { + store.invalidateStore(); + } + }, + optimisticResponse: { + sendMessage: { + message: { + node: { + id: `${recipientId}:${currUser?.id}:${localId}`, + content, + from: { + avatar: currUser?.avatar, + id: currUser?.id ?? "", + username: currUser?.username ?? "", + }, + localId, + createdAt: new Date(), + chat: null, + seen: false, + seenAt: null, + delivered: false, + deliveredAt: null, + }, + }, + }, + } satisfies chatComposerMutation["response"], variables: { - input: { to: chatId, content: textbox.current.innerText.trim() }, + input: { + toId: recipientId, + content, + localId, + }, + connections: [ + ConnectionHandler.getConnectionID( + "client:root", + "ChatMessagesFragment_messages", + { chatId: recipientId }, + ), + ], }, }); textbox.current.innerText = ""; + chatId && setNextLocalId(chatId, localId + 1); setText(""); } }; @@ -157,7 +206,7 @@ export function ChatComposer({ role="textbox" contentEditable data-placeholder="Enter a message" - className="w-full px-6 py-4 text-sm leading-normal outline-none dark:before:text-neutral-400 data-[placeholder]:empty:before:content-[attr(data-placeholder)]" + className="w-full px-6 py-4 text-sm leading-normal outline-none data-[placeholder]:empty:before:content-[attr(data-placeholder)] dark:before:text-neutral-400" /> diff --git a/apps/web/src/components/chat/chat-layout.tsx b/apps/web/src/components/chat/chat-layout.tsx index 1f0fb68..456924d 100644 --- a/apps/web/src/components/chat/chat-layout.tsx +++ b/apps/web/src/components/chat/chat-layout.tsx @@ -3,7 +3,11 @@ import { Dialog, Spinner } from "@ui/components"; import { useRouter } from "next/navigation"; import { Suspense, useEffect, useState } from "react"; -import { useLazyLoadQuery, useMutation } from "react-relay"; +import { + useLazyLoadQuery, + useMutation, + useSubscribeToInvalidationState, +} from "react-relay"; import { graphql } from "relay-runtime"; import type { chatLayoutDeleteMessagesMutation } from "../../../__generated__/chatLayoutDeleteMessagesMutation.graphql"; import type { chatLayoutQuery } from "../../../__generated__/chatLayoutQuery.graphql"; @@ -40,10 +44,17 @@ const ChatMessagesDeleteMutation = graphql` `; export function ChatLayout({ chatId }: ChatLayoutProps) { + const [fetchKey, setFetchKey] = useState(0); const router = useRouter(); - const data = useLazyLoadQuery(ChatLayoutQuery, { - chatId, - }); + const data = useLazyLoadQuery( + ChatLayoutQuery, + { + chatId, + }, + { fetchKey }, + ); + + useSubscribeToInvalidationState([], () => setFetchKey((prev) => prev + 1)); const [selectable, setSelectable] = useState(false); const [selectedMessages, setSelectedMessages] = useState([]); @@ -116,7 +127,8 @@ export function ChatLayout({ chatId }: ChatLayoutProps) { /> { setSelectedMessages([]); diff --git a/apps/web/src/components/chat/chat-message.tsx b/apps/web/src/components/chat/chat-message.tsx index af5ea18..e3a125a 100644 --- a/apps/web/src/components/chat/chat-message.tsx +++ b/apps/web/src/components/chat/chat-message.tsx @@ -17,7 +17,7 @@ import { DialogTrigger, } from "@ui/components"; import { cn } from "@ui/lib/utils"; -import { Check, CheckCircle2, Trash } from "lucide-react"; +import { Check, CheckCircle2, Clock, Trash } from "lucide-react"; import murmurhash from "murmurhash"; import { useFragment } from "react-relay"; import { graphql } from "relay-runtime"; @@ -37,6 +37,7 @@ const ChatMessageFragment = graphql` group } } + localId content seen createdAt @@ -74,13 +75,16 @@ export function ChatMessage({ message, ); + // When using optimistic response the fragment data is returned as undefined, even though the data exists + if (!data) return; + const sentByMe = user?.id === data.from.id; const createdAt = new Date(data.createdAt); - const userHash = murmurhash.v3(data.from.id); + const isPending = !data.chat; const userColor = data.chat?.node?.group - ? userHash % (CHAT_COLORS.length - 1) + ? murmurhash.v3(data.from.id) % (CHAT_COLORS.length - 1) : 0; const showUserAvatar = !!data.chat?.node?.group && !sentByMe && !sentByMe; @@ -175,11 +179,15 @@ export function ChatMessage({

{data.content} - + {new Intl.DateTimeFormat("default", { timeStyle: "short", }).format(createdAt)} - + {isPending ? ( + + ) : ( + + )}

diff --git a/apps/web/src/components/chat/chat-messages.tsx b/apps/web/src/components/chat/chat-messages.tsx index f00dd82..3d7bc92 100644 --- a/apps/web/src/components/chat/chat-messages.tsx +++ b/apps/web/src/components/chat/chat-messages.tsx @@ -1,4 +1,6 @@ import { useTypingStatusSubscription } from "@/hooks/useTypingStatusSubscription"; +import { useUser } from "@/hooks/useUser"; +import { setNextLocalId } from "@/utils/localId"; import { DotLottiePlayer } from "@dotlottie/react-player"; import { cn } from "@ui/lib/utils"; import { @@ -29,6 +31,7 @@ const ChatMessageFragment = graphql` edges { node { id + localId from { id } @@ -58,6 +61,7 @@ export function ChatMessages({ setSelectable, onDelete, }: ChatMessagesProps) { + const currentUser = useUser(); const { data, loadNext, hasNext } = usePaginationFragment< chatLayoutQuery, chatMessagesFragment$key @@ -65,6 +69,15 @@ export function ChatMessages({ const userTyping = useTypingStatusSubscription({ chatId }); + useEffect(() => { + const message = data.messages.edges.find( + (e) => e?.node?.from.id === currentUser?.id, + ); + if (message) { + setNextLocalId(chatId, (message?.node?.localId ?? 0) + 1); + } + }, [data, chatId, currentUser]); + useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape" && selectable) { @@ -118,7 +131,7 @@ export function ChatMessages({ return ( message?.node && ( { - cookies.set(null, "token", token); + cookies.set(null, "token", token); }; export const logout = () => { - cookies.destroy(null, "token"); + localStorage.clear(); + cookies.destroy(null, "token"); }; diff --git a/apps/web/src/utils/localId.ts b/apps/web/src/utils/localId.ts new file mode 100644 index 0000000..1321bed --- /dev/null +++ b/apps/web/src/utils/localId.ts @@ -0,0 +1,17 @@ +type IdStore = Record; + +const getIdStore = () => + JSON.parse(localStorage.getItem("idStore") ?? "{}") as IdStore; + +export const getNextLocalId = (chatId: string) => { + const idStore = getIdStore(); + + return idStore[chatId] ?? 0; +}; + +export const setNextLocalId = (chatId: string, value: number) => { + const idStore = getIdStore(); + idStore[chatId] = value; + + localStorage.setItem("idStore", JSON.stringify(idStore)); +}; diff --git a/fly.toml b/fly.toml index a5a4ae9..b41b8e3 100644 --- a/fly.toml +++ b/fly.toml @@ -15,7 +15,7 @@ PORT = '8080' [http_service] internal_port = 8080 force_https = true -auto_stop_machines = "stop" +auto_stop_machines = "suspend" auto_start_machines = true min_machines_running = 0 processes = ['app'] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7426500..2d81b55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@entria/graphql-mongoose-loader': specifier: ^4.4.0 version: 4.4.0(graphql-relay@0.10.1(graphql@16.8.2))(graphql@16.8.2)(mongoose@8.4.3(socks@2.8.3)) + '@faker-js/faker': + specifier: ^8.4.1 + version: 8.4.1 '@koa/bodyparser': specifier: ^5.1.1 version: 5.1.1(koa@2.15.3) @@ -98,12 +101,18 @@ importers: koa-mount: specifier: ^4.0.0 version: 4.0.0 + mongodb-memory-server: + specifier: ^10.0.0 + version: 10.0.0(socks@2.8.3) mongoose: specifier: ^8.4.3 version: 8.4.3(socks@2.8.3) regex-escape: specifier: ^3.4.10 version: 3.4.10 + supertest: + specifier: ^7.0.0 + version: 7.0.0 tsup: specifier: ^8.1.0 version: 8.1.0(postcss@8.4.41)(ts-node@10.9.2(@types/node@20.14.6)(typescript@5.4.5))(typescript@5.4.5) @@ -135,6 +144,9 @@ importers: '@types/node': specifier: ^20.14.6 version: 20.14.6 + '@types/supertest': + specifier: ^6.0.2 + version: 6.0.2 '@types/ws': specifier: ^8.5.11 version: 8.5.11 @@ -144,6 +156,12 @@ importers: typescript: specifier: ^5.4.5 version: 5.4.5 + vite-tsconfig-paths: + specifier: ^5.0.1 + version: 5.0.1(typescript@5.4.5)(vite@5.4.0(@types/node@20.14.6)) + vitest: + specifier: ^2.0.5 + version: 2.0.5(@types/node@20.14.6) apps/web: dependencies: @@ -356,6 +374,10 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + '@babel/runtime-corejs3@7.24.7': resolution: {integrity: sha512-eytSX6JLBY6PVAeQa2bFlDx/7Mmln/gaEpsit5a3WEvjGfiIytEsgAwuIXCPM0xvw0v0cJn3ilq0/TvXrW0kgA==} engines: {node: '>=6.9.0'} @@ -643,6 +665,10 @@ packages: cpu: [x64] os: [win32] + '@faker-js/faker@8.4.1': + resolution: {integrity: sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'} + '@floating-ui/core@1.6.4': resolution: {integrity: sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==} @@ -801,6 +827,9 @@ packages: '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} @@ -1394,6 +1423,9 @@ packages: '@types/content-disposition@0.5.8': resolution: {integrity: sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==} + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + '@types/cookies@0.9.0': resolution: {integrity: sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==} @@ -1442,6 +1474,9 @@ packages: '@types/koa__router@12.0.4': resolution: {integrity: sha512-Y7YBbSmfXZpa/m5UGGzb7XadJIRBRnwNY9cdAojZGp65Cpe5MAP3mOZE7e3bImt8dfKS4UFcR16SLH8L/z7PBw==} + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} @@ -1487,6 +1522,12 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/superagent@8.1.8': + resolution: {integrity: sha512-nTqHJ2OTa7PFEpLahzSEEeFeqbMpmcN7OeayiOc7v+xk+/vyTKljRe+o4MPqSnPeRCMvtxuLG+5QqluUVQJOnA==} + + '@types/supertest@6.0.2': + resolution: {integrity: sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==} + '@types/through@0.0.33': resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} @@ -1502,6 +1543,24 @@ packages: '@types/ws@8.5.11': resolution: {integrity: sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==} + '@vitest/expect@2.0.5': + resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} + + '@vitest/pretty-format@2.0.5': + resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} + + '@vitest/runner@2.0.5': + resolution: {integrity: sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==} + + '@vitest/snapshot@2.0.5': + resolution: {integrity: sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==} + + '@vitest/spy@2.0.5': + resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} + + '@vitest/utils@2.0.5': + resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} + abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -1589,10 +1648,20 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} + async-mutex@0.5.0: + resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + autoprefixer@10.4.19: resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} engines: {node: ^10 || ^12 || >=14} @@ -1600,9 +1669,15 @@ packages: peerDependencies: postcss: ^8.1.0 + b4a@1.6.6: + resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bare-events@2.4.2: + resolution: {integrity: sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1647,6 +1722,9 @@ packages: resolution: {integrity: sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ==} engines: {node: '>=16.20.1'} + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -1686,9 +1764,17 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + caniuse-lite@1.0.30001639: resolution: {integrity: sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==} + chai@5.1.1: + resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} + engines: {node: '>=12'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -1707,6 +1793,10 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -1785,6 +1875,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -1796,6 +1890,12 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1817,6 +1917,9 @@ packages: resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} engines: {node: '>= 0.6'} + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + cookies@0.9.1: resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} engines: {node: '>= 0.8'} @@ -1864,6 +1967,10 @@ packages: decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-equal@1.0.1: resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} @@ -1886,6 +1993,10 @@ packages: resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} engines: {node: '>=8'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} @@ -1912,6 +2023,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -1996,6 +2110,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2004,14 +2121,24 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} @@ -2032,10 +2159,34 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + foreground-child@3.2.1: resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} engines: {node: '>=14'} + form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + + formidable@3.5.1: + resolution: {integrity: sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -2085,6 +2236,9 @@ packages: engines: {node: '>=10'} deprecated: This package is no longer supported. + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} @@ -2097,6 +2251,10 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + get-tsconfig@4.7.5: resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==} @@ -2129,6 +2287,9 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} @@ -2231,6 +2392,10 @@ packages: header-case@1.0.1: resolution: {integrity: sha512-i0q9mkOeSuhXw6bGgiQCCBgY/jlZuV/7dZXyZ9c6LcBrqwvT8eT719E9uxE5LiZftdl+z81Ugbg/VvXV4OJOeQ==} + hexoid@1.0.0: + resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} + engines: {node: '>=8'} + howler@2.2.4: resolution: {integrity: sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==} @@ -2262,6 +2427,10 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + humanize-number@0.0.2: resolution: {integrity: sha512-un3ZAcNQGI7RzaWGZzQDH47HETM4Wrj6z6E4TId8Yeq9w5ZKUVB1nrT2jwFheTUjEmqcgTjXDc959jum+ai1kQ==} @@ -2363,6 +2532,10 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -2457,6 +2630,10 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} @@ -2511,6 +2688,9 @@ packages: lottie-web@5.12.2: resolution: {integrity: sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==} + loupe@3.1.1: + resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} + lower-case-first@1.0.2: resolution: {integrity: sha512-UuxaYakO7XeONbKrZf5FEgkantPf5DUqDayzP5VXZrtRPdH86s4kN47I8B3TW10S4QKiE3ziHNf3kRN//okHjA==} @@ -2530,6 +2710,9 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 + magic-string@0.30.11: + resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -2567,10 +2750,19 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2609,6 +2801,14 @@ packages: mongodb-connection-string-url@3.0.1: resolution: {integrity: sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==} + mongodb-memory-server-core@10.0.0: + resolution: {integrity: sha512-AdYi4nVqe3Pk95fRJ+DegbDdEfAG9wujNsVvJWbwh8+ZJd+d3JJK1PHxRyJ9rMvoczvlli5M30eMig7zBuF5pQ==} + engines: {node: '>=16.20.1'} + + mongodb-memory-server@10.0.0: + resolution: {integrity: sha512-7Geo/s4lst/QHw+N8/stdnyb08xn68O0zbSee62jgoPfWOXfSPhX9a8OvyOY/o23oYk9ra2EpA2Oejenb3JKfw==} + engines: {node: '>=16.20.1'} + mongodb@6.6.2: resolution: {integrity: sha512-ZF9Ugo2JCG/GfR7DEb4ypfyJJyiKbg5qBYKRintebj8+DNS33CyGMkWbrS9lara+u+h+yEOGSRiLhFO/g1s1aw==} engines: {node: '>=16.20.1'} @@ -2636,6 +2836,33 @@ packages: socks: optional: true + mongodb@6.8.0: + resolution: {integrity: sha512-HGQ9NWDle5WvwMnrvUxsFYPd3JEbqD3RgABHBQRuoCEND0qzhsd0iH5ypHsf1eJ+sXmvmyKpP+FLOKY8Il7jMw==} + engines: {node: '>=16.20.1'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.188.0 + '@mongodb-js/zstd': ^1.1.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' + snappy: ^7.2.2 + socks: ^2.7.1 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + mongoose@8.4.3: resolution: {integrity: sha512-GxPVLD+I/dxVkgcts2r2QmJJvS62/++btVj3RFt8YnHt+DSOp1Qjj62YEvgZaElwIOTcc4KGJM95X5LlrU1qQg==} engines: {node: '>=16.20.1'} @@ -2679,6 +2906,10 @@ packages: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} + new-find-package-json@2.0.0: + resolution: {integrity: sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==} + engines: {node: '>=12.22.0'} + next@14.2.5: resolution: {integrity: sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==} engines: {node: '>=18.17.0'} @@ -2739,6 +2970,10 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + npmlog@5.0.1: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} deprecated: This package is no longer supported. @@ -2768,6 +3003,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + only@0.0.2: resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==} @@ -2783,6 +3022,14 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-map@2.1.0: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} @@ -2791,6 +3038,10 @@ packages: resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} engines: {node: '>=8'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + pac-proxy-agent@7.0.2: resolution: {integrity: sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==} engines: {node: '>= 14'} @@ -2818,6 +3069,10 @@ packages: path-case@2.1.1: resolution: {integrity: sha512-Ou0N05MioItesaLr9q8TtHVWmJ6fxWdqKB2RohFmNWVyJ+2zeKIeDNWAN6B/Pe7wpzWChhZX6nONYmOnMeJQ/Q==} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -2826,6 +3081,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -2840,6 +3099,16 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} @@ -2855,6 +3124,10 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -2930,6 +3203,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + raw-body@2.5.2: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} @@ -3094,6 +3370,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + sentence-case@2.1.1: resolution: {integrity: sha512-ENl7cYHaK/Ktwk5OTD+aDbQ3uC8IByu/6Bkg+HDv8Mm+XnBnppVNalcfJTNsp1ibstKh030/JKQQWglDvtKwEQ==} @@ -3138,6 +3419,9 @@ packages: sift@17.1.3: resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3192,6 +3476,9 @@ packages: stack-generator@2.0.10: resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} @@ -3212,10 +3499,16 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + streamx@2.18.0: + resolution: {integrity: sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -3239,6 +3532,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -3261,6 +3558,14 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + superagent@9.0.2: + resolution: {integrity: sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==} + engines: {node: '>=14.18.0'} + + supertest@7.0.0: + resolution: {integrity: sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==} + engines: {node: '>=14.18.0'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -3294,10 +3599,16 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + text-decoder@1.1.1: + resolution: {integrity: sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -3308,12 +3619,27 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} tinygradient@1.1.5: resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} + tinypool@1.0.0: + resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.0: + resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==} + engines: {node: '>=14.0.0'} + title-case@2.1.1: resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==} @@ -3360,6 +3686,16 @@ packages: '@swc/wasm': optional: true + tsconfck@3.1.1: + resolution: {integrity: sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -3520,6 +3856,75 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-node@2.0.5: + resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite-tsconfig-paths@5.0.1: + resolution: {integrity: sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@5.4.0: + resolution: {integrity: sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.0.5: + resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.0.5 + '@vitest/ui': 2.0.5 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + wasm-imagemagick@1.2.8: resolution: {integrity: sha512-V7u80n7g+iAoV7sYgQKGSdG59J6/aSMGO0DDK0zxKnwOGjmVXyjP0yU4tX4cMrfC0t/Wk3I8TX7cmdbFQOYHpg==} @@ -3551,6 +3956,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} @@ -3600,6 +4010,10 @@ packages: engines: {node: '>= 14'} hasBin: true + yauzl@3.1.3: + resolution: {integrity: sha512-JCCdmlJJWv7L0q/KylOekyRaUrdEoUxWkWVcgorosTROCFWiS9p2NNPE9Yb91ak7b1N5SxAZEliWpspbZccivw==} + engines: {node: '>=12'} + ylru@1.4.0: resolution: {integrity: sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==} engines: {node: '>= 4.0.0'} @@ -3615,6 +4029,11 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + '@babel/runtime-corejs3@7.24.7': dependencies: core-js-pure: 3.37.1 @@ -3807,6 +4226,8 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true + '@faker-js/faker@8.4.1': {} + '@floating-ui/core@1.6.4': dependencies: '@floating-ui/utils': 0.2.4 @@ -3928,6 +4349,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.4.15': {} + '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -4493,6 +4916,8 @@ snapshots: '@types/content-disposition@0.5.8': {} + '@types/cookiejar@2.1.5': {} + '@types/cookies@0.9.0': dependencies: '@types/connect': 3.4.38 @@ -4567,6 +4992,8 @@ snapshots: dependencies: '@types/koa': 2.15.0 + '@types/methods@1.1.4': {} + '@types/mime@1.3.5': {} '@types/minimatch@5.1.2': {} @@ -4618,6 +5045,18 @@ snapshots: '@types/node': 20.14.11 '@types/send': 0.17.4 + '@types/superagent@8.1.8': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 20.14.11 + form-data: 4.0.0 + + '@types/supertest@6.0.2': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.8 + '@types/through@0.0.33': dependencies: '@types/node': 20.14.11 @@ -4634,6 +5073,39 @@ snapshots: dependencies: '@types/node': 20.14.11 + '@vitest/expect@2.0.5': + dependencies: + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.0.5': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.0.5': + dependencies: + '@vitest/utils': 2.0.5 + pathe: 1.1.2 + + '@vitest/snapshot@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + magic-string: 0.30.11 + pathe: 1.1.2 + + '@vitest/spy@2.0.5': + dependencies: + tinyspy: 3.0.0 + + '@vitest/utils@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + estree-walker: 3.0.3 + loupe: 3.1.1 + tinyrainbow: 1.2.0 + abbrev@1.1.1: {} accepts@1.3.8: @@ -4710,10 +5182,18 @@ snapshots: asap@2.0.6: {} + assertion-error@2.0.1: {} + ast-types@0.13.4: dependencies: tslib: 2.6.3 + async-mutex@0.5.0: + dependencies: + tslib: 2.6.3 + + asynckit@0.4.0: {} + autoprefixer@10.4.19(postcss@8.4.39): dependencies: browserslist: 4.23.1 @@ -4724,8 +5204,13 @@ snapshots: postcss: 8.4.39 postcss-value-parser: 4.2.0 + b4a@1.6.6: {} + balanced-match@1.0.2: {} + bare-events@2.4.2: + optional: true + base64-js@1.5.1: {} base64-url@2.3.3: {} @@ -4776,6 +5261,8 @@ snapshots: bson@6.7.0: {} + buffer-crc32@0.2.13: {} + buffer-equal-constant-time@1.0.1: {} buffer@5.7.1: @@ -4816,8 +5303,18 @@ snapshots: camelcase-css@2.0.1: {} + camelcase@6.3.0: {} + caniuse-lite@1.0.30001639: {} + chai@5.1.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.1 + pathval: 2.0.0 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -4857,6 +5354,8 @@ snapshots: chardet@0.7.0: {} + check-error@2.1.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -4929,12 +5428,20 @@ snapshots: color-convert: 2.0.1 color-string: 1.9.1 + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@10.0.1: {} commander@2.20.3: {} commander@4.1.1: {} + commondir@1.0.1: {} + + component-emitter@1.3.1: {} + concat-map@0.0.1: {} console-control-strings@1.1.0: {} @@ -4952,6 +5459,8 @@ snapshots: cookie@0.4.2: {} + cookiejar@2.1.4: {} + cookies@0.9.1: dependencies: depd: 2.0.0 @@ -4989,6 +5498,8 @@ snapshots: decimal.js@10.4.3: {} + deep-eql@5.0.2: {} + deep-equal@1.0.1: {} deep-extend@0.6.0: {} @@ -5020,6 +5531,8 @@ snapshots: rimraf: 3.0.2 slash: 3.0.0 + delayed-stream@1.0.0: {} + delegates@1.0.0: {} denque@2.1.0: {} @@ -5034,6 +5547,11 @@ snapshots: detect-node-es@1.1.0: {} + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + didyoumean@1.2.2: {} diff@4.0.2: {} @@ -5120,6 +5638,10 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.5 + esutils@2.0.3: {} execa@5.1.1: @@ -5134,12 +5656,26 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 + execa@8.0.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + external-editor@3.1.0: dependencies: chardet: 0.7.0 iconv-lite: 0.4.24 tmp: 0.0.33 + fast-fifo@1.3.2: {} + fast-glob@3.3.2: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5148,6 +5684,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.7 + fast-safe-stringify@2.1.1: {} + fastq@1.15.0: dependencies: reusify: 1.0.4 @@ -5176,11 +5714,38 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + follow-redirects@1.15.6(debug@4.3.5): + optionalDependencies: + debug: 4.3.5 + foreground-child@3.2.1: dependencies: cross-spawn: 7.0.3 signal-exit: 4.1.0 + form-data@4.0.0: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + formidable@3.5.1: + dependencies: + dezalgo: 1.0.4 + hexoid: 1.0.0 + once: 1.4.0 + fraction.js@4.3.7: {} framer-motion@11.3.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -5227,6 +5792,8 @@ snapshots: strip-ansi: 6.0.1 wide-align: 1.1.5 + get-func-name@2.0.2: {} + get-intrinsic@1.2.4: dependencies: es-errors: 1.3.0 @@ -5239,6 +5806,8 @@ snapshots: get-stream@6.0.1: {} + get-stream@8.0.1: {} + get-tsconfig@4.7.5: dependencies: resolve-pkg-maps: 1.0.0 @@ -5298,6 +5867,8 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + globrex@0.1.2: {} + gopd@1.0.1: dependencies: get-intrinsic: 1.2.4 @@ -5396,6 +5967,8 @@ snapshots: no-case: 2.3.2 upper-case: 1.1.3 + hexoid@1.0.0: {} + howler@2.2.4: {} http-assert@1.5.0: @@ -5442,6 +6015,8 @@ snapshots: human-signals@2.1.0: {} + human-signals@5.0.0: {} + humanize-number@0.0.2: {} iconv-lite@0.4.24: @@ -5558,6 +6133,8 @@ snapshots: is-stream@2.0.1: {} + is-stream@3.0.0: {} + is-unicode-supported@0.1.0: {} is-upper-case@1.1.2: @@ -5681,6 +6258,10 @@ snapshots: load-tsconfig@0.2.5: {} + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + lodash.defaults@4.2.0: {} lodash.get@4.4.2: {} @@ -5722,6 +6303,10 @@ snapshots: lottie-web@5.12.2: {} + loupe@3.1.1: + dependencies: + get-func-name: 2.0.2 + lower-case-first@1.0.2: dependencies: lower-case: 1.1.4 @@ -5736,6 +6321,10 @@ snapshots: dependencies: react: 18.3.1 + magic-string@0.30.11: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + make-dir@3.1.0: dependencies: semver: 6.3.1 @@ -5763,8 +6352,12 @@ snapshots: dependencies: mime-db: 1.52.0 + mime@2.6.0: {} + mimic-fn@2.1.0: {} + mimic-fn@4.0.0: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -5799,6 +6392,44 @@ snapshots: '@types/whatwg-url': 11.0.5 whatwg-url: 13.0.0 + mongodb-memory-server-core@10.0.0(socks@2.8.3): + dependencies: + async-mutex: 0.5.0 + camelcase: 6.3.0 + debug: 4.3.5 + find-cache-dir: 3.3.2 + follow-redirects: 1.15.6(debug@4.3.5) + https-proxy-agent: 7.0.5 + mongodb: 6.8.0(socks@2.8.3) + new-find-package-json: 2.0.0 + semver: 7.6.3 + tar-stream: 3.1.7 + tslib: 2.6.3 + yauzl: 3.1.3 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - gcp-metadata + - kerberos + - mongodb-client-encryption + - snappy + - socks + - supports-color + + mongodb-memory-server@10.0.0(socks@2.8.3): + dependencies: + mongodb-memory-server-core: 10.0.0(socks@2.8.3) + tslib: 2.6.3 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - gcp-metadata + - kerberos + - mongodb-client-encryption + - snappy + - socks + - supports-color + mongodb@6.6.2(socks@2.8.3): dependencies: '@mongodb-js/saslprep': 1.1.7 @@ -5807,6 +6438,14 @@ snapshots: optionalDependencies: socks: 2.8.3 + mongodb@6.8.0(socks@2.8.3): + dependencies: + '@mongodb-js/saslprep': 1.1.7 + bson: 6.7.0 + mongodb-connection-string-url: 3.0.1 + optionalDependencies: + socks: 2.8.3 + mongoose@8.4.3(socks@2.8.3): dependencies: bson: 6.7.0 @@ -5856,6 +6495,12 @@ snapshots: netmask@2.0.2: {} + new-find-package-json@2.0.0: + dependencies: + debug: 4.3.5 + transitivePeerDependencies: + - supports-color + next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.5 @@ -5924,6 +6569,10 @@ snapshots: dependencies: path-key: 3.1.1 + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + npmlog@5.0.1: dependencies: are-we-there-yet: 2.0.0 @@ -5951,6 +6600,10 @@ snapshots: dependencies: mimic-fn: 2.1.0 + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + only@0.0.2: {} ora@4.1.1: @@ -5978,12 +6631,22 @@ snapshots: os-tmpdir@1.0.2: {} + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-map@2.1.0: {} p-map@3.0.0: dependencies: aggregate-error: 3.1.0 + p-try@2.2.0: {} + pac-proxy-agent@7.0.2: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 @@ -6021,10 +6684,14 @@ snapshots: dependencies: no-case: 2.3.2 + path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-scurry@1.11.1: @@ -6036,6 +6703,12 @@ snapshots: path-type@4.0.0: {} + pathe@1.1.2: {} + + pathval@2.0.0: {} + + pend@1.2.0: {} + picocolors@1.0.1: {} picomatch@2.3.1: {} @@ -6044,6 +6717,10 @@ snapshots: pirates@4.0.6: {} + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + postcss-import@15.1.0(postcss@8.4.39): dependencies: postcss: 8.4.39 @@ -6109,7 +6786,6 @@ snapshots: nanoid: 3.3.7 picocolors: 1.0.1 source-map-js: 1.2.0 - optional: true prettier@3.3.3: {} @@ -6140,6 +6816,8 @@ snapshots: queue-microtask@1.2.3: {} + queue-tick@1.0.1: {} + raw-body@2.5.2: dependencies: bytes: 3.1.2 @@ -6324,6 +7002,8 @@ snapshots: semver@7.6.2: {} + semver@7.6.3: {} + sentence-case@2.1.1: dependencies: no-case: 2.3.2 @@ -6391,6 +7071,8 @@ snapshots: sift@17.1.3: {} + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -6440,6 +7122,8 @@ snapshots: dependencies: stackframe: 1.3.4 + stackback@0.0.2: {} + stackframe@1.3.4: {} stacktrace-gps@3.1.2: @@ -6459,8 +7143,18 @@ snapshots: statuses@2.0.1: {} + std-env@3.7.0: {} + streamsearch@1.1.0: {} + streamx@2.18.0: + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + text-decoder: 1.1.1 + optionalDependencies: + bare-events: 2.4.2 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -6487,6 +7181,8 @@ snapshots: strip-final-newline@2.0.0: {} + strip-final-newline@3.0.0: {} + strip-json-comments@2.0.1: {} styled-jsx@5.1.1(react@18.3.1): @@ -6504,6 +7200,27 @@ snapshots: pirates: 4.0.6 ts-interface-checker: 0.1.13 + superagent@9.0.2: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.3.5 + fast-safe-stringify: 2.1.1 + form-data: 4.0.0 + formidable: 3.5.1 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.12.1 + transitivePeerDependencies: + - supports-color + + supertest@7.0.0: + dependencies: + methods: 1.1.2 + superagent: 9.0.2 + transitivePeerDependencies: + - supports-color + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -6583,6 +7300,12 @@ snapshots: transitivePeerDependencies: - ts-node + tar-stream@3.1.7: + dependencies: + b4a: 1.6.6 + fast-fifo: 1.3.2 + streamx: 2.18.0 + tar@6.2.1: dependencies: chownr: 2.0.0 @@ -6592,6 +7315,10 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 + text-decoder@1.1.1: + dependencies: + b4a: 1.6.6 + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -6602,6 +7329,8 @@ snapshots: through@2.3.8: {} + tinybench@2.9.0: {} + tinycolor2@1.6.0: {} tinygradient@1.1.5: @@ -6609,6 +7338,12 @@ snapshots: '@types/tinycolor2': 1.4.6 tinycolor2: 1.6.0 + tinypool@1.0.0: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.0: {} + title-case@2.1.1: dependencies: no-case: 2.3.2 @@ -6694,6 +7429,10 @@ snapshots: yn: 3.1.1 optional: true + tsconfck@3.1.1(typescript@5.4.5): + optionalDependencies: + typescript: 5.4.5 + tslib@1.14.1: {} tslib@2.6.3: {} @@ -6823,6 +7562,77 @@ snapshots: vary@1.1.2: {} + vite-node@2.0.5(@types/node@20.14.6): + dependencies: + cac: 6.7.14 + debug: 4.3.5 + pathe: 1.1.2 + tinyrainbow: 1.2.0 + vite: 5.4.0(@types/node@20.14.6) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-tsconfig-paths@5.0.1(typescript@5.4.5)(vite@5.4.0(@types/node@20.14.6)): + dependencies: + debug: 4.3.5 + globrex: 0.1.2 + tsconfck: 3.1.1(typescript@5.4.5) + optionalDependencies: + vite: 5.4.0(@types/node@20.14.6) + transitivePeerDependencies: + - supports-color + - typescript + + vite@5.4.0(@types/node@20.14.6): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.41 + rollup: 4.18.1 + optionalDependencies: + '@types/node': 20.14.6 + fsevents: 2.3.3 + + vitest@2.0.5(@types/node@20.14.6): + dependencies: + '@ampproject/remapping': 2.3.0 + '@vitest/expect': 2.0.5 + '@vitest/pretty-format': 2.0.5 + '@vitest/runner': 2.0.5 + '@vitest/snapshot': 2.0.5 + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 + debug: 4.3.5 + execa: 8.0.1 + magic-string: 0.30.11 + pathe: 1.1.2 + std-env: 3.7.0 + tinybench: 2.9.0 + tinypool: 1.0.0 + tinyrainbow: 1.2.0 + vite: 5.4.0(@types/node@20.14.6) + vite-node: 2.0.5(@types/node@20.14.6) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.14.6 + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + wasm-imagemagick@1.2.8: dependencies: p-map: 2.1.0 @@ -6858,6 +7668,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wide-align@1.1.5: dependencies: string-width: 4.2.3 @@ -6897,6 +7712,11 @@ snapshots: yaml@2.4.5: {} + yauzl@3.1.3: + dependencies: + buffer-crc32: 0.2.13 + pend: 1.2.0 + ylru@1.4.0: {} yn@3.1.1: {} diff --git a/turbo.json b/turbo.json index d333050..f3d0fd4 100644 --- a/turbo.json +++ b/turbo.json @@ -13,6 +13,7 @@ "lint": { "dependsOn": ["^lint"] }, + "test": {}, "dev": { "cache": false, "persistent": true