From 9f3b05f0b74a6412092e61b433c9ddc299d43479 Mon Sep 17 00:00:00 2001 From: TASNEEM KOUSHAR Date: Fri, 1 Nov 2024 21:43:19 +0530 Subject: [PATCH 1/6] Onboard renovate (#2634) * fix: adding renovate bot * fix: liniting error * fix: formatting json * fix: formatting json * fix: formatting json --- renovate.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000000..ccbd3b8344 --- /dev/null +++ b/renovate.json @@ -0,0 +1,17 @@ +{ + "extends": ["config:base"], + "baseBranches": ["develop"], + "schedule": ["every weekend"], + "packageRules": [ + { + "packagePatterns": ["*"], + "groupName": "All Minor Updates", + "group": true, + "commitMessageAction": "Update dependencies (renov): " + } + ], + "labels": ["dependencies"], + "branchPrefix": "renovate/", + "automergeType": "pr", + "automerge": true +} From f1f5f3be381716463125a5690ec965f6829fa6c0 Mon Sep 17 00:00:00 2001 From: Disha Talreja Date: Fri, 1 Nov 2024 22:22:50 +0530 Subject: [PATCH 2/6] fix: removed all references to direct chat and group chat (#2629) * fix: removed all references to direct chat and group chat * fix: documentation and test description --- codegen.ts | 2 - schema.graphql | 13 ---- src/models/Chat.ts | 4 +- src/models/ChatMessage.ts | 4 +- src/models/MessageChat.ts | 71 ------------------- src/models/index.ts | 1 - src/resolvers/Mutation/sendMessageToChat.ts | 2 +- src/resolvers/Query/chatsByUserId.ts | 8 +-- src/resolvers/index.ts | 2 - src/typeDefs/errors/createDirectChatError.ts | 16 ----- src/typeDefs/errors/index.ts | 2 - src/typeDefs/inputs.ts | 11 --- src/types/generatedGraphQLTypes.ts | 26 ------- tests/resolvers/Chat/creator.spec.ts | 2 +- .../ChatMessage/chatMessageBelongsTo.spec.ts | 6 +- tests/resolvers/ChatMessage/replyTo.spec.ts | 6 +- tests/resolvers/ChatMessage/sender.spec.ts | 2 +- tests/resolvers/Mutation/createChat.spec.ts | 4 +- .../Mutation/sendMessageToChat.spec.ts | 6 +- .../Subscription/messageSentToChat.spec.ts | 2 +- 20 files changed, 22 insertions(+), 168 deletions(-) delete mode 100644 src/models/MessageChat.ts delete mode 100644 src/typeDefs/errors/createDirectChatError.ts diff --git a/codegen.ts b/codegen.ts index 1a2ab9b479..bc996c81da 100644 --- a/codegen.ts +++ b/codegen.ts @@ -40,8 +40,6 @@ const config: CodegenConfig = { CheckIn: "../models/CheckIn#InterfaceCheckIn", - MessageChat: "../models/MessageChat#InterfaceMessageChat", - Comment: "../models/Comment#InterfaceComment", Community: "../models/Community#InterfaceCommunity", diff --git a/schema.graphql b/schema.graphql index 183b9efacd..7acb77d8a4 100644 --- a/schema.graphql +++ b/schema.graphql @@ -370,8 +370,6 @@ type CreateCommentPayload { userErrors: [CreateCommentError!]! } -union CreateDirectChatError = OrganizationNotFoundError | UserNotFoundError - union CreateMemberError = MemberNotFoundError | OrganizationNotFoundError | UserNotAuthorizedAdminError | UserNotAuthorizedError | UserNotFoundError type CreateMemberPayload { @@ -1040,11 +1038,6 @@ type Message { videoUrl: URL } -input MessageChatInput { - message: String! - receiver: ID! -} - type MinimumLengthError implements FieldError { limit: Int! message: String! @@ -2043,12 +2036,6 @@ input chatInput { userIds: [ID!]! } -input createGroupChatInput { - organizationId: ID! - title: String! - userIds: [ID!]! -} - input createUserFamilyInput { title: String! userIds: [ID!]! diff --git a/src/models/Chat.ts b/src/models/Chat.ts index 813cd539d4..563550d7bb 100644 --- a/src/models/Chat.ts +++ b/src/models/Chat.ts @@ -104,11 +104,11 @@ const chatSchema = new Schema( }, ); -// Add logging middleware for directChatSchema +// Add logging middleware for Chat createLoggingMiddleware(chatSchema, "Chat"); /** - * Retrieves or creates the Mongoose model for DirectChat. + * Retrieves or creates the Mongoose model for Chat. * Prevents Mongoose OverwriteModelError during testing. */ const chatModel = (): Model => diff --git a/src/models/ChatMessage.ts b/src/models/ChatMessage.ts index 1229935834..1b2d9529a3 100644 --- a/src/models/ChatMessage.ts +++ b/src/models/ChatMessage.ts @@ -96,10 +96,10 @@ const chatMessageSchema = new Schema( ); // Apply logging middleware to the schema -createLoggingMiddleware(chatMessageSchema, "DirectChatMessage"); +createLoggingMiddleware(chatMessageSchema, "ChatMessage"); /** - * Returns the Mongoose Model for DirectChatMessage to prevent OverwriteModelError. + * Returns the Mongoose Model for ChatMessage to prevent OverwriteModelError. */ const chatMessageModel = (): Model => model("ChatMessage", chatMessageSchema); diff --git a/src/models/MessageChat.ts b/src/models/MessageChat.ts deleted file mode 100644 index 7510ea8d1a..0000000000 --- a/src/models/MessageChat.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { PopulatedDoc, Types, Document, Model } from "mongoose"; -import { Schema, model, models } from "mongoose"; -import type { InterfaceUser } from "./User"; -import { createLoggingMiddleware } from "../libraries/dbLogger"; -/** - * Interface representing a document for a chat in the database (MongoDB). - */ -export interface InterfaceMessageChat { - _id: Types.ObjectId; - message: string; - languageBarrier: boolean; - sender: PopulatedDoc; - receiver: PopulatedDoc; - createdAt: Date; - updatedAt: Date; -} -/** - * Mongoose schema for a Message Chat. - * Defines the structure of the Message Chat document stored in MongoDB. - * @param message - The content of the chat message. - * @param languageBarrier - Indicates if there's a language barrier in the chat. - * @param sender - Reference to the User who sent the chat message. - * @param receiver - Reference to the User who received the chat message. - * @param createdAt - The date and time when the chat was created. - * @param updatedAt - The date and time when the chat was last updated. - */ -const messageChatSchema = new Schema( - { - message: { - type: String, - required: true, - }, - languageBarrier: { - type: Boolean, - required: false, - default: false, - }, - sender: { - type: Schema.Types.ObjectId, - ref: "User", - required: true, - }, - receiver: { - type: Schema.Types.ObjectId, - ref: "User", - required: true, - }, - }, - { - timestamps: true, // Automatically adds `createdAt` and `updatedAt` fields - }, -); - -// Add logging middleware for messageChatSchema -createLoggingMiddleware(messageChatSchema, "MessageChat"); - -/** - * Function to retrieve or create the Mongoose model for the MessageChat. - * This is necessary to avoid the OverwriteModelError during testing. - * @returns The Mongoose model for the MessageChat. - */ -const messageChatModel = (): Model => - model("MessageChat", messageChatSchema); - -/** - * The Mongoose model for the MessageChat. - * If the model already exists (e.g., during testing), it uses the existing model. - * Otherwise, it creates a new model. - */ -export const MessageChat = (models.MessageChat || - messageChatModel()) as ReturnType; diff --git a/src/models/index.ts b/src/models/index.ts index 6c88b18db6..1292a2ac41 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -23,7 +23,6 @@ export * from "./ImageHash"; export * from "./Language"; export * from "./MembershipRequest"; export * from "./Message"; -export * from "./MessageChat"; export * from "./Organization"; export * from "./OrganizationCustomField"; export * from "./OrganizationTagUser"; diff --git a/src/resolvers/Mutation/sendMessageToChat.ts b/src/resolvers/Mutation/sendMessageToChat.ts index a5c7e83558..24145efbd0 100644 --- a/src/resolvers/Mutation/sendMessageToChat.ts +++ b/src/resolvers/Mutation/sendMessageToChat.ts @@ -52,7 +52,7 @@ export const sendMessageToChat: MutationResolvers["sendMessageToChat"] = async ( updatedAt: now, }); - // add createdDirectChatMessage to directChat + // add createdChatMessage to Chat await Chat.updateOne( { _id: chat._id, diff --git a/src/resolvers/Query/chatsByUserId.ts b/src/resolvers/Query/chatsByUserId.ts index 57945ad2f6..c99904ab9a 100644 --- a/src/resolvers/Query/chatsByUserId.ts +++ b/src/resolvers/Query/chatsByUserId.ts @@ -4,8 +4,8 @@ import { Chat } from "../../models"; * This query will fetch all the Chats for the current user from the database. * @param _parent- * @param args - An object that contains `id` of the user. - * @returns An object `directChats` that contains all direct chats of the current user. - * If the `directChats` object is null then it throws `NotFoundError` error. + * @returns An object `chats` that contains all chats of the current user. + * If the `Chats` object is null then returns an empty array. * @remarks You can learn about GraphQL `Resolvers` * {@link https://www.apollographql.com/docs/apollo-server/data/resolvers/ | here}. */ @@ -17,7 +17,5 @@ export const chatsByUserId: QueryResolvers["chatsByUserId"] = async ( users: args.id, }).lean(); - console.log(chats); - - return chats; + return chats || []; }; diff --git a/src/resolvers/index.ts b/src/resolvers/index.ts index d3dd903ae6..5319a9fc70 100644 --- a/src/resolvers/index.ts +++ b/src/resolvers/index.ts @@ -86,8 +86,6 @@ const resolversComposition = { "Mutation.blockPluginCreationBySuperadmin": [currentUserExists()], "Mutation.createComment": [currentUserExists()], "Mutation.createChat": [currentUserExists()], - "Mutation.createDirectChat": [currentUserExists()], - "Mutation.createGroupChat": [currentUserExists()], "Mutation.createOrganization": [currentUserExists()], "Mutation.createVenue": [currentUserExists()], "Mutation.deleteVenue": [currentUserExists()], diff --git a/src/typeDefs/errors/createDirectChatError.ts b/src/typeDefs/errors/createDirectChatError.ts deleted file mode 100644 index f2a9b9eb43..0000000000 --- a/src/typeDefs/errors/createDirectChatError.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { gql } from "graphql-tag"; - -/** - * GraphQL schema definition for errors related to creating a direct chat. - */ -export const createDirectChatErrors = gql` - type OrganizationNotFoundError implements Error { - message: String! - } - - type UserNotFoundError implements Error { - message: String! - } - - union CreateDirectChatError = OrganizationNotFoundError | UserNotFoundError -`; diff --git a/src/typeDefs/errors/index.ts b/src/typeDefs/errors/index.ts index a71db2d6e3..7306e51027 100644 --- a/src/typeDefs/errors/index.ts +++ b/src/typeDefs/errors/index.ts @@ -3,7 +3,6 @@ import { connectionError } from "./connectionError"; import { createMemberErrors } from "./createMemberErrors"; import { createAdminErrors } from "./createAdminErrors"; import { createCommentErrors } from "./createCommentErrors"; -import { createDirectChatErrors } from "./createDirectChatError"; /** * Array of all error definitions. @@ -14,5 +13,4 @@ export const errors = [ createMemberErrors, createAdminErrors, createCommentErrors, - createDirectChatErrors, ]; diff --git a/src/typeDefs/inputs.ts b/src/typeDefs/inputs.ts index 06888ea4ee..3cd3c33450 100644 --- a/src/typeDefs/inputs.ts +++ b/src/typeDefs/inputs.ts @@ -24,12 +24,6 @@ export const inputs = gql` image: String } - input createGroupChatInput { - userIds: [ID!]! - organizationId: ID! - title: String! - } - input createUserFamilyInput { title: String! userIds: [ID!]! @@ -294,11 +288,6 @@ export const inputs = gql` creatorId_not_in: [ID!] } - input MessageChatInput { - message: String! - receiver: ID! - } - input NoteInput { content: String! agendaItemId: ID! diff --git a/src/types/generatedGraphQLTypes.ts b/src/types/generatedGraphQLTypes.ts index 13e9ba20ba..fe1f9b0567 100644 --- a/src/types/generatedGraphQLTypes.ts +++ b/src/types/generatedGraphQLTypes.ts @@ -7,7 +7,6 @@ import type { InterfaceAdvertisement as InterfaceAdvertisementModel } from '../m import type { InterfaceAgendaItem as InterfaceAgendaItemModel } from '../models/AgendaItem'; import type { InterfaceAgendaSection as InterfaceAgendaSectionModel } from '../models/AgendaSection'; import type { InterfaceCheckIn as InterfaceCheckInModel } from '../models/CheckIn'; -import type { InterfaceMessageChat as InterfaceMessageChatModel } from '../models/MessageChat'; import type { InterfaceComment as InterfaceCommentModel } from '../models/Comment'; import type { InterfaceCommunity as InterfaceCommunityModel } from '../models/Community'; import type { InterfaceChat as InterfaceChatModel } from '../models/Chat'; @@ -447,8 +446,6 @@ export type CreateCommentPayload = { userErrors: Array; }; -export type CreateDirectChatError = OrganizationNotFoundError | UserNotFoundError; - export type CreateMemberError = MemberNotFoundError | OrganizationNotFoundError | UserNotAuthorizedAdminError | UserNotAuthorizedError | UserNotFoundError; export type CreateMemberPayload = { @@ -1123,11 +1120,6 @@ export type Message = { videoUrl?: Maybe; }; -export type MessageChatInput = { - message: Scalars['String']['input']; - receiver: Scalars['ID']['input']; -}; - export type MinimumLengthError = FieldError & { __typename?: 'MinimumLengthError'; limit: Scalars['Int']['output']; @@ -3152,12 +3144,6 @@ export type ChatInput = { userIds: Array; }; -export type CreateGroupChatInput = { - organizationId: Scalars['ID']['input']; - title: Scalars['String']['input']; - userIds: Array; -}; - export type CreateUserFamilyInput = { title: Scalars['String']['input']; userIds: Array; @@ -3231,7 +3217,6 @@ export type ResolversUnionTypes<_RefType extends Record> = { ConnectionError: ( InvalidCursor ) | ( MaximumValueError ); CreateAdminError: ( OrganizationMemberNotFoundError ) | ( OrganizationNotFoundError ) | ( UserNotAuthorizedError ) | ( UserNotFoundError ); CreateCommentError: ( PostNotFoundError ); - CreateDirectChatError: ( OrganizationNotFoundError ) | ( UserNotFoundError ); CreateMemberError: ( MemberNotFoundError ) | ( OrganizationNotFoundError ) | ( UserNotAuthorizedAdminError ) | ( UserNotAuthorizedError ) | ( UserNotFoundError ); }; @@ -3289,7 +3274,6 @@ export type ResolversTypes = { CreateAgendaSectionInput: CreateAgendaSectionInput; CreateCommentError: ResolverTypeWrapper['CreateCommentError']>; CreateCommentPayload: ResolverTypeWrapper & { comment?: Maybe, userErrors: Array }>; - CreateDirectChatError: ResolverTypeWrapper['CreateDirectChatError']>; CreateMemberError: ResolverTypeWrapper['CreateMemberError']>; CreateMemberPayload: ResolverTypeWrapper & { organization?: Maybe, userErrors: Array }>; CreateUserTagInput: CreateUserTagInput; @@ -3354,7 +3338,6 @@ export type ResolversTypes = { MembershipRequest: ResolverTypeWrapper; MembershipRequestsWhereInput: MembershipRequestsWhereInput; Message: ResolverTypeWrapper; - MessageChatInput: MessageChatInput; MinimumLengthError: ResolverTypeWrapper; MinimumValueError: ResolverTypeWrapper; Mutation: ResolverTypeWrapper<{}>; @@ -3453,7 +3436,6 @@ export type ResolversTypes = { VenueWhereInput: VenueWhereInput; WeekDays: WeekDays; chatInput: ChatInput; - createGroupChatInput: CreateGroupChatInput; createUserFamilyInput: CreateUserFamilyInput; }; @@ -3501,7 +3483,6 @@ export type ResolversParentTypes = { CreateAgendaSectionInput: CreateAgendaSectionInput; CreateCommentError: ResolversUnionTypes['CreateCommentError']; CreateCommentPayload: Omit & { comment?: Maybe, userErrors: Array }; - CreateDirectChatError: ResolversUnionTypes['CreateDirectChatError']; CreateMemberError: ResolversUnionTypes['CreateMemberError']; CreateMemberPayload: Omit & { organization?: Maybe, userErrors: Array }; CreateUserTagInput: CreateUserTagInput; @@ -3556,7 +3537,6 @@ export type ResolversParentTypes = { MembershipRequest: InterfaceMembershipRequestModel; MembershipRequestsWhereInput: MembershipRequestsWhereInput; Message: InterfaceMessageModel; - MessageChatInput: MessageChatInput; MinimumLengthError: MinimumLengthError; MinimumValueError: MinimumValueError; Mutation: {}; @@ -3643,7 +3623,6 @@ export type ResolversParentTypes = { VenueInput: VenueInput; VenueWhereInput: VenueWhereInput; chatInput: ChatInput; - createGroupChatInput: CreateGroupChatInput; createUserFamilyInput: CreateUserFamilyInput; }; @@ -3925,10 +3904,6 @@ export type CreateCommentPayloadResolvers; }; -export type CreateDirectChatErrorResolvers = { - __resolveType: TypeResolveFn<'OrganizationNotFoundError' | 'UserNotFoundError', ParentType, ContextType>; -}; - export type CreateMemberErrorResolvers = { __resolveType: TypeResolveFn<'MemberNotFoundError' | 'OrganizationNotFoundError' | 'UserNotAuthorizedAdminError' | 'UserNotAuthorizedError' | 'UserNotFoundError', ParentType, ContextType>; }; @@ -4774,7 +4749,6 @@ export type Resolvers = { CreateAdvertisementPayload?: CreateAdvertisementPayloadResolvers; CreateCommentError?: CreateCommentErrorResolvers; CreateCommentPayload?: CreateCommentPayloadResolvers; - CreateDirectChatError?: CreateDirectChatErrorResolvers; CreateMemberError?: CreateMemberErrorResolvers; CreateMemberPayload?: CreateMemberPayloadResolvers; Date?: GraphQLScalarType; diff --git a/tests/resolvers/Chat/creator.spec.ts b/tests/resolvers/Chat/creator.spec.ts index 52ecda19d7..4a1554d00d 100644 --- a/tests/resolvers/Chat/creator.spec.ts +++ b/tests/resolvers/Chat/creator.spec.ts @@ -20,7 +20,7 @@ afterAll(async () => { await disconnect(MONGOOSE_INSTANCE); }); -describe("resolvers -> DirectChat -> creator", () => { +describe("resolvers -> Chat -> creator", () => { it(`returns user object for parent.creator`, async () => { const parent = testChat?.toObject(); if (!parent) { diff --git a/tests/resolvers/ChatMessage/chatMessageBelongsTo.spec.ts b/tests/resolvers/ChatMessage/chatMessageBelongsTo.spec.ts index b3146fb739..bba2b832f5 100644 --- a/tests/resolvers/ChatMessage/chatMessageBelongsTo.spec.ts +++ b/tests/resolvers/ChatMessage/chatMessageBelongsTo.spec.ts @@ -22,8 +22,8 @@ afterAll(async () => { await disconnect(MONGOOSE_INSTANCE); }); -describe("resolvers -> DirectChatMessage -> directChatMessageBelongsTo", () => { - it(`returns directChat object for parent.directChatMessageBelongsTo`, async () => { +describe("resolvers -> ChatMessage -> chatMessageBelongsTo", () => { + it(`returns chat object for parent.chatMessageBelongsTo`, async () => { const parent = testChatMessage?.toObject(); if (!parent) { @@ -46,7 +46,7 @@ describe("resolvers -> DirectChatMessage -> directChatMessageBelongsTo", () => { expect(chatMessageBelongsToPayload).toEqual(chatMessageBelongsTo); }); - it(`throws NotFoundError if no directChat exists`, async () => { + it(`throws NotFoundError if no chat exists`, async () => { const { requestContext } = await import("../../../src/libraries"); const spy = vi .spyOn(requestContext, "translate") diff --git a/tests/resolvers/ChatMessage/replyTo.spec.ts b/tests/resolvers/ChatMessage/replyTo.spec.ts index 162c2d2ba8..585fd09b7e 100644 --- a/tests/resolvers/ChatMessage/replyTo.spec.ts +++ b/tests/resolvers/ChatMessage/replyTo.spec.ts @@ -47,8 +47,8 @@ afterAll(async () => { } }); -describe("resolvers -> DirectChatMessage -> replyTo", () => { - it(`returns directChat object for parent.replyTo`, async () => { +describe("resolvers -> ChatMessage -> replyTo", () => { + it(`returns chat object for parent.replyTo`, async () => { const parent = testChatMessage ? testChatMessage.toObject() : null; if (!parent) { @@ -70,7 +70,7 @@ describe("resolvers -> DirectChatMessage -> replyTo", () => { throw error; } }); - it(`throws NotFoundError if no directChat exists`, async () => { + it(`throws NotFoundError if no chat exists`, async () => { const { requestContext } = await import("../../../src/libraries"); const spy = vi .spyOn(requestContext, "translate") diff --git a/tests/resolvers/ChatMessage/sender.spec.ts b/tests/resolvers/ChatMessage/sender.spec.ts index c5bbdd572d..8c64c91c08 100644 --- a/tests/resolvers/ChatMessage/sender.spec.ts +++ b/tests/resolvers/ChatMessage/sender.spec.ts @@ -22,7 +22,7 @@ afterAll(async () => { await disconnect(MONGOOSE_INSTANCE); }); -describe("resolvers -> DirectChatMessage -> sender", () => { +describe("resolvers -> ChatMessage -> sender", () => { it(`returns user object for parent.sender`, async () => { const parent = testChatMessage?.toObject(); if (!parent) { diff --git a/tests/resolvers/Mutation/createChat.spec.ts b/tests/resolvers/Mutation/createChat.spec.ts index 9013afca46..560c9b6207 100644 --- a/tests/resolvers/Mutation/createChat.spec.ts +++ b/tests/resolvers/Mutation/createChat.spec.ts @@ -99,7 +99,7 @@ describe("resolvers -> Mutation -> createChat", () => { expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); } }); - it(`creates the directChat and returns it`, async () => { + it(`creates the chat and returns it`, async () => { const args: MutationCreateChatArgs = { data: { organizationId: testOrganization?.id, @@ -125,7 +125,7 @@ describe("resolvers -> Mutation -> createChat", () => { ); }); - it(`creates the groupChat and returns it`, async () => { + it(`creates the chat and returns it`, async () => { const args: MutationCreateChatArgs = { data: { organizationId: testOrganization?.id, diff --git a/tests/resolvers/Mutation/sendMessageToChat.spec.ts b/tests/resolvers/Mutation/sendMessageToChat.spec.ts index ace1591c82..b661488ca6 100644 --- a/tests/resolvers/Mutation/sendMessageToChat.spec.ts +++ b/tests/resolvers/Mutation/sendMessageToChat.spec.ts @@ -72,13 +72,13 @@ afterAll(async () => { await disconnect(MONGOOSE_INSTANCE); }); -describe("resolvers -> Mutation -> sendMessageToDirectChat", () => { +describe("resolvers -> Mutation -> sendMessageToChat", () => { afterEach(async () => { vi.doUnmock("../../../src/constants"); vi.resetModules(); }); - it(`throws NotFoundError if no directChat exists with _id === args.chatId`, async () => { + it(`throws NotFoundError if no chat exists with _id === args.chatId`, async () => { const { requestContext } = await import("../../../src/libraries"); const spy = vi .spyOn(requestContext, "translate") @@ -128,7 +128,7 @@ describe("resolvers -> Mutation -> sendMessageToDirectChat", () => { } }); - it(`creates the directChatMessage and returns it`, async () => { + it(`creates the chatMessage and returns it`, async () => { await Chat.updateOne( { _id: testChat._id, diff --git a/tests/resolvers/Subscription/messageSentToChat.spec.ts b/tests/resolvers/Subscription/messageSentToChat.spec.ts index cf3cc484da..768b0f5c47 100644 --- a/tests/resolvers/Subscription/messageSentToChat.spec.ts +++ b/tests/resolvers/Subscription/messageSentToChat.spec.ts @@ -54,7 +54,7 @@ describe("src -> resolvers -> Subscription -> messageSentToChat", () => { expect(await filterFunction(payload, variables)).toBe(true); }); - it("user is not notified if it is not a part of DirectChat", async () => { + it("user is not notified if it is not a part of chat", async () => { const { messageSentToChat: messageSentToChatPayload } = await import( "../../../src/resolvers/Subscription/messageSentToChat" ); From 3f83e44b3d8be2cf92f39980534c5f775767f295 Mon Sep 17 00:00:00 2001 From: Meetul Rathore Date: Sat, 2 Nov 2024 13:58:21 +0530 Subject: [PATCH 3/6] feat: Add filter and sort for userTags (GSoC) (#2635) * implement tagsAssignedWith connection on the User schema * fix OrganizationTagUser indexing * add ancestorTags field and resolver on the UserTag schema * add user tags filter and sort * add error handling to parseWhere and parseSortedBy * add tests * minor correction * change utility function names * change error paths * remove getUserTagAncestors query * update TagUser model to include organizationId * fix tests * add ts-doc comments * fix formatting * add coderabbitai suggested changes * coderabbitai changes * restore package.json * more changes * move userTags pagination utils * refactor --- locales/en.json | 3 +- locales/fr.json | 3 +- locales/hi.json | 3 +- locales/sp.json | 3 +- locales/zh.json | 3 +- schema.graphql | 42 ++- src/constants.ts | 3 +- src/models/OrganizationTagUser.ts | 18 +- src/models/TagUser.ts | 5 + src/resolvers/Mutation/addPeopleToUserTag.ts | 8 +- src/resolvers/Mutation/assignToUserTags.ts | 8 +- src/resolvers/Mutation/assignUserTag.ts | 8 +- src/resolvers/Organization/userTags.ts | 45 ++- src/resolvers/Query/getUserTagAncestors.ts | 45 --- src/resolvers/Query/index.ts | 2 - src/resolvers/User/index.ts | 4 +- src/resolvers/User/tagsAssignedWith.ts | 163 ++++++++ src/resolvers/UserTag/ancestorTags.ts | 41 ++ src/resolvers/UserTag/childTags.ts | 40 +- src/resolvers/UserTag/index.ts | 2 + src/resolvers/UserTag/usersAssignedTo.ts | 116 ++++-- src/resolvers/UserTag/usersToAssignTo.ts | 106 +++--- src/typeDefs/inputs.ts | 31 ++ src/typeDefs/queries.ts | 2 - src/typeDefs/types.ts | 11 + src/types/generatedGraphQLTypes.ts | 61 ++- .../getUserTagGraphQLConnectionFilter.ts | 67 ++++ .../getUserTagGraphQLConnectionSort.ts | 45 +++ ...getUserTagMemberGraphQLConnectionFilter.ts | 81 ++++ .../userTagsPaginationUtils/index.ts | 6 + .../parseUserTagMemberWhere.ts | 73 ++++ .../parseUserTagSortedBy.ts | 50 +++ .../parseUserTagWhere.ts | 61 +++ tests/helpers/tags.ts | 3 + .../Mutation/assignToUserTags.spec.ts | 4 + .../Mutation/removeFromUserTags.spec.ts | 4 + .../resolvers/Mutation/removeUserTag.spec.ts | 3 + .../Mutation/unassignUserTag.spec.ts | 3 + tests/resolvers/Organization/userTags.spec.ts | 76 +++- tests/resolvers/Query/getUserTag.spec.ts | 6 +- .../Query/getUserTagAncestors.spec.ts | 64 ---- tests/resolvers/User/tagsAssignedWith.spec.ts | 128 +++++++ tests/resolvers/UserTag/ancestorTags.spec.ts | 50 +++ .../resolvers/UserTag/usersAssignedTo.spec.ts | 104 +++++- .../resolvers/UserTag/usersToAssignTo.spec.ts | 43 --- .../utilities/userTagsPaginationUtils.spec.ts | 350 ++++++++++++++++++ 46 files changed, 1694 insertions(+), 303 deletions(-) delete mode 100644 src/resolvers/Query/getUserTagAncestors.ts create mode 100644 src/resolvers/User/tagsAssignedWith.ts create mode 100644 src/resolvers/UserTag/ancestorTags.ts create mode 100644 src/utilities/userTagsPaginationUtils/getUserTagGraphQLConnectionFilter.ts create mode 100644 src/utilities/userTagsPaginationUtils/getUserTagGraphQLConnectionSort.ts create mode 100644 src/utilities/userTagsPaginationUtils/getUserTagMemberGraphQLConnectionFilter.ts create mode 100644 src/utilities/userTagsPaginationUtils/index.ts create mode 100644 src/utilities/userTagsPaginationUtils/parseUserTagMemberWhere.ts create mode 100644 src/utilities/userTagsPaginationUtils/parseUserTagSortedBy.ts create mode 100644 src/utilities/userTagsPaginationUtils/parseUserTagWhere.ts delete mode 100644 tests/resolvers/Query/getUserTagAncestors.spec.ts create mode 100644 tests/resolvers/User/tagsAssignedWith.spec.ts create mode 100644 tests/resolvers/UserTag/ancestorTags.spec.ts create mode 100644 tests/utilities/userTagsPaginationUtils.spec.ts diff --git a/locales/en.json b/locales/en.json index d989fa282c..58ca2bd8ff 100644 --- a/locales/en.json +++ b/locales/en.json @@ -35,5 +35,6 @@ "registrant.alreadyUnregistered": "Already unregistered for the event", "translation.alreadyPresent": "Translation Already Present", "translation.notFound": "Translation not found", - "parameter.missing": "Missing Skip parameter. Set it to either 0 or some other value not found" + "parameter.missing": "Missing Skip parameter. Set it to either 0 or some other value not found", + "tag.alreadyExists": "A tag with the same name already exists at this level" } diff --git a/locales/fr.json b/locales/fr.json index 4b299485c5..f54df13c4c 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -34,5 +34,6 @@ "registrant.alreadyUnregistered": "Déjà non inscrit à l'événement", "translation.alreadyPresent": "Traduction déjà présente", "translation.notFound": "Traduction introuvable", - "parameter.missing": "Paramètre de saut manquant. Réglez-le sur 0 ou sur une autre valeur" + "parameter.missing": "Paramètre de saut manquant. Réglez-le sur 0 ou sur une autre valeur", + "tag.alreadyExists": "Un tag avec le même nom existe déjà à ce niveau" } diff --git a/locales/hi.json b/locales/hi.json index b21fb5775b..52b87cbae8 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -35,5 +35,6 @@ "registrant.alreadyUnregistered": "घटना के लिए पहले से ही अपंजीकृत", "translation.alreadyPresent": "अनुवाद पहले से मौजूद है", "translation.notFound": "अनुवाद नहीं मिला", - "parameter.missing": "छोड़ें पैरामीटर मौजूद नहीं है. इसे 0 या किसी अन्य मान पर सेट करें" + "parameter.missing": "छोड़ें पैरामीटर मौजूद नहीं है. इसे 0 या किसी अन्य मान पर सेट करें", + "tag.alreadyExists": "इस स्तर पर समान नाम वाला टैग पहले से मौजूद है" } diff --git a/locales/sp.json b/locales/sp.json index e468585977..f291588472 100644 --- a/locales/sp.json +++ b/locales/sp.json @@ -34,5 +34,6 @@ "registrant.alreadyUnregistered": "Ya no está registrado para el evento", "translation.alreadyPresent": "Traducción ya presente", "translation.notFound": "Traducción no encontrada", - "parameter.missing": "Falta el parámetro Omitir. Establézcalo en 0 o en algún otro valor" + "parameter.missing": "Falta el parámetro Omitir. Establézcalo en 0 o en algún otro valor", + "tag.alreadyExists": "Ya existe una etiqueta con el mismo nombre en este nivel" } diff --git a/locales/zh.json b/locales/zh.json index 511a75784a..3f065d31bf 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -34,5 +34,6 @@ "registrant.alreadyUnregistered": "已取消注册该活动", "translation.alreadyPresent": "翻译已经存在", "translation.notFound": "找不到翻译", - "parameter.missing": "缺少跳过参数。将其设置为 0 或其他值" + "parameter.missing": "缺少跳过参数。将其设置为 0 或其他值", + "tag.alreadyExists": "此级别已存在相同名称的标签" } diff --git a/schema.graphql b/schema.graphql index 7acb77d8a4..8fcaa7d503 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1214,7 +1214,7 @@ type Organization { posts(after: String, before: String, first: PositiveInt, last: PositiveInt): PostsConnection updatedAt: DateTime! userRegistrationRequired: Boolean! - userTags(after: String, before: String, first: PositiveInt, last: PositiveInt): UserTagsConnection + userTags(after: String, before: String, first: PositiveInt, last: PositiveInt, sortedBy: UserTagSortedByInput, where: UserTagWhereInput): UserTagsConnection venues: [Venue] visibleInSearch: Boolean! } @@ -1497,7 +1497,6 @@ type Query { getPledgesByUserId(orderBy: PledgeOrderByInput, userId: ID!, where: PledgeWhereInput): [FundraisingCampaignPledge] getPlugins: [Plugin] getUserTag(id: ID!): UserTag - getUserTagAncestors(id: ID!): [UserTag] getVenueByOrgId(first: Int, orderBy: VenueOrderByInput, orgId: ID!, skip: Int, where: VenueWhereInput): [Venue] getlanguage(lang_code: String!): [Translation] hasSubmittedFeedback(eventId: ID!, userId: ID!): Boolean @@ -1859,6 +1858,10 @@ input UserInput { selectedOrganization: ID! } +input UserNameWhereInput { + starts_with: String! +} + type UserNotAuthorizedAdminError implements Error { message: String! } @@ -1900,11 +1903,14 @@ type UserTag { """A field to get the mongodb object id identifier for this UserTag.""" _id: ID! + """A field to traverse the ancestor tags of this UserTag.""" + ancestorTags: [UserTag] + """ A connection field to traverse a list of UserTag this UserTag is a parent to. """ - childTags(after: String, before: String, first: PositiveInt, last: PositiveInt): UserTagsConnection + childTags(after: String, before: String, first: PositiveInt, last: PositiveInt, sortedBy: UserTagSortedByInput, where: UserTagWhereInput): UserTagsConnection """A field to get the name of this UserTag.""" name: String! @@ -1919,13 +1925,39 @@ type UserTag { A connection field to traverse a list of User this UserTag is assigned to. """ - usersAssignedTo(after: String, before: String, first: PositiveInt, last: PositiveInt): UsersConnection + usersAssignedTo(after: String, before: String, first: PositiveInt, last: PositiveInt, sortedBy: UserTagUsersAssignedToSortedByInput, where: UserTagUsersAssignedToWhereInput): UsersConnection """ A connection field to traverse a list of Users this UserTag is not assigned to, to see and select among them and assign this tag. """ - usersToAssignTo(after: String, before: String, first: PositiveInt, last: PositiveInt): UsersConnection + usersToAssignTo(after: String, before: String, first: PositiveInt, last: PositiveInt, where: UserTagUsersToAssignToWhereInput): UsersConnection +} + +input UserTagNameWhereInput { + starts_with: String! +} + +input UserTagSortedByInput { + id: SortedByOrder! +} + +input UserTagUsersAssignedToSortedByInput { + id: SortedByOrder! +} + +input UserTagUsersAssignedToWhereInput { + firstName: UserNameWhereInput + lastName: UserNameWhereInput +} + +input UserTagUsersToAssignToWhereInput { + firstName: UserNameWhereInput + lastName: UserNameWhereInput +} + +input UserTagWhereInput { + name: UserTagNameWhereInput } """A default connection on the UserTag type.""" diff --git a/src/constants.ts b/src/constants.ts index b4d4d5939d..71a5d0eab3 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -471,8 +471,7 @@ export const NO_CHANGE_IN_TAG_NAME = Object.freeze({ }); export const TAG_ALREADY_EXISTS = Object.freeze({ - MESSAGE: - "A tag with the same name and the same parent tag already exists for this organization.", + MESSAGE: "A tag with the same name already exists at this level", CODE: "tag.alreadyExists", PARAM: "tag.alreadyExists", }); diff --git a/src/models/OrganizationTagUser.ts b/src/models/OrganizationTagUser.ts index 467a500df5..62228951e5 100644 --- a/src/models/OrganizationTagUser.ts +++ b/src/models/OrganizationTagUser.ts @@ -45,10 +45,22 @@ const organizationTagUserSchema = new Schema({ }, }); -// Index to ensure unique combination of organizationId, parentOrganizationTagUserId, and name +// Define partial indexes to enforce the unique constraints +// two tags at the same level can't have the same name organizationTagUserSchema.index( - { organizationId: 1, parentOrganizationTagUserId: 1, name: 1 }, - { unique: true }, + { organizationId: 1, name: 1 }, + { + unique: true, + partialFilterExpression: { parentTagId: { $eq: null } }, + }, +); + +organizationTagUserSchema.index( + { organizationId: 1, parentTagId: 1, name: 1 }, + { + unique: true, + partialFilterExpression: { parentTagId: { $ne: null } }, + }, ); // Add logging middleware for organizationTagUserSchema diff --git a/src/models/TagUser.ts b/src/models/TagUser.ts index cd2ccb8d0c..fd7889ca35 100644 --- a/src/models/TagUser.ts +++ b/src/models/TagUser.ts @@ -30,6 +30,11 @@ const tagUserSchema = new Schema({ ref: "OrganizationTagUser", required: true, }, + organizationId: { + type: Schema.Types.ObjectId, + ref: "Organization", + required: true, + }, tagColor: { type: String, required: false, diff --git a/src/resolvers/Mutation/addPeopleToUserTag.ts b/src/resolvers/Mutation/addPeopleToUserTag.ts index 77ad67bae3..4be0fd68f1 100644 --- a/src/resolvers/Mutation/addPeopleToUserTag.ts +++ b/src/resolvers/Mutation/addPeopleToUserTag.ts @@ -187,7 +187,13 @@ export const addPeopleToUserTag: MutationResolvers["addPeopleToUserTag"] = allAncestorTags.map((tagId) => ({ updateOne: { filter: { userId: user._id, tagId }, - update: { $setOnInsert: { userId: user._id, tagId } }, + update: { + $setOnInsert: { + userId: user._id, + tagId, + organizationId: tag.organizationId, + }, + }, upsert: true, setDefaultsOnInsert: true, }, diff --git a/src/resolvers/Mutation/assignToUserTags.ts b/src/resolvers/Mutation/assignToUserTags.ts index 0cc3e40783..0aa6ad0ce0 100644 --- a/src/resolvers/Mutation/assignToUserTags.ts +++ b/src/resolvers/Mutation/assignToUserTags.ts @@ -153,7 +153,13 @@ export const assignToUserTags: MutationResolvers["assignToUserTags"] = async ( Array.from(allTagsToAssign).map((tagId) => ({ updateOne: { filter: { userId, tagId: new Types.ObjectId(tagId) }, - update: { $setOnInsert: { userId, tagId: new Types.ObjectId(tagId) } }, + update: { + $setOnInsert: { + userId, + tagId: new Types.ObjectId(tagId), + organizationId: currentTag.organizationId, + }, + }, upsert: true, setDefaultsOnInsert: true, }, diff --git a/src/resolvers/Mutation/assignUserTag.ts b/src/resolvers/Mutation/assignUserTag.ts index ee2186642d..d7f076ae8a 100644 --- a/src/resolvers/Mutation/assignUserTag.ts +++ b/src/resolvers/Mutation/assignUserTag.ts @@ -171,7 +171,13 @@ export const assignUserTag: MutationResolvers["assignUserTag"] = async ( const tagUserDocs = allAncestorTags.map((tagId) => ({ updateOne: { filter: { userId: assigneeId, tagId }, - update: { $setOnInsert: { userId: assigneeId, tagId } }, + update: { + $setOnInsert: { + userId: assigneeId, + tagId, + organizationId: tag.organizationId, + }, + }, upsert: true, setDefaultsOnInsert: true, }, diff --git a/src/resolvers/Organization/userTags.ts b/src/resolvers/Organization/userTags.ts index 2b4160cf36..c82cceaef7 100644 --- a/src/resolvers/Organization/userTags.ts +++ b/src/resolvers/Organization/userTags.ts @@ -2,9 +2,7 @@ import type { OrganizationResolvers } from "../../types/generatedGraphQLTypes"; import type { InterfaceOrganizationTagUser } from "../../models"; import { OrganizationTagUser } from "../../models"; import { - getCommonGraphQLConnectionFilter, - getCommonGraphQLConnectionSort, - parseGraphQLConnectionArguments, + parseGraphQLConnectionArgumentsWithSortedByAndWhere, transformToDefaultGraphQLConnection, type DefaultGraphQLArgumentError, type ParseGraphQLConnectionCursorArguments, @@ -13,6 +11,12 @@ import { import { GraphQLError } from "graphql"; import { MAXIMUM_FETCH_LIMIT } from "../../constants"; import type { Types } from "mongoose"; +import { + getUserTagGraphQLConnectionFilter, + getUserTagGraphQLConnectionSort, + parseUserTagSortedBy, + parseUserTagWhere, +} from "../../utilities/userTagsPaginationUtils"; /** * Resolver function for the `userTags` field of an `Organization`. @@ -37,14 +41,20 @@ export const userTags: OrganizationResolvers["userTags"] = async ( parent, args, ) => { + const parseWhereResult = parseUserTagWhere(args.where); + const parseSortedByResult = parseUserTagSortedBy(args.sortedBy); + const parseGraphQLConnectionArgumentsResult = - await parseGraphQLConnectionArguments({ + await parseGraphQLConnectionArgumentsWithSortedByAndWhere({ args, - parseCursor: (args) => + parseSortedByResult, + parseWhereResult, + parseCursor: /* c8 ignore start */ (args) => parseCursor({ ...args, organizationId: parent._id, }), + /* c8 ignore stop */ maximumLimit: MAXIMUM_FETCH_LIMIT, }); @@ -59,26 +69,43 @@ export const userTags: OrganizationResolvers["userTags"] = async ( const { parsedArgs } = parseGraphQLConnectionArgumentsResult; - const filter = getCommonGraphQLConnectionFilter({ + const objectListFilter = getUserTagGraphQLConnectionFilter({ cursor: parsedArgs.cursor, direction: parsedArgs.direction, + sortById: parsedArgs.sort.sortById, + nameStartsWith: parsedArgs.filter.nameStartsWith, }); - const sort = getCommonGraphQLConnectionSort({ + // don't use _id as a filter in while counting the documents + // _id is only used for pagination + const totalCountFilter = Object.fromEntries( + Object.entries(objectListFilter).filter(([key]) => key !== "_id"), + ); + + const sort = getUserTagGraphQLConnectionSort({ direction: parsedArgs.direction, + sortById: parsedArgs.sort.sortById, }); + // if there's no search input, we'll list all the root tag + // otherwise we'll also list the subtags matching the filter + const parentTagIdFilter = parsedArgs.filter.nameStartsWith + ? {} + : { parentTagId: null }; + const [objectList, totalCount] = await Promise.all([ OrganizationTagUser.find({ - ...filter, + ...objectListFilter, + ...parentTagIdFilter, organizationId: parent._id, - parentTagId: null, }) .sort(sort) .limit(parsedArgs.limit) .lean() .exec(), OrganizationTagUser.find({ + ...totalCountFilter, + ...parentTagIdFilter, organizationId: parent._id, }) .countDocuments() diff --git a/src/resolvers/Query/getUserTagAncestors.ts b/src/resolvers/Query/getUserTagAncestors.ts deleted file mode 100644 index cf516eb43e..0000000000 --- a/src/resolvers/Query/getUserTagAncestors.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { InterfaceOrganizationTagUser } from "../../models"; -import { OrganizationTagUser } from "../../models"; -import { errors, requestContext } from "../../libraries"; -import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; -import { TAG_NOT_FOUND } from "../../constants"; - -/** - * Retrieves the ancestor tags of a given user tag. - * - * This function fetches the ancestor tags of a specific user tag from the database. If the user tag - * is not found, it throws an error indicating that the item does not exist. - * - * @param _parent - This parameter is not used in this resolver function. - * @param args - The arguments provided by the GraphQL query, including the ID of the given user tag. - * - * @returns The ancestor tags of the user tag. - */ - -export const getUserTagAncestors: QueryResolvers["getUserTagAncestors"] = - async (_parent, args) => { - let currentTag = await OrganizationTagUser.findById(args.id).lean(); - - if (!currentTag) { - throw new errors.NotFoundError( - requestContext.translate(TAG_NOT_FOUND.MESSAGE), - TAG_NOT_FOUND.CODE, - TAG_NOT_FOUND.PARAM, - ); - } - - const tagAncestors = [currentTag]; - - while (currentTag?.parentTagId) { - const currentParent = (await OrganizationTagUser.findById( - currentTag.parentTagId, - ).lean()) as InterfaceOrganizationTagUser | null; - - if (currentParent) { - tagAncestors.push(currentParent); - currentTag = currentParent; - } - } - - return tagAncestors.reverse(); - }; diff --git a/src/resolvers/Query/index.ts b/src/resolvers/Query/index.ts index e6ca8a74ed..8474b60298 100644 --- a/src/resolvers/Query/index.ts +++ b/src/resolvers/Query/index.ts @@ -32,7 +32,6 @@ import { getPledgesByUserId } from "./getPledgesByUserId"; import { getPlugins } from "./getPlugins"; import { getlanguage } from "./getlanguage"; import { getUserTag } from "./getUserTag"; -import { getUserTagAncestors } from "./getUserTagAncestors"; import { me } from "./me"; import { myLanguage } from "./myLanguage"; import { organizations } from "./organizations"; @@ -81,7 +80,6 @@ export const Query: QueryResolvers = { getlanguage, getPlugins, getUserTag, - getUserTagAncestors, isSampleOrganization, me, myLanguage, diff --git a/src/resolvers/User/index.ts b/src/resolvers/User/index.ts index 538fe582cf..79d556c2b3 100644 --- a/src/resolvers/User/index.ts +++ b/src/resolvers/User/index.ts @@ -1,8 +1,8 @@ import type { UserResolvers } from "../../types/generatedGraphQLTypes"; -// import { tagsAssignedWith } from "./tagsAssignedWith"; +import { tagsAssignedWith } from "./tagsAssignedWith"; import { posts } from "./posts"; export const User: UserResolvers = { - // tagsAssignedWith, + tagsAssignedWith, posts, }; diff --git a/src/resolvers/User/tagsAssignedWith.ts b/src/resolvers/User/tagsAssignedWith.ts new file mode 100644 index 0000000000..92212b1633 --- /dev/null +++ b/src/resolvers/User/tagsAssignedWith.ts @@ -0,0 +1,163 @@ +import type { UserResolvers } from "../../types/generatedGraphQLTypes"; +import type { + InterfaceOrganizationTagUser, + InterfaceTagUser, +} from "../../models"; +import { TagUser } from "../../models"; +import { + type DefaultGraphQLArgumentError, + type ParseGraphQLConnectionCursorArguments, + type ParseGraphQLConnectionCursorResult, + getCommonGraphQLConnectionFilter, + getCommonGraphQLConnectionSort, + parseGraphQLConnectionArguments, + transformToDefaultGraphQLConnection, +} from "../../utilities/graphQLConnection"; +import { GraphQLError } from "graphql"; +import { MAXIMUM_FETCH_LIMIT } from "../../constants"; +import type { Types } from "mongoose"; + +/** + * Resolver function for the `tagsAssignedWith` field of a `User`. + * + * This resolver is used to resolve the `tagsAssignedWith` field of a `User` type. + * + * @param parent - The parent object representing the user. It contains information about the user, including the ID of the user. + * @param args - The arguments provided to the field. These arguments are used to filter, sort, and paginate the tags assigned to the user. + * @returns A promise that resolves to a connection object containing the tags assigned to the user. + * + * @see TagUser - The TagUser model used to interact with the tag users collection in the database. + * @see parseGraphQLConnectionArguments - The function used to parse the GraphQL connection arguments (filter, sort, pagination). + * @see transformToDefaultGraphQLConnection - The function used to transform the list of tags assigned to the user into a connection object. + * @see getCommonGraphQLConnectionFilter - The function used to get the common filter object for the GraphQL connection. + * @see getCommonGraphQLConnectionSort - The function used to get the common sort object for the GraphQL connection. + * @see MAXIMUM_FETCH_LIMIT - The maximum number of users that can be fetched in a single request. + * @see GraphQLError - The error class used to throw GraphQL errors. + * @see UserTagResolvers - The type definition for the resolvers of the User fields. + * + */ +export const tagsAssignedWith: UserResolvers["tagsAssignedWith"] = async ( + parent, + args, +) => { + const parseGraphQLConnectionArgumentsResult = + await parseGraphQLConnectionArguments({ + args, + parseCursor: /* c8 ignore start */ (args) => + parseCursor({ + ...args, + userId: parent._id, + }), + /* c8 ignore stop */ + maximumLimit: MAXIMUM_FETCH_LIMIT, + }); + + if (!parseGraphQLConnectionArgumentsResult.isSuccessful) { + throw new GraphQLError("Invalid arguments provided.", { + extensions: { + code: "INVALID_ARGUMENTS", + errors: parseGraphQLConnectionArgumentsResult.errors, + }, + }); + } + + const { parsedArgs } = parseGraphQLConnectionArgumentsResult; + + const filter = getCommonGraphQLConnectionFilter({ + cursor: parsedArgs.cursor, + direction: parsedArgs.direction, + }); + + const sort = getCommonGraphQLConnectionSort({ + direction: parsedArgs.direction, + }); + + const [objectList, totalCount] = await Promise.all([ + TagUser.find({ + ...filter, + userId: parent._id, + organizationId: args.organizationId, + }) + .sort(sort) + .limit(parsedArgs.limit) + .populate("tagId") + .lean() + .exec(), + + TagUser.find({ + userId: parent._id, + organizationId: args.organizationId, + }) + .countDocuments() + .exec(), + ]); + + return transformToDefaultGraphQLConnection< + ParsedCursor, + InterfaceTagUser, + InterfaceOrganizationTagUser + >({ + createNode: (object) => { + return object.tagId as InterfaceOrganizationTagUser; + }, + objectList, + parsedArgs, + totalCount, + }); +}; + +/* +This is typescript type of the parsed cursor for this connection resolver. +*/ +type ParsedCursor = string; + +/** + * Parses the cursor value for the `tagsAssignedWith` connection resolver. + * + * This function is used to parse the cursor value provided to the `tagsAssignedWith` connection resolver. + * + * @param cursorValue - The cursor value to be parsed. + * @param cursorName - The name of the cursor argument. + * @param cursorPath - The path of the cursor argument in the GraphQL query. + * @param userId - The ID of the user for which assigned tags are being queried. + * @returns An object containing the parsed cursor value or an array of errors if the cursor value is invalid. + * + * @see TagUser - The TagUser model used to interact with the tag users collection in the database. + * @see DefaultGraphQLArgumentError - The type definition for the default GraphQL argument error. + * @see ParseGraphQLConnectionCursorArguments - The type definition for the arguments provided to the parseCursor function. + * @see ParseGraphQLConnectionCursorResult - The type definition for the result of the parseCursor function. + * + */ +export const parseCursor = async ({ + cursorValue, + cursorName, + cursorPath, + userId, +}: ParseGraphQLConnectionCursorArguments & { + userId: string | Types.ObjectId; +}): ParseGraphQLConnectionCursorResult => { + const errors: DefaultGraphQLArgumentError[] = []; + const tagUser = await TagUser.findOne({ + _id: cursorValue, + userId, + }); + + if (!tagUser) { + errors.push({ + message: `Argument ${cursorName} is an invalid cursor.`, + path: cursorPath, + }); + } + + if (errors.length !== 0) { + return { + errors, + isSuccessful: false, + }; + } + + return { + isSuccessful: true, + parsedCursor: cursorValue, + }; +}; diff --git a/src/resolvers/UserTag/ancestorTags.ts b/src/resolvers/UserTag/ancestorTags.ts new file mode 100644 index 0000000000..890a6cd51e --- /dev/null +++ b/src/resolvers/UserTag/ancestorTags.ts @@ -0,0 +1,41 @@ +import type { InterfaceOrganizationTagUser } from "../../models"; +import { OrganizationTagUser } from "../../models"; +import type { UserTagResolvers } from "../../types/generatedGraphQLTypes"; + +/** + * Resolver function for the `ancestorTags` field of an `OrganizationTagUser`. + * + * This function retrieves the ancestor tags of a specific organization user tag by recursively finding + * each parent tag until the root tag (where parentTagId is null) is reached. It then reverses the order, + * appends the current tag at the end, and returns the final array of tags. + * + * @param parent - The parent object representing the user tag. It contains information about the tag, including its ID and parentTagId. + * @returns A promise that resolves to the ordered array of ancestor tag documents found in the database. + */ +export const ancestorTags: UserTagResolvers["ancestorTags"] = async ( + parent, +) => { + // Initialize an array to collect the ancestor tags + const ancestorTags: InterfaceOrganizationTagUser[] = []; + + // Start with the current parentTagId + let currentParentId = parent.parentTagId; + + // Traverse up the hierarchy to find all ancestorTags + while (currentParentId) { + const tag = await OrganizationTagUser.findById(currentParentId).lean(); + + if (!tag) break; + + // Add the found tag to the ancestorTags array + ancestorTags.push(tag); + + // Move up to the next parent + currentParentId = tag.parentTagId; + } + + // Reverse the ancestorTags to have the root tag first, then append the current tag + ancestorTags.reverse(); + + return ancestorTags; +}; diff --git a/src/resolvers/UserTag/childTags.ts b/src/resolvers/UserTag/childTags.ts index ae95fb2b16..3c886d35d2 100644 --- a/src/resolvers/UserTag/childTags.ts +++ b/src/resolvers/UserTag/childTags.ts @@ -2,9 +2,7 @@ import type { UserTagResolvers } from "../../types/generatedGraphQLTypes"; import type { InterfaceOrganizationTagUser } from "../../models"; import { OrganizationTagUser } from "../../models"; import { - getCommonGraphQLConnectionFilter, - getCommonGraphQLConnectionSort, - parseGraphQLConnectionArguments, + parseGraphQLConnectionArgumentsWithSortedByAndWhere, transformToDefaultGraphQLConnection, type DefaultGraphQLArgumentError, type ParseGraphQLConnectionCursorArguments, @@ -13,6 +11,12 @@ import { import { GraphQLError } from "graphql"; import { MAXIMUM_FETCH_LIMIT } from "../../constants"; import type { Types } from "mongoose"; +import { + getUserTagGraphQLConnectionFilter, + getUserTagGraphQLConnectionSort, + parseUserTagSortedBy, + parseUserTagWhere, +} from "../../utilities/userTagsPaginationUtils"; /** * Resolver function for the `childTags` field of a `UserTag`. @@ -26,8 +30,8 @@ import type { Types } from "mongoose"; * @see OrganizationTagUser - The OrganizationTagUser model used to interact with the organization tag users collection in the database. * @see parseGraphQLConnectionArguments - The function used to parse the GraphQL connection arguments (filter, sort, pagination). * @see transformToDefaultGraphQLConnection - The function used to transform the list of child tags into a connection object. - * @see getCommonGraphQLConnectionFilter - The function used to get the common filter object for the GraphQL connection. - * @see getCommonGraphQLConnectionSort - The function used to get the common sort object for the GraphQL connection. + * @see getGraphQLConnectionFilter - The function used to get the common filter object for the GraphQL connection. + * @see getGraphQLConnectionSort - The function used to get the common sort object for the GraphQL connection. * @see MAXIMUM_FETCH_LIMIT - The maximum number of child tags that can be fetched in a single request. * @see GraphQLError - The error class used to throw GraphQL errors. * @see UserTagResolvers - The type definition for the resolvers of the UserTag fields. @@ -37,14 +41,20 @@ export const childTags: UserTagResolvers["childTags"] = async ( parent, args, ) => { + const parseWhereResult = parseUserTagWhere(args.where); + const parseSortedByResult = parseUserTagSortedBy(args.sortedBy); + const parseGraphQLConnectionArgumentsResult = - await parseGraphQLConnectionArguments({ + await parseGraphQLConnectionArgumentsWithSortedByAndWhere({ args, - parseCursor: (args) => + parseSortedByResult, + parseWhereResult, + parseCursor: /* c8 ignore start */ (args) => parseCursor({ ...args, parentTagId: parent._id, }), + /* c8 ignore stop */ maximumLimit: MAXIMUM_FETCH_LIMIT, }); @@ -59,18 +69,27 @@ export const childTags: UserTagResolvers["childTags"] = async ( const { parsedArgs } = parseGraphQLConnectionArgumentsResult; - const filter = getCommonGraphQLConnectionFilter({ + const objectListFilter = getUserTagGraphQLConnectionFilter({ cursor: parsedArgs.cursor, direction: parsedArgs.direction, + sortById: parsedArgs.sort.sortById, + nameStartsWith: parsedArgs.filter.nameStartsWith, }); - const sort = getCommonGraphQLConnectionSort({ + // don't use _id as a filter in while counting the documents + // _id is only used for pagination + const totalCountFilter = Object.fromEntries( + Object.entries(objectListFilter).filter(([key]) => key !== "_id"), + ); + + const sort = getUserTagGraphQLConnectionSort({ direction: parsedArgs.direction, + sortById: parsedArgs.sort.sortById, }); const [objectList, totalCount] = await Promise.all([ OrganizationTagUser.find({ - ...filter, + ...objectListFilter, parentTagId: parent._id, }) .sort(sort) @@ -78,6 +97,7 @@ export const childTags: UserTagResolvers["childTags"] = async ( .lean() .exec(), OrganizationTagUser.find({ + ...totalCountFilter, parentTagId: parent._id, }) .countDocuments() diff --git a/src/resolvers/UserTag/index.ts b/src/resolvers/UserTag/index.ts index e2f5fcd824..c2e8c73c5d 100644 --- a/src/resolvers/UserTag/index.ts +++ b/src/resolvers/UserTag/index.ts @@ -2,6 +2,7 @@ import type { UserTagResolvers } from "../../types/generatedGraphQLTypes"; import { childTags } from "./childTags"; import { organization } from "./organization"; import { parentTag } from "./parentTag"; +import { ancestorTags } from "./ancestorTags"; import { usersAssignedTo } from "./usersAssignedTo"; import { usersToAssignTo } from "./usersToAssignTo"; @@ -9,6 +10,7 @@ export const UserTag: UserTagResolvers = { childTags, organization, parentTag, + ancestorTags, usersAssignedTo, usersToAssignTo, }; diff --git a/src/resolvers/UserTag/usersAssignedTo.ts b/src/resolvers/UserTag/usersAssignedTo.ts index ebc8dd20a6..8722c94af2 100644 --- a/src/resolvers/UserTag/usersAssignedTo.ts +++ b/src/resolvers/UserTag/usersAssignedTo.ts @@ -5,14 +5,18 @@ import { type DefaultGraphQLArgumentError, type ParseGraphQLConnectionCursorArguments, type ParseGraphQLConnectionCursorResult, - getCommonGraphQLConnectionFilter, - getCommonGraphQLConnectionSort, - parseGraphQLConnectionArguments, + parseGraphQLConnectionArgumentsWithSortedByAndWhere, transformToDefaultGraphQLConnection, } from "../../utilities/graphQLConnection"; import { GraphQLError } from "graphql"; import { MAXIMUM_FETCH_LIMIT } from "../../constants"; -import type { Types } from "mongoose"; +import { Types } from "mongoose"; +import { + parseUserTagSortedBy, + parseUserTagMemberWhere, + getUserTagMemberGraphQLConnectionFilter, + getUserTagGraphQLConnectionSort, +} from "../../utilities/userTagsPaginationUtils"; /** * Resolver function for the `usersAssignedTo` field of a `UserTag`. @@ -37,14 +41,20 @@ export const usersAssignedTo: UserTagResolvers["usersAssignedTo"] = async ( parent, args, ) => { + const parseWhereResult = parseUserTagMemberWhere(args.where); + const parseSortedByResult = parseUserTagSortedBy(args.sortedBy); + const parseGraphQLConnectionArgumentsResult = - await parseGraphQLConnectionArguments({ + await parseGraphQLConnectionArgumentsWithSortedByAndWhere({ args, - parseCursor: (args) => + parseSortedByResult, + parseWhereResult, + parseCursor: /* c8 ignore start */ (args) => parseCursor({ ...args, tagId: parent._id, }), + /* c8 ignore stop */ maximumLimit: MAXIMUM_FETCH_LIMIT, }); @@ -59,31 +69,91 @@ export const usersAssignedTo: UserTagResolvers["usersAssignedTo"] = async ( const { parsedArgs } = parseGraphQLConnectionArgumentsResult; - const filter = getCommonGraphQLConnectionFilter({ + const objectListFilter = getUserTagMemberGraphQLConnectionFilter({ cursor: parsedArgs.cursor, direction: parsedArgs.direction, + sortById: parsedArgs.sort.sortById, + firstNameStartsWith: parsedArgs.filter.firstNameStartsWith, + lastNameStartsWith: parsedArgs.filter.lastNameStartsWith, }); - const sort = getCommonGraphQLConnectionSort({ + // Separate the _id filter from the rest + // _id filter will be applied on the TagUser model + // the rest on the User model referenced by the userId field + const { _id: tagUserIdFilter, ...userFilter } = objectListFilter; + const tagUserFilter = tagUserIdFilter ? { _id: tagUserIdFilter } : {}; + + const sort = getUserTagGraphQLConnectionSort({ direction: parsedArgs.direction, + sortById: parsedArgs.sort.sortById, }); + // create the filter object to match the user documents in the pipeline + const userMatchFilter = Object.fromEntries( + Object.entries(userFilter).map(([key, value]) => [`user.${key}`, value]), + ); + + // commonPipeline object for the query + const commonPipeline = [ + // Perform a left join with User collection on _id + { + $lookup: { + from: "users", + localField: "userId", + foreignField: "_id", + as: "user", + }, + }, + { + $unwind: "$user", + }, + // apply the userFilter to the user + { + $match: { + ...userMatchFilter, + }, + }, + ]; + + // we want to assign the userFilter to the user referenced by userId + // and the _id filter (specified by the after/before) to the TagUser document const [objectList, totalCount] = await Promise.all([ - TagUser.find({ - ...filter, - tagId: parent._id, - }) - .sort(sort) - .limit(parsedArgs.limit) - .populate("userId") - .lean() - .exec(), - - TagUser.find({ - tagId: parent._id, - }) - .countDocuments() - .exec(), + // First aggregation to get the TagUser list matching the filter criteria + TagUser.aggregate([ + { + $match: { + ...tagUserFilter, + tagId: new Types.ObjectId(parent._id), + }, + }, + ...commonPipeline, + { + $sort: sort, + }, + { + $limit: parsedArgs.limit, + }, + { + $addFields: { userId: "$user" }, + }, + { + $project: { + user: 0, + }, + }, + ]), + // Second aggregation to count the total TagUser documents matching the filter criteria + TagUser.aggregate([ + { + $match: { + tagId: new Types.ObjectId(parent._id), + }, + }, + ...commonPipeline, + { + $count: "totalCount", + }, + ]).then((res) => res[0]?.totalCount || 0), ]); return transformToDefaultGraphQLConnection< diff --git a/src/resolvers/UserTag/usersToAssignTo.ts b/src/resolvers/UserTag/usersToAssignTo.ts index 79cad7d573..dfadc04264 100644 --- a/src/resolvers/UserTag/usersToAssignTo.ts +++ b/src/resolvers/UserTag/usersToAssignTo.ts @@ -3,20 +3,23 @@ import type { InterfaceUser } from "../../models"; import { User } from "../../models"; import type { DefaultGraphQLArgumentError, - GraphQLConnectionTraversalDirection, ParseGraphQLConnectionCursorArguments, ParseGraphQLConnectionCursorResult, } from "../../utilities/graphQLConnection"; import { getCommonGraphQLConnectionSort, - parseGraphQLConnectionArguments, + parseGraphQLConnectionArgumentsWithWhere, transformToDefaultGraphQLConnection, } from "../../utilities/graphQLConnection"; import { GraphQLError } from "graphql"; import { MAXIMUM_FETCH_LIMIT } from "../../constants"; import { Types } from "mongoose"; +import { + getUserTagMemberGraphQLConnectionFilter, + parseUserTagMemberWhere, +} from "../../utilities/userTagsPaginationUtils"; /** * Resolver function for the `usersToAssignTo` field of a `UserTag`. @@ -39,13 +42,17 @@ export const usersToAssignTo: UserTagResolvers["usersToAssignTo"] = async ( parent, args, ) => { + const parseWhereResult = parseUserTagMemberWhere(args.where); + const parseGraphQLConnectionArgumentsResult = - await parseGraphQLConnectionArguments({ + await parseGraphQLConnectionArgumentsWithWhere({ args, - parseCursor: (args) => + parseWhereResult, + parseCursor: /* c8 ignore start */ (args) => parseCursor({ ...args, }), + /* c8 ignore stop */ maximumLimit: MAXIMUM_FETCH_LIMIT, }); @@ -60,24 +67,26 @@ export const usersToAssignTo: UserTagResolvers["usersToAssignTo"] = async ( const { parsedArgs } = parseGraphQLConnectionArgumentsResult; - const filter = getGraphQLConnectionFilter({ + const objectListFilter = getUserTagMemberGraphQLConnectionFilter({ cursor: parsedArgs.cursor, direction: parsedArgs.direction, + sortById: "DESCENDING", + firstNameStartsWith: parsedArgs.where.firstNameStartsWith, + lastNameStartsWith: parsedArgs.where.lastNameStartsWith, }); + // don't use _id as a filter in while counting the documents + // _id is only used for pagination + const totalCountFilter = Object.fromEntries( + Object.entries(objectListFilter).filter(([key]) => key !== "_id"), + ); + const sort = getCommonGraphQLConnectionSort({ direction: parsedArgs.direction, }); const commonPipeline = [ - // Step 1: Match users whose joinedOrgs contains the orgId - { - $match: { - ...filter, - joinedOrganizations: parent.organizationId, - }, - }, - // Step 2: Perform a left join with TagUser collection on userId + // Perform a left join with TagUser collection on userId { $lookup: { from: "tagusers", // Name of the collection holding TagUser documents @@ -86,12 +95,12 @@ export const usersToAssignTo: UserTagResolvers["usersToAssignTo"] = async ( as: "tagUsers", }, }, - // Step 3: Filter out users that have a tagUser document with the specified tagId + // Filter out users that have a tagUser document with the specified tagId { $match: { tagUsers: { $not: { - $elemMatch: { tagId: parent._id }, + $elemMatch: { tagId: new Types.ObjectId(parent._id) }, }, }, }, @@ -99,24 +108,34 @@ export const usersToAssignTo: UserTagResolvers["usersToAssignTo"] = async ( ]; // Execute the queries using the common pipeline - const [objectList, totalCountResult] = await Promise.all([ - // First aggregation to get the user list + const [objectList, totalCount] = await Promise.all([ + // First aggregation to get the user list matching the filter criteria User.aggregate([ + { + $match: { + ...objectListFilter, + joinedOrganizations: { $in: [parent.organizationId] }, + }, + }, ...commonPipeline, { $sort: { ...sort }, }, { $limit: parsedArgs.limit }, ]), - // Second aggregation to count total users - User.aggregate([...commonPipeline, { $count: "totalCount" }]), + // Second aggregation to count total users matching the filter criteria + User.aggregate([ + { + $match: { + ...totalCountFilter, + joinedOrganizations: { $in: [parent.organizationId] }, + }, + }, + ...commonPipeline, + { $count: "totalCount" }, + ]).then((res) => res[0]?.totalCount || 0), ]); - const totalCount = - totalCountResult.length > 0 ? totalCountResult[0].totalCount : 0; - - // The users and totalCount are now ready for use - return transformToDefaultGraphQLConnection< ParsedCursor, InterfaceUser, @@ -178,42 +197,3 @@ export const parseCursor = async ({ parsedCursor: cursorValue, }; }; - -type GraphQLConnectionFilter = - | { - _id: { - $lt: Types.ObjectId; - }; - } - | { - _id: { - $gt: Types.ObjectId; - }; - } - | Record; - -export const getGraphQLConnectionFilter = ({ - cursor, - direction, -}: { - cursor: string | null; - direction: GraphQLConnectionTraversalDirection; -}): GraphQLConnectionFilter => { - if (cursor !== null) { - if (direction === "BACKWARD") { - return { - _id: { - $gt: new Types.ObjectId(cursor), - }, - }; - } else { - return { - _id: { - $lt: new Types.ObjectId(cursor), - }, - }; - } - } else { - return {}; - } -}; diff --git a/src/typeDefs/inputs.ts b/src/typeDefs/inputs.ts index 3cd3c33450..a16daeda49 100644 --- a/src/typeDefs/inputs.ts +++ b/src/typeDefs/inputs.ts @@ -431,6 +431,18 @@ export const inputs = gql` selectedTagIds: [ID!]! } + input UserTagWhereInput { + name: UserTagNameWhereInput + } + + input UserTagNameWhereInput { + starts_with: String! + } + + input UserTagSortedByInput { + id: SortedByOrder! + } + input UpdateActionItemInput { assigneeId: ID preCompletionNotes: String @@ -607,6 +619,25 @@ export const inputs = gql` event_title_contains: String } + + input UserTagUsersAssignedToSortedByInput { + id: SortedByOrder! + } + + input UserTagUsersAssignedToWhereInput { + firstName: UserNameWhereInput + lastName: UserNameWhereInput + } + + input UserTagUsersToAssignToWhereInput { + firstName: UserNameWhereInput + lastName: UserNameWhereInput + } + + input UserNameWhereInput { + starts_with: String! + } + input PostUpdateInput { text: String title: String diff --git a/src/typeDefs/queries.ts b/src/typeDefs/queries.ts index b4e40c3e6c..7572b3387c 100644 --- a/src/typeDefs/queries.ts +++ b/src/typeDefs/queries.ts @@ -124,8 +124,6 @@ export const queries = gql` getUserTag(id: ID!): UserTag - getUserTagAncestors(id: ID!): [UserTag] - getAllNotesForAgendaItem(agendaItemId: ID!): [Note] advertisementsConnection( diff --git a/src/typeDefs/types.ts b/src/typeDefs/types.ts index d6bf4952b7..99c1d43720 100644 --- a/src/typeDefs/types.ts +++ b/src/typeDefs/types.ts @@ -423,6 +423,8 @@ export const types = gql` before: String first: PositiveInt last: PositiveInt + where: UserTagWhereInput + sortedBy: UserTagSortedByInput ): UserTagsConnection posts( after: String @@ -663,6 +665,10 @@ export const types = gql` """ parentTag: UserTag """ + A field to traverse the ancestor tags of this UserTag. + """ + ancestorTags: [UserTag] + """ A connection field to traverse a list of UserTag this UserTag is a parent to. """ @@ -671,6 +677,8 @@ export const types = gql` before: String first: PositiveInt last: PositiveInt + where: UserTagWhereInput + sortedBy: UserTagSortedByInput ): UserTagsConnection """ A connection field to traverse a list of User this UserTag is assigned @@ -681,6 +689,8 @@ export const types = gql` before: String first: PositiveInt last: PositiveInt + where: UserTagUsersAssignedToWhereInput + sortedBy: UserTagUsersAssignedToSortedByInput ): UsersConnection """ @@ -692,6 +702,7 @@ export const types = gql` before: String first: PositiveInt last: PositiveInt + where: UserTagUsersToAssignToWhereInput ): UsersConnection } diff --git a/src/types/generatedGraphQLTypes.ts b/src/types/generatedGraphQLTypes.ts index fe1f9b0567..a802e174be 100644 --- a/src/types/generatedGraphQLTypes.ts +++ b/src/types/generatedGraphQLTypes.ts @@ -1968,6 +1968,8 @@ export type OrganizationUserTagsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortedBy?: InputMaybe; + where?: InputMaybe; }; export type OrganizationCustomField = { @@ -2251,7 +2253,6 @@ export type Query = { getPledgesByUserId?: Maybe>>; getPlugins?: Maybe>>; getUserTag?: Maybe; - getUserTagAncestors?: Maybe>>; getVenueByOrgId?: Maybe>>; getlanguage?: Maybe>>; hasSubmittedFeedback?: Maybe; @@ -2468,11 +2469,6 @@ export type QueryGetUserTagArgs = { }; -export type QueryGetUserTagAncestorsArgs = { - id: Scalars['ID']['input']; -}; - - export type QueryGetVenueByOrgIdArgs = { first?: InputMaybe; orderBy?: InputMaybe; @@ -2943,6 +2939,10 @@ export type UserInput = { selectedOrganization: Scalars['ID']['input']; }; +export type UserNameWhereInput = { + starts_with: Scalars['String']['input']; +}; + export type UserNotAuthorizedAdminError = Error & { __typename?: 'UserNotAuthorizedAdminError'; message: Scalars['String']['output']; @@ -2987,6 +2987,8 @@ export type UserTag = { __typename?: 'UserTag'; /** A field to get the mongodb object id identifier for this UserTag. */ _id: Scalars['ID']['output']; + /** A field to traverse the ancestor tags of this UserTag. */ + ancestorTags?: Maybe>>; /** * A connection field to traverse a list of UserTag this UserTag is a * parent to. @@ -3016,6 +3018,8 @@ export type UserTagChildTagsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortedBy?: InputMaybe; + where?: InputMaybe; }; @@ -3024,6 +3028,8 @@ export type UserTagUsersAssignedToArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortedBy?: InputMaybe; + where?: InputMaybe; }; @@ -3032,6 +3038,33 @@ export type UserTagUsersToAssignToArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + where?: InputMaybe; +}; + +export type UserTagNameWhereInput = { + starts_with: Scalars['String']['input']; +}; + +export type UserTagSortedByInput = { + id: SortedByOrder; +}; + +export type UserTagUsersAssignedToSortedByInput = { + id: SortedByOrder; +}; + +export type UserTagUsersAssignedToWhereInput = { + firstName?: InputMaybe; + lastName?: InputMaybe; +}; + +export type UserTagUsersToAssignToWhereInput = { + firstName?: InputMaybe; + lastName?: InputMaybe; +}; + +export type UserTagWhereInput = { + name?: InputMaybe; }; /** A default connection on the UserTag type. */ @@ -3417,6 +3450,7 @@ export type ResolversTypes = { UserData: ResolverTypeWrapper & { appUserProfile?: Maybe, user: ResolversTypes['User'] }>; UserFamily: ResolverTypeWrapper; UserInput: UserInput; + UserNameWhereInput: UserNameWhereInput; UserNotAuthorizedAdminError: ResolverTypeWrapper; UserNotAuthorizedError: ResolverTypeWrapper; UserNotFoundError: ResolverTypeWrapper; @@ -3424,6 +3458,12 @@ export type ResolversTypes = { UserPhone: ResolverTypeWrapper; UserPhoneInput: UserPhoneInput; UserTag: ResolverTypeWrapper; + UserTagNameWhereInput: UserTagNameWhereInput; + UserTagSortedByInput: UserTagSortedByInput; + UserTagUsersAssignedToSortedByInput: UserTagUsersAssignedToSortedByInput; + UserTagUsersAssignedToWhereInput: UserTagUsersAssignedToWhereInput; + UserTagUsersToAssignToWhereInput: UserTagUsersToAssignToWhereInput; + UserTagWhereInput: UserTagWhereInput; UserTagsConnection: ResolverTypeWrapper & { edges: Array }>; UserTagsConnectionEdge: ResolverTypeWrapper & { node: ResolversTypes['UserTag'] }>; UserType: UserType; @@ -3608,12 +3648,19 @@ export type ResolversParentTypes = { UserData: Omit & { appUserProfile?: Maybe, user: ResolversParentTypes['User'] }; UserFamily: InterfaceUserFamilyModel; UserInput: UserInput; + UserNameWhereInput: UserNameWhereInput; UserNotAuthorizedAdminError: UserNotAuthorizedAdminError; UserNotAuthorizedError: UserNotAuthorizedError; UserNotFoundError: UserNotFoundError; UserPhone: UserPhone; UserPhoneInput: UserPhoneInput; UserTag: InterfaceOrganizationTagUserModel; + UserTagNameWhereInput: UserTagNameWhereInput; + UserTagSortedByInput: UserTagSortedByInput; + UserTagUsersAssignedToSortedByInput: UserTagUsersAssignedToSortedByInput; + UserTagUsersAssignedToWhereInput: UserTagUsersAssignedToWhereInput; + UserTagUsersToAssignToWhereInput: UserTagUsersToAssignToWhereInput; + UserTagWhereInput: UserTagWhereInput; UserTagsConnection: Omit & { edges: Array }; UserTagsConnectionEdge: Omit & { node: ResolversParentTypes['UserTag'] }; UserWhereInput: UserWhereInput; @@ -4503,7 +4550,6 @@ export type QueryResolvers>>, ParentType, ContextType, RequireFields>; getPlugins?: Resolver>>, ParentType, ContextType>; getUserTag?: Resolver, ParentType, ContextType, RequireFields>; - getUserTagAncestors?: Resolver>>, ParentType, ContextType, RequireFields>; getVenueByOrgId?: Resolver>>, ParentType, ContextType, RequireFields>; getlanguage?: Resolver>>, ParentType, ContextType, RequireFields>; hasSubmittedFeedback?: Resolver, ParentType, ContextType, RequireFields>; @@ -4674,6 +4720,7 @@ export type UserPhoneResolvers = { _id?: Resolver; + ancestorTags?: Resolver>>, ParentType, ContextType>; childTags?: Resolver, ParentType, ContextType, Partial>; name?: Resolver; organization?: Resolver, ParentType, ContextType>; diff --git a/src/utilities/userTagsPaginationUtils/getUserTagGraphQLConnectionFilter.ts b/src/utilities/userTagsPaginationUtils/getUserTagGraphQLConnectionFilter.ts new file mode 100644 index 0000000000..e7f6d88a2a --- /dev/null +++ b/src/utilities/userTagsPaginationUtils/getUserTagGraphQLConnectionFilter.ts @@ -0,0 +1,67 @@ +import type { GraphQLConnectionTraversalDirection } from "../graphQLConnection"; +import type { + ParseSortedByResult, + ParseUserTagWhereResult, +} from "../userTagsPaginationUtils"; + +/** + * This is typescript type of the object returned from function `getUserTagGraphQLConnectionFilter`. + */ +type BaseUserTagGraphQLConnectionFilter = { + name: { + $regex: RegExp; + }; +}; + +type UserTagGraphQLConnectionFilter = BaseUserTagGraphQLConnectionFilter & + ( + | { + _id?: { + $lt: string; + }; + } + | { + _id?: { + $gt: string; + }; + } + ); +/** + * This function is used to get an object containing filtering logic. + */ +export function getUserTagGraphQLConnectionFilter({ + cursor, + direction, + sortById, + nameStartsWith, +}: ParseSortedByResult & + ParseUserTagWhereResult & { + cursor: string | null; + direction: GraphQLConnectionTraversalDirection; + }): UserTagGraphQLConnectionFilter { + const filter = {} as UserTagGraphQLConnectionFilter; + + filter.name = { + $regex: new RegExp( + `^${nameStartsWith.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, + "i", + ), + }; + + if (cursor !== null) { + filter._id = getCursorFilter(cursor, sortById, direction); + } + + return filter; +} + +function getCursorFilter( + cursor: string, + sortById: "ASCENDING" | "DESCENDING", + direction: GraphQLConnectionTraversalDirection, +): { $lt: string } | { $gt: string } { + if (sortById === "ASCENDING") { + return direction === "BACKWARD" ? { $lt: cursor } : { $gt: cursor }; + } + return direction === "BACKWARD" ? { $gt: cursor } : { $lt: cursor }; +} diff --git a/src/utilities/userTagsPaginationUtils/getUserTagGraphQLConnectionSort.ts b/src/utilities/userTagsPaginationUtils/getUserTagGraphQLConnectionSort.ts new file mode 100644 index 0000000000..a5082f3d88 --- /dev/null +++ b/src/utilities/userTagsPaginationUtils/getUserTagGraphQLConnectionSort.ts @@ -0,0 +1,45 @@ +import type { GraphQLConnectionTraversalDirection } from "../graphQLConnection"; +import type { ParseSortedByResult } from "../userTagsPaginationUtils"; + +/** + *This is typescript type of the object returned from `getUserTagGraphQLConnectionSort` function. + */ +type UserTagGraphQLConnectionSort = + | { + _id: 1; + } + | { + _id: -1; + }; + +/** + * This function is used to get an object containing sorting logic.a + */ +export function getUserTagGraphQLConnectionSort({ + direction, + sortById, +}: ParseSortedByResult & { + direction: GraphQLConnectionTraversalDirection; +}): UserTagGraphQLConnectionSort { + if (sortById === "ASCENDING") { + if (direction === "BACKWARD") { + return { + _id: -1, + }; + } else { + return { + _id: 1, + }; + } + } else { + if (direction === "BACKWARD") { + return { + _id: 1, + }; + } else { + return { + _id: -1, + }; + } + } +} diff --git a/src/utilities/userTagsPaginationUtils/getUserTagMemberGraphQLConnectionFilter.ts b/src/utilities/userTagsPaginationUtils/getUserTagMemberGraphQLConnectionFilter.ts new file mode 100644 index 0000000000..3d00232d4e --- /dev/null +++ b/src/utilities/userTagsPaginationUtils/getUserTagMemberGraphQLConnectionFilter.ts @@ -0,0 +1,81 @@ +import { Types } from "mongoose"; +import type { GraphQLConnectionTraversalDirection } from "../graphQLConnection"; +import type { + ParseSortedByResult, + ParseUserTagMemberWhereResult, +} from "../userTagsPaginationUtils"; + +/** + * This is typescript type of the object returned from function `getUserTagMemberGraphQLConnectionFilter`. + */ +type BaseUserTagMemberGraphQLConnectionFilter = { + firstName: { + $regex: RegExp; + }; + lastName: { + $regex: RegExp; + }; +}; + +type UserTagMemberGraphQLConnectionFilter = + BaseUserTagMemberGraphQLConnectionFilter & + ( + | { + _id?: { + $lt: Types.ObjectId; + }; + } + | { + _id?: { + $gt: Types.ObjectId; + }; + } + ); + +/** + * This function is used to get an object containing filtering logic. + */ +export function getUserTagMemberGraphQLConnectionFilter({ + cursor, + direction, + sortById, + firstNameStartsWith, + lastNameStartsWith, +}: ParseSortedByResult & + ParseUserTagMemberWhereResult & { + cursor: string | null; + direction: GraphQLConnectionTraversalDirection; + }): UserTagMemberGraphQLConnectionFilter { + const filter = {} as UserTagMemberGraphQLConnectionFilter; + + filter.firstName = { + $regex: new RegExp( + `^${firstNameStartsWith.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, + "i", + ), + }; + filter.lastName = { + $regex: new RegExp( + `^${lastNameStartsWith.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, + "i", + ), + }; + + if (cursor !== null) { + filter._id = getCursorFilter(cursor, sortById, direction); + } + + return filter; +} + +function getCursorFilter( + cursor: string, + sortById: "ASCENDING" | "DESCENDING", + direction: GraphQLConnectionTraversalDirection, +): { $lt: Types.ObjectId } | { $gt: Types.ObjectId } { + const cursorId = new Types.ObjectId(cursor); + if (sortById === "ASCENDING") { + return direction === "BACKWARD" ? { $lt: cursorId } : { $gt: cursorId }; + } + return direction === "BACKWARD" ? { $gt: cursorId } : { $lt: cursorId }; +} diff --git a/src/utilities/userTagsPaginationUtils/index.ts b/src/utilities/userTagsPaginationUtils/index.ts new file mode 100644 index 0000000000..40b6dde26a --- /dev/null +++ b/src/utilities/userTagsPaginationUtils/index.ts @@ -0,0 +1,6 @@ +export * from "./parseUserTagSortedBy"; +export * from "./parseUserTagWhere"; +export * from "./parseUserTagMemberWhere"; +export * from "./getUserTagGraphQLConnectionSort"; +export * from "./getUserTagGraphQLConnectionFilter"; +export * from "./getUserTagMemberGraphQLConnectionFilter"; diff --git a/src/utilities/userTagsPaginationUtils/parseUserTagMemberWhere.ts b/src/utilities/userTagsPaginationUtils/parseUserTagMemberWhere.ts new file mode 100644 index 0000000000..636cb03087 --- /dev/null +++ b/src/utilities/userTagsPaginationUtils/parseUserTagMemberWhere.ts @@ -0,0 +1,73 @@ +import type { UserTagUsersAssignedToWhereInput } from "../../types/generatedGraphQLTypes"; +import type { + DefaultGraphQLArgumentError, + ParseGraphQLConnectionWhereResult, +} from "../graphQLConnection"; + +/** + * Type of the where object returned if the parsing is successful + */ +export type ParseUserTagMemberWhereResult = { + firstNameStartsWith: string; + lastNameStartsWith: string; +}; + +/** + * Function to parse the args.where for UserTag member assignment queries + */ +export function parseUserTagMemberWhere( + where: UserTagUsersAssignedToWhereInput | null | undefined, +): ParseGraphQLConnectionWhereResult { + const errors: DefaultGraphQLArgumentError[] = []; + + if (!where) { + return { + isSuccessful: true, + parsedWhere: { + firstNameStartsWith: "", + lastNameStartsWith: "", + }, + }; + } + + if (!where.firstName && !where.lastName) { + errors.push({ + message: `At least one of firstName or lastName should be provided`, + path: ["where"], + }); + + return { + isSuccessful: false, + errors, + }; + } + + if (where.firstName && typeof where.firstName.starts_with !== "string") { + errors.push({ + message: "Invalid firstName provided. It must be a string.", + path: ["where", "firstName", "starts_with"], + }); + } + + if (where.lastName && typeof where.lastName.starts_with !== "string") { + errors.push({ + message: "Invalid lastName provided. It must be a string.", + path: ["where", "lastName", "starts_with"], + }); + } + + if (errors.length > 0) { + return { + isSuccessful: false, + errors, + }; + } + + return { + isSuccessful: true, + parsedWhere: { + firstNameStartsWith: where.firstName?.starts_with.trim() ?? "", + lastNameStartsWith: where.lastName?.starts_with.trim() ?? "", + }, + }; +} diff --git a/src/utilities/userTagsPaginationUtils/parseUserTagSortedBy.ts b/src/utilities/userTagsPaginationUtils/parseUserTagSortedBy.ts new file mode 100644 index 0000000000..825fca40b6 --- /dev/null +++ b/src/utilities/userTagsPaginationUtils/parseUserTagSortedBy.ts @@ -0,0 +1,50 @@ +import type { + SortedByOrder, + UserTagSortedByInput, +} from "../../types/generatedGraphQLTypes"; +import type { + DefaultGraphQLArgumentError, + ParseGraphQLConnectionSortedByResult, +} from "../graphQLConnection"; + +/** + * type of the sort object returned if the parsing is successful + */ +export type ParseSortedByResult = { + sortById: SortedByOrder; +}; + +/** + * function to parse the args.sortedBy for UserTag queries + */ +export function parseUserTagSortedBy( + sortedBy: UserTagSortedByInput | null | undefined, +): ParseGraphQLConnectionSortedByResult { + const errors: DefaultGraphQLArgumentError[] = []; + + if (!sortedBy) { + return { + isSuccessful: true, + parsedSortedBy: { sortById: "DESCENDING" }, + }; + } + + if (sortedBy.id !== "DESCENDING" && sortedBy.id !== "ASCENDING") { + errors.push({ + message: + "Invalid sortedById provided. It must be a of type SortedByOrder.", + path: ["sortedBy", "id"], + }); + return { + isSuccessful: false, + errors, + }; + } + + return { + isSuccessful: true, + parsedSortedBy: { + sortById: sortedBy.id, + }, + }; +} diff --git a/src/utilities/userTagsPaginationUtils/parseUserTagWhere.ts b/src/utilities/userTagsPaginationUtils/parseUserTagWhere.ts new file mode 100644 index 0000000000..2f0728363d --- /dev/null +++ b/src/utilities/userTagsPaginationUtils/parseUserTagWhere.ts @@ -0,0 +1,61 @@ +import type { UserTagWhereInput } from "../../types/generatedGraphQLTypes"; +import type { + DefaultGraphQLArgumentError, + ParseGraphQLConnectionWhereResult, +} from "../graphQLConnection"; + +/** + * type of the where object returned if the parsing is successful + */ +export type ParseUserTagWhereResult = { + nameStartsWith: string; +}; + +/** + * function to parse the args.where for UserTag queries + */ +export function parseUserTagWhere( + where: UserTagWhereInput | null | undefined, +): ParseGraphQLConnectionWhereResult { + const errors: DefaultGraphQLArgumentError[] = []; + + if (!where) { + return { + isSuccessful: true, + parsedWhere: { + nameStartsWith: "", + }, + }; + } + + if (!where.name) { + errors.push({ + message: "Invalid where input, name should be provided.", + path: ["where"], + }); + + return { + isSuccessful: false, + errors, + }; + } + + if (typeof where.name.starts_with !== "string") { + errors.push({ + message: "Invalid name provided. It must be a string.", + path: ["where", "name"], + }); + + return { + isSuccessful: false, + errors, + }; + } + + return { + isSuccessful: true, + parsedWhere: { + nameStartsWith: where.name.starts_with.trim(), + }, + }; +} diff --git a/tests/helpers/tags.ts b/tests/helpers/tags.ts index ddbf70cc8a..06d3f49e57 100644 --- a/tests/helpers/tags.ts +++ b/tests/helpers/tags.ts @@ -79,6 +79,7 @@ export const createAndAssignUsersToTag = async ( await TagUser.create({ userId: user?._id, tagId: tag?._id, + organizationId: tag?.organizationId, }); testUsers.push(user); } @@ -93,6 +94,7 @@ export const createTagsAndAssignToUser = async ( await TagUser.create({ userId: testUser?._id, tagId: testTag?._id, + organizationId: testTag?.organizationId, }); const tags: TestUserTagType[] = [testTag]; @@ -108,6 +110,7 @@ export const createTagsAndAssignToUser = async ( await TagUser.create({ tagId: newTag?._id, userId: testUser?._id, + organizationId: newTag.organizationId, }); } diff --git a/tests/resolvers/Mutation/assignToUserTags.spec.ts b/tests/resolvers/Mutation/assignToUserTags.spec.ts index 9486c2db7e..4d9397d9fb 100644 --- a/tests/resolvers/Mutation/assignToUserTags.spec.ts +++ b/tests/resolvers/Mutation/assignToUserTags.spec.ts @@ -165,10 +165,12 @@ describe("resolvers -> Mutation -> assignToUserTags", () => { TagUser.create({ userId: randomUser2?._id, tagId: testTag2?._id, + organizationId: testTag2?.organizationId, }), TagUser.create({ userId: randomUser3?._id, tagId: testTag2?._id, + organizationId: testTag2?.organizationId, }), ]); @@ -234,10 +236,12 @@ describe("resolvers -> Mutation -> assignToUserTags", () => { TagUser.create({ userId: randomUser2?._id, tagId: newTestTag?._id, + organizationId: newTestTag?.organizationId, }), TagUser.create({ userId: randomUser3?._id, tagId: newTestTag?._id, + organizationId: newTestTag?.organizationId, }), ]); diff --git a/tests/resolvers/Mutation/removeFromUserTags.spec.ts b/tests/resolvers/Mutation/removeFromUserTags.spec.ts index 9ac18b68db..76069c8c38 100644 --- a/tests/resolvers/Mutation/removeFromUserTags.spec.ts +++ b/tests/resolvers/Mutation/removeFromUserTags.spec.ts @@ -168,10 +168,12 @@ describe("resolvers -> Mutation -> removeFromUserTags", () => { TagUser.create({ userId: randomUser2?._id, tagId: testTag2?._id, + organizationId: testTag2?.organizationId, }), TagUser.create({ userId: randomUser3?._id, tagId: testTag2?._id, + organizationId: testTag2?.organizationId, }), ]); @@ -276,10 +278,12 @@ describe("resolvers -> Mutation -> removeFromUserTags", () => { TagUser.create({ userId: randomUser2?._id, tagId: newTestTag?._id, + organizationId: newTestTag?.organizationId, }), TagUser.create({ userId: randomUser3?._id, tagId: newTestTag?._id, + organizationId: newTestTag?.organizationId, }), ]); diff --git a/tests/resolvers/Mutation/removeUserTag.spec.ts b/tests/resolvers/Mutation/removeUserTag.spec.ts index cb9f4e815d..7fd2375a9a 100644 --- a/tests/resolvers/Mutation/removeUserTag.spec.ts +++ b/tests/resolvers/Mutation/removeUserTag.spec.ts @@ -49,14 +49,17 @@ beforeAll(async () => { { userId: testUser?._id, tagId: rootTag?._id, + organizationId: rootTag?.organizationId, }, { userId: testUser?._id, tagId: childTag1?._id, + organizationId: childTag1?.organizationId, }, { userId: testUser?._id, tagId: childTag2?._id, + organizationId: childTag2?.organizationId, }, ]); }); diff --git a/tests/resolvers/Mutation/unassignUserTag.spec.ts b/tests/resolvers/Mutation/unassignUserTag.spec.ts index 16ce061c8f..de0a4fd84d 100644 --- a/tests/resolvers/Mutation/unassignUserTag.spec.ts +++ b/tests/resolvers/Mutation/unassignUserTag.spec.ts @@ -228,6 +228,7 @@ describe("resolvers -> Mutation -> unassignUserTag", () => { // Assign the tag to the user await TagUser.create({ ...args.input, + organizationId: testTag?.organizationId, }); // Test the unassignUserTag resolver @@ -266,11 +267,13 @@ describe("resolvers -> Mutation -> unassignUserTag", () => { // Assign the parent and sub tag to the user await TagUser.create({ ...args.input, + organizationId: testTag?.organizationId, }); await TagUser.create({ ...args.input, tagId: testSubTag1 ? testSubTag1._id.toString() : "", + organizationId: testSubTag1?.organizationId, }); // Test the unassignUserTag resolver diff --git a/tests/resolvers/Organization/userTags.spec.ts b/tests/resolvers/Organization/userTags.spec.ts index d3d3e9b948..bc7409611a 100644 --- a/tests/resolvers/Organization/userTags.spec.ts +++ b/tests/resolvers/Organization/userTags.spec.ts @@ -12,17 +12,44 @@ import { import type { DefaultGraphQLArgumentError } from "../../../src/utilities/graphQLConnection"; import { connect, disconnect } from "../../helpers/db"; import type { TestUserTagType } from "../../helpers/tags"; -import { createRootTagsWithOrg } from "../../helpers/tags"; +import { + createRootTagsWithOrg, + createTwoLevelTagsWithOrg, +} from "../../helpers/tags"; import type { TestOrganizationType } from "../../helpers/userAndOrg"; let MONGOOSE_INSTANCE: typeof mongoose; let testUserTag1: TestUserTagType, testUserTag2: TestUserTagType; +let testRootTag: TestUserTagType, testSubTag: TestUserTagType; let testOrganization: TestOrganizationType; +let testOrganization2: TestOrganizationType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); [, testOrganization, [testUserTag1, testUserTag2]] = await createRootTagsWithOrg(2); + [, testOrganization2, [testRootTag, testSubTag]] = + await createTwoLevelTagsWithOrg(); + + testRootTag = await OrganizationTagUser.findOneAndUpdate( + { + _id: testRootTag?._id, + }, + { + name: "testRootTag", + }, + { new: true }, + ).lean(); + + testSubTag = await OrganizationTagUser.findOneAndUpdate( + { + _id: testSubTag?._id, + }, + { + name: "testSubTag", + }, + { new: true }, + ).lean(); }); afterAll(async () => { @@ -44,7 +71,7 @@ describe("userTags resolver", () => { } }); - it(`returns the expected connection object`, async () => { + it(`returns the expected connection object, i.e. all the root tags`, async () => { const parent = testOrganization?.toObject() as InterfaceOrganization; const connection = await userTagsResolver?.( @@ -85,6 +112,51 @@ describe("userTags resolver", () => { totalCount, }); }); + + it(`returns all the tags (including nested tags), if the where input is defined`, async () => { + const parent = testOrganization2?.toObject() as InterfaceOrganization; + + const connection = await userTagsResolver?.( + parent, + { + first: 2, + where: { name: { starts_with: "test" } }, + sortedBy: { id: "ASCENDING" }, + }, + {}, + ); + + const totalCount = await OrganizationTagUser.find({ + name: new RegExp("^test", "i"), + organizationId: testOrganization2?._id, + }).countDocuments(); + + expect(connection).toEqual({ + edges: [ + { + cursor: testRootTag?._id.toString(), + node: { + ...testRootTag, + _id: testRootTag?._id.toString(), + }, + }, + { + cursor: testSubTag?._id.toString(), + node: { + ...testSubTag, + _id: testSubTag?._id.toString(), + }, + }, + ], + pageInfo: { + endCursor: testSubTag?._id.toString(), + hasNextPage: false, + hasPreviousPage: false, + startCursor: testRootTag?._id.toString(), + }, + totalCount, + }); + }); }); describe("parseCursor function", () => { diff --git a/tests/resolvers/Query/getUserTag.spec.ts b/tests/resolvers/Query/getUserTag.spec.ts index f81f51d960..0e935a383a 100644 --- a/tests/resolvers/Query/getUserTag.spec.ts +++ b/tests/resolvers/Query/getUserTag.spec.ts @@ -22,7 +22,7 @@ afterAll(async () => { await disconnect(MONGOOSE_INSTANCE); }); -describe("resolvers -> Query -> getUserTagAncestors", () => { +describe("resolvers -> Query -> getUserTag", () => { it(`throws NotFoundError if no userTag exists with _id === args.id`, async () => { const { requestContext } = await import("../../../src/libraries"); @@ -49,8 +49,8 @@ describe("resolvers -> Query -> getUserTagAncestors", () => { id: testTag?._id.toString() ?? "", }; - const getUserTagAncestorsPayload = await getUserTagResolver?.({}, args, {}); + const getUserTagPayload = await getUserTagResolver?.({}, args, {}); - expect(getUserTagAncestorsPayload).toEqual(testTag); + expect(getUserTagPayload).toEqual(testTag); }); }); diff --git a/tests/resolvers/Query/getUserTagAncestors.spec.ts b/tests/resolvers/Query/getUserTagAncestors.spec.ts deleted file mode 100644 index 2a3ee00143..0000000000 --- a/tests/resolvers/Query/getUserTagAncestors.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import "dotenv/config"; -import type mongoose from "mongoose"; -import { Types } from "mongoose"; -import { connect, disconnect } from "../../helpers/db"; - -import { getUserTagAncestors as getUserTagAncestorsResolver } from "../../../src/resolvers/Query/getUserTagAncestors"; -import type { QueryGetUserTagAncestorsArgs } from "../../../src/types/generatedGraphQLTypes"; -import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; -import { - createTwoLevelTagsWithOrg, - type TestUserTagType, -} from "../../helpers/tags"; -import { TAG_NOT_FOUND } from "../../../src/constants"; - -let MONGOOSE_INSTANCE: typeof mongoose; - -let testTag: TestUserTagType; -let testSubTag1: TestUserTagType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - [, , [testTag, testSubTag1]] = await createTwoLevelTagsWithOrg(); -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> Query -> getUserTagAncestors", () => { - it(`throws NotFoundError if no userTag exists with _id === args.id`, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - - try { - const args: QueryGetUserTagAncestorsArgs = { - id: new Types.ObjectId().toString(), - }; - - await getUserTagAncestorsResolver?.({}, args, {}); - } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith(TAG_NOT_FOUND.MESSAGE); - expect((error as Error).message).toEqual( - `Translated ${TAG_NOT_FOUND.MESSAGE}`, - ); - } - }); - - it(`returns the list of all the ancestor tags for a tag with _id === args.id`, async () => { - const args: QueryGetUserTagAncestorsArgs = { - id: testSubTag1?._id.toString() ?? "", - }; - - const getUserTagAncestorsPayload = await getUserTagAncestorsResolver?.( - {}, - args, - {}, - ); - - expect(getUserTagAncestorsPayload).toEqual([testTag, testSubTag1]); - }); -}); diff --git a/tests/resolvers/User/tagsAssignedWith.spec.ts b/tests/resolvers/User/tagsAssignedWith.spec.ts new file mode 100644 index 0000000000..6c8fd975dc --- /dev/null +++ b/tests/resolvers/User/tagsAssignedWith.spec.ts @@ -0,0 +1,128 @@ +import "dotenv/config"; +import { + parseCursor, + tagsAssignedWith as tagsAssignedWithResolver, +} from "../../../src/resolvers/User/tagsAssignedWith"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { TestUserTagType } from "../../helpers/tags"; +import type { + TestOrganizationType, + TestUserType, +} from "../../helpers/userAndOrg"; +import { createTagsAndAssignToUser } from "../../helpers/tags"; +import { GraphQLError } from "graphql"; +import type { DefaultGraphQLArgumentError } from "../../../src/utilities/graphQLConnection"; +import { + type InterfaceUser, + TagUser, + OrganizationTagUser, +} from "../../../src/models"; +import { Types } from "mongoose"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testTag: TestUserTagType, + testUser: TestUserType, + testOrganization: TestOrganizationType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [testUser, testOrganization, [testTag]] = await createTagsAndAssignToUser(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("tagsAssignedWith resolver", () => { + it(`throws GraphQLError if invalid arguments are provided to the resolver`, async () => { + const parent = testUser as InterfaceUser; + try { + await tagsAssignedWithResolver?.(parent, {}, {}); + } catch (error) { + if (error instanceof GraphQLError) { + expect(error.extensions.code).toEqual("INVALID_ARGUMENTS"); + expect( + (error.extensions.errors as DefaultGraphQLArgumentError[]).length, + ).toBeGreaterThan(0); + } + } + }); + + it(`returns the expected connection object`, async () => { + const parent = testUser as InterfaceUser; + const connection = await tagsAssignedWithResolver?.( + parent, + { + first: 3, + organizationId: testOrganization?._id, + }, + {}, + ); + + const tagUser = await TagUser.findOne({ + tagId: testTag?._id, + userId: testUser?._id, + }); + + const tag = await OrganizationTagUser.findOne({ + _id: testTag?._id, + }); + + const totalCount = await TagUser.find({ + userId: testUser?._id, + }).countDocuments(); + + expect(connection).toEqual({ + edges: [ + { + cursor: tagUser?._id.toString(), + node: tag?.toObject(), + }, + ], + pageInfo: { + endCursor: tagUser?._id.toString(), + hasNextPage: false, + hasPreviousPage: false, + startCursor: tagUser?._id.toString(), + }, + totalCount, + }); + }); +}); + +describe("parseCursor function", () => { + it("returns failure state if argument cursorValue is an invalid cursor", async () => { + const result = await parseCursor({ + cursorName: "after", + cursorPath: ["after"], + cursorValue: new Types.ObjectId().toString(), + userId: testUser?._id.toString() as string, + }); + + expect(result.isSuccessful).toEqual(false); + if (result.isSuccessful === false) { + expect(result.errors.length).toBeGreaterThan(0); + } + }); + + it("returns success state if argument cursorValue is a valid cursor", async () => { + const tagUser = await TagUser.findOne({ + tagId: testTag?._id, + userId: testUser?._id, + }); + + const result = await parseCursor({ + cursorName: "after", + cursorPath: ["after"], + cursorValue: tagUser?._id.toString() as string, + userId: testUser?._id.toString() as string, + }); + + expect(result.isSuccessful).toEqual(true); + if (result.isSuccessful === true) { + expect(result.parsedCursor).toEqual(tagUser?._id.toString()); + } + }); +}); diff --git a/tests/resolvers/UserTag/ancestorTags.spec.ts b/tests/resolvers/UserTag/ancestorTags.spec.ts new file mode 100644 index 0000000000..dd06ea5279 --- /dev/null +++ b/tests/resolvers/UserTag/ancestorTags.spec.ts @@ -0,0 +1,50 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import type { InterfaceOrganizationTagUser } from "../../../src/models"; +import { OrganizationTagUser } from "../../../src/models"; +import { ancestorTags as ancestorTagsResolver } from "../../../src/resolvers/UserTag/ancestorTags"; +import { connect, disconnect } from "../../helpers/db"; +import type { TestUserTagType } from "../../helpers/tags"; +import { createTwoLevelTagsWithOrg } from "../../helpers/tags"; +import type { TestOrganizationType } from "../../helpers/userAndOrg"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testRootTag: TestUserTagType, + testSubTagLevel1: TestUserTagType, + testSubTagLevel2: TestUserTagType; +let testOrganization: TestOrganizationType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [, testOrganization, [testRootTag, testSubTagLevel1]] = + await createTwoLevelTagsWithOrg(); + + testSubTagLevel2 = await OrganizationTagUser.create({ + name: "testSubTagLevel2", + parentTagId: testSubTagLevel1?._id, + organizationId: testOrganization?._id, + }); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Tag -> ancestorTags", () => { + it(`returns an empty ancestorTags array for the root tag`, async () => { + const parent = testRootTag as InterfaceOrganizationTagUser; + + const payload = await ancestorTagsResolver?.(parent, {}, {}); + + expect(payload).toEqual([]); + }); + + it(`returns the correct ancestorTags array for a nested tag`, async () => { + const parent = testSubTagLevel2 as InterfaceOrganizationTagUser; + + const payload = await ancestorTagsResolver?.(parent, {}, {}); + + expect(payload).toEqual([testRootTag, testSubTagLevel1]); + }); +}); diff --git a/tests/resolvers/UserTag/usersAssignedTo.spec.ts b/tests/resolvers/UserTag/usersAssignedTo.spec.ts index 52c52469ee..0333954353 100644 --- a/tests/resolvers/UserTag/usersAssignedTo.spec.ts +++ b/tests/resolvers/UserTag/usersAssignedTo.spec.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ import "dotenv/config"; import { parseCursor, @@ -8,7 +7,11 @@ import { connect, disconnect } from "../../helpers/db"; import type mongoose from "mongoose"; import { beforeAll, afterAll, describe, it, expect } from "vitest"; import type { TestUserTagType } from "../../helpers/tags"; -import type { TestUserType } from "../../helpers/userAndOrg"; +import type { + TestOrganizationType, + TestUserType, +} from "../../helpers/userAndOrg"; +import { createTestUser } from "../../helpers/userAndOrg"; import { createTagsAndAssignToUser } from "../../helpers/tags"; import { GraphQLError } from "graphql"; import type { DefaultGraphQLArgumentError } from "../../../src/utilities/graphQLConnection"; @@ -20,11 +23,30 @@ import { import { Types } from "mongoose"; let MONGOOSE_INSTANCE: typeof mongoose; -let testTag: TestUserTagType, testUser: TestUserType; +let testTag: TestUserTagType, + testUser: TestUserType, + testOrganization: TestOrganizationType, + randomUser: TestUserType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); - [testUser, , [testTag]] = await createTagsAndAssignToUser(); + [testUser, testOrganization, [testTag]] = await createTagsAndAssignToUser(); + randomUser = await createTestUser(); + + await User.updateOne( + { + _id: randomUser?._id, + }, + { + joinedOrganizations: testOrganization?._id, + }, + ); + + await TagUser.create({ + tagId: testTag?._id, + userId: randomUser?._id, + organizationId: testTag?.organizationId, + }); }); afterAll(async () => { @@ -56,15 +78,26 @@ describe("usersAssignedTo resolver", () => { {}, ); - const tagUser = await TagUser.findOne({ + const tagUser1 = await TagUser.findOne({ tagId: testTag?._id, userId: testUser?._id, + organizationId: testTag?.organizationId, }); - const user = await User.findOne({ + const tagUser2 = await TagUser.findOne({ + tagId: testTag?._id, + userId: randomUser?._id, + organizationId: testTag?.organizationId, + }); + + const user1 = await User.findOne({ _id: testUser?._id, }); + const user2 = await User.findOne({ + _id: randomUser?._id, + }); + const totalCount = await TagUser.find({ tagId: testTag?._id, }).countDocuments(); @@ -72,15 +105,66 @@ describe("usersAssignedTo resolver", () => { expect(connection).toEqual({ edges: [ { - cursor: tagUser?._id.toString(), - node: user?.toObject(), + cursor: tagUser2?._id.toString(), + node: user2?.toObject(), + }, + { + cursor: tagUser1?._id.toString(), + node: user1?.toObject(), }, ], pageInfo: { - endCursor: tagUser?._id.toString(), + endCursor: tagUser1?._id.toString(), hasNextPage: false, hasPreviousPage: false, - startCursor: tagUser?._id.toString(), + startCursor: tagUser2?._id.toString(), + }, + totalCount, + }); + }); + + it(`returns the expected connection object, after a specified cursor`, async () => { + const tagUser1 = await TagUser.findOne({ + tagId: testTag?._id, + userId: testUser?._id, + }).lean(); + + const parent = testTag as InterfaceOrganizationTagUser; + const connection = await usersAssignedToResolver?.( + parent, + { + first: 1, + after: tagUser1?._id.toString(), + sortedBy: { id: "ASCENDING" }, + }, + {}, + ); + + const tagUser2 = await TagUser.findOne({ + tagId: testTag?._id, + userId: randomUser?._id, + }); + + const user2 = await User.findOne({ + _id: randomUser?._id, + }); + + const totalCount = await TagUser.find({ + tagId: testTag?._id, + }).countDocuments(); + + expect(connection).toEqual({ + edges: [ + { + cursor: tagUser2?._id.toString(), + node: user2?.toObject(), + }, + ], + pageInfo: { + endCursor: tagUser2?._id.toString(), + hasNextPage: false, + hasPreviousPage: true, + startCursor: tagUser2?._id.toString(), }, totalCount, }); diff --git a/tests/resolvers/UserTag/usersToAssignTo.spec.ts b/tests/resolvers/UserTag/usersToAssignTo.spec.ts index fdcc04ff2b..3b502b7db0 100644 --- a/tests/resolvers/UserTag/usersToAssignTo.spec.ts +++ b/tests/resolvers/UserTag/usersToAssignTo.spec.ts @@ -1,8 +1,6 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ import "dotenv/config"; import { parseCursor, - getGraphQLConnectionFilter, usersToAssignTo as usersToAssignToResolver, } from "../../../src/resolvers/UserTag/usersToAssignTo"; import { connect, disconnect } from "../../helpers/db"; @@ -116,44 +114,3 @@ describe("parseCursor function", () => { } }); }); - -describe("getGraphQLConnectionFilter function", () => { - it(`when argument cursor is non-null and argument direction corresponds to backward`, async () => { - const cursor = new Types.ObjectId().toString(); - - expect( - getGraphQLConnectionFilter({ - cursor, - direction: "BACKWARD", - }), - ).toEqual({ - _id: { - $gt: new Types.ObjectId(cursor), - }, - }); - }); - - it(`when argument cursor is non-null and argument direction corresponds to forward`, async () => { - const cursor = new Types.ObjectId().toString(); - - expect( - getGraphQLConnectionFilter({ - cursor, - direction: "FORWARD", - }), - ).toEqual({ - _id: { - $lt: new Types.ObjectId(cursor), - }, - }); - }); - - it(`when argument cursor is null`, async () => { - expect( - getGraphQLConnectionFilter({ - cursor: null, - direction: "BACKWARD", - }), - ).toEqual({}); - }); -}); diff --git a/tests/utilities/userTagsPaginationUtils.spec.ts b/tests/utilities/userTagsPaginationUtils.spec.ts new file mode 100644 index 0000000000..be910d02ba --- /dev/null +++ b/tests/utilities/userTagsPaginationUtils.spec.ts @@ -0,0 +1,350 @@ +import { describe, expect, it } from "vitest"; +import { + parseUserTagSortedBy, + parseUserTagMemberWhere, + parseUserTagWhere, + getUserTagGraphQLConnectionSort, + getUserTagMemberGraphQLConnectionFilter, + getUserTagGraphQLConnectionFilter, +} from "../../src/utilities/userTagsPaginationUtils"; +import type { SortedByOrder } from "../../src/types/generatedGraphQLTypes"; +import { Types } from "mongoose"; + +describe("parseUserTagWhere function", () => { + it("returns the failure state if name isn't provided", async () => { + const result = await parseUserTagWhere({}); + expect(result.isSuccessful).toEqual(false); + }); + + it("returns the failure state if provided name.starts_with isn't a string", async () => { + const result = await parseUserTagWhere({ + name: { + starts_with: Math.random() as unknown as string, + }, + }); + + expect(result.isSuccessful).toEqual(false); + }); + + it("returns the success state if where input is nullish", async () => { + const result = await parseUserTagWhere(undefined); + + expect(result.isSuccessful).toEqual(true); + }); + + it("returns the success state if provided name.starts_with is an empty string", async () => { + const result = await parseUserTagWhere({ + name: { + starts_with: "", + }, + }); + + expect(result.isSuccessful).toEqual(true); + }); +}); + +describe("parseUserTagMemberWhere function", () => { + it("returns the failure state if neither firstName nor lastName is provided", async () => { + const result = await parseUserTagMemberWhere({}); + + expect(result.isSuccessful).toEqual(false); + }); + + it("returns the failure state if firstName isn't a string", async () => { + const result = await parseUserTagMemberWhere({ + firstName: { starts_with: Math.random() as unknown as string }, + }); + + expect(result.isSuccessful).toEqual(false); + }); + + it("returns the failure state if lastName isn't a string", async () => { + const result = await parseUserTagMemberWhere({ + firstName: { starts_with: "firstName" }, + lastName: { starts_with: Math.random() as unknown as string }, + }); + + expect(result.isSuccessful).toEqual(false); + }); + + it("returns the success state if where input is nullish", async () => { + const result = await parseUserTagMemberWhere(undefined); + + expect(result.isSuccessful).toEqual(true); + }); + + it("returns the success state if provided firstName is provided and lastName isn't", async () => { + const result = await parseUserTagMemberWhere({ + firstName: { starts_with: "firstName" }, + }); + + expect(result.isSuccessful).toEqual(true); + }); + + it("returns the success state if provided lastName is provided and firstName isn't", async () => { + const result = await parseUserTagMemberWhere({ + lastName: { starts_with: "lastName" }, + }); + + expect(result.isSuccessful).toEqual(true); + }); + + it("returns the success state if provided names are non-empty and valid strings", async () => { + const result = await parseUserTagMemberWhere({ + firstName: { starts_with: "firstName" }, + lastName: { starts_with: "lastName" }, + }); + + expect(result.isSuccessful).toEqual(true); + }); +}); + +describe("parseUserTagSortedBy function", () => { + it("returns the failure state if provided sortedBy isn't of type SortedByOrder", async () => { + const result = await parseUserTagSortedBy({ + id: "" as unknown as SortedByOrder, + }); + + expect(result.isSuccessful).toEqual(false); + }); + + it("returns the success state if where input is nullish", async () => { + const result = await parseUserTagSortedBy(undefined); + + expect(result.isSuccessful).toEqual(true); + }); + + it("returns the success state if provided sort order is valid", async () => { + const result = await parseUserTagSortedBy({ + id: "ASCENDING", + }); + + expect(result.isSuccessful).toEqual(true); + }); +}); + +describe("getUserTagGraphQLConnectionFilter function", () => { + it(`when sort order is "ASCENDING" argument cursor is non-null and argument direction corresponds to backward`, async () => { + const cursor = "cursor"; + + expect( + getUserTagGraphQLConnectionFilter({ + cursor, + direction: "BACKWARD", + sortById: "ASCENDING", + nameStartsWith: "userName", + }), + ).toEqual({ + _id: { + $lt: cursor, + }, + name: { + $regex: /^userName/i, + }, + }); + }); + + it(`when sort order is "ASCENDING" argument cursor is non-null and argument direction corresponds to forward`, async () => { + const cursor = "cursor"; + + expect( + getUserTagGraphQLConnectionFilter({ + cursor, + direction: "FORWARD", + sortById: "ASCENDING", + nameStartsWith: "userName", + }), + ).toEqual({ + _id: { + $gt: cursor, + }, + name: { + $regex: /^userName/i, + }, + }); + }); + + it(`when sort order is "DESCENDING" argument cursor is non-null and argument direction corresponds to backward`, async () => { + const cursor = "cursor"; + + expect( + getUserTagGraphQLConnectionFilter({ + cursor, + direction: "BACKWARD", + sortById: "DESCENDING", + nameStartsWith: "userName", + }), + ).toEqual({ + _id: { + $gt: cursor, + }, + name: { + $regex: /^userName/i, + }, + }); + }); + + it(`when sort order is "DESCENDING" argument cursor is non-null and argument direction corresponds to forward`, async () => { + const cursor = "cursor"; + + expect( + getUserTagGraphQLConnectionFilter({ + cursor, + direction: "FORWARD", + sortById: "DESCENDING", + nameStartsWith: "userName", + }), + ).toEqual({ + _id: { + $lt: cursor, + }, + name: { + $regex: /^userName/i, + }, + }); + }); +}); + +describe("getUserTagMemberGraphQLConnectionFilter function", () => { + it(`when sort order is "ASCENDING" argument cursor is non-null and argument direction corresponds to backward`, async () => { + const cursor = new Types.ObjectId().toString(); + + expect( + getUserTagMemberGraphQLConnectionFilter({ + cursor, + direction: "BACKWARD", + sortById: "ASCENDING", + firstNameStartsWith: "firstName", + lastNameStartsWith: "lastName", + }), + ).toEqual({ + _id: { + $lt: new Types.ObjectId(cursor), + }, + firstName: { + $regex: /^firstName/i, + }, + lastName: { + $regex: /^lastName/i, + }, + }); + }); + + it(`when sort order is "ASCENDING" argument cursor is non-null and argument direction corresponds to forward`, async () => { + const cursor = new Types.ObjectId().toString(); + + expect( + getUserTagMemberGraphQLConnectionFilter({ + cursor, + direction: "FORWARD", + sortById: "ASCENDING", + firstNameStartsWith: "firstName", + lastNameStartsWith: "lastName", + }), + ).toEqual({ + _id: { + $gt: new Types.ObjectId(cursor), + }, + firstName: { + $regex: /^firstName/i, + }, + lastName: { + $regex: /^lastName/i, + }, + }); + }); + + it(`when sort order is "DESCENDING" argument cursor is non-null and argument direction corresponds to backward`, async () => { + const cursor = new Types.ObjectId().toString(); + + expect( + getUserTagMemberGraphQLConnectionFilter({ + cursor, + direction: "BACKWARD", + sortById: "DESCENDING", + firstNameStartsWith: "firstName", + lastNameStartsWith: "lastName", + }), + ).toEqual({ + _id: { + $gt: new Types.ObjectId(cursor), + }, + firstName: { + $regex: /^firstName/i, + }, + lastName: { + $regex: /^lastName/i, + }, + }); + }); + + it(`when sort order is "DESCENDING" argument cursor is non-null and argument direction corresponds to forward`, async () => { + const cursor = new Types.ObjectId().toString(); + + expect( + getUserTagMemberGraphQLConnectionFilter({ + cursor, + direction: "FORWARD", + sortById: "DESCENDING", + firstNameStartsWith: "firstName", + lastNameStartsWith: "lastName", + }), + ).toEqual({ + _id: { + $lt: new Types.ObjectId(cursor), + }, + firstName: { + $regex: /^firstName/i, + }, + lastName: { + $regex: /^lastName/i, + }, + }); + }); +}); + +describe("getUserTagGraphQLConnectionSort function", () => { + it(`when sort order is "ASCENDING" and argument direction corresponds to backward`, async () => { + expect( + getUserTagGraphQLConnectionSort({ + direction: "BACKWARD", + sortById: "ASCENDING", + }), + ).toEqual({ + _id: -1, + }); + }); + + it(`when sort order is "ASCENDING" and argument direction corresponds to forward`, async () => { + expect( + getUserTagGraphQLConnectionSort({ + direction: "FORWARD", + sortById: "ASCENDING", + }), + ).toEqual({ + _id: 1, + }); + }); + + it(`when sort order is "DESCENDING" and argument direction corresponds to backward`, async () => { + expect( + getUserTagGraphQLConnectionSort({ + direction: "BACKWARD", + sortById: "DESCENDING", + }), + ).toEqual({ + _id: 1, + }); + }); + + it(`when sort order is "DESCENDING" and argument direction corresponds to forward`, async () => { + expect( + getUserTagGraphQLConnectionSort({ + direction: "FORWARD", + sortById: "DESCENDING", + }), + ).toEqual({ + _id: -1, + }); + }); +}); From f4877b986932181336f42a7336754de05976cd97 Mon Sep 17 00:00:00 2001 From: Glen Dsouza Date: Sat, 2 Nov 2024 18:18:32 +0530 Subject: [PATCH 4/6] Add support for Volunteer Leaderboard, linking Volunteer/Groups/Membership & Integrated with Action Items (#2615) * restructure eventVolunteer & volunteerGroup Models * Support for Volunteer Membership * Add mark action item and add hours volunteered * fix testcases 75 * Add support for Volunteer leaderboard * Add tests for query resolvers 100 * Add tests for checks * Fix lints issues & add tests for helper funcs * Fix failing tests for removeOrganization & updateEventVolunteerGroup * fix failing test for actionItem * Add test coverage * Add test coverage for updateActionItem * coderabbit suggesstions * remove session changes * add support for filtering upcoming events & getVolunteerMembership related to a group * codeRabbit suggestions * Add inputs.ts in countlint exclusion * change allotedHours to allottedHours * coderabbit suggestions * coderabbit suggestion --- .github/workflows/pull-request.yml | 2 +- codegen.ts | 3 + schema.graphql | 134 ++++- src/constants.ts | 7 + src/models/ActionItem.ts | 28 +- src/models/Event.ts | 11 + src/models/EventVolunteer.ts | 95 ++-- src/models/EventVolunteerGroup.ts | 37 +- src/models/VolunteerMembership.ts | 94 ++++ src/models/index.ts | 1 + src/resolvers/Event/actionItems.ts | 2 +- src/resolvers/EventVolunteer/creator.ts | 20 - src/resolvers/EventVolunteer/event.ts | 20 - src/resolvers/EventVolunteer/group.ts | 20 - src/resolvers/EventVolunteer/index.ts | 12 - src/resolvers/EventVolunteer/user.ts | 22 - src/resolvers/EventVolunteerGroup/creator.ts | 22 - src/resolvers/EventVolunteerGroup/event.ts | 20 - src/resolvers/EventVolunteerGroup/index.ts | 10 - src/resolvers/EventVolunteerGroup/leader.ts | 24 - src/resolvers/Mutation/createActionItem.ts | 169 +++---- .../Mutation/createEventVolunteer.ts | 100 ++-- .../Mutation/createEventVolunteerGroup.ts | 128 +++-- .../Mutation/createVolunteerMembership.ts | 81 +++ src/resolvers/Mutation/index.ts | 4 + .../Mutation/removeEventVolunteer.ts | 91 +--- .../Mutation/removeEventVolunteerGroup.ts | 94 ++-- src/resolvers/Mutation/updateActionItem.ts | 282 +++++++---- .../Mutation/updateEventVolunteer.ts | 82 +-- .../Mutation/updateEventVolunteerGroup.ts | 64 +-- .../Mutation/updateVolunteerMembership.ts | 137 +++++ .../Query/actionItemsByOrganization.ts | 30 +- src/resolvers/Query/actionItemsByUser.ts | 109 ++++ src/resolvers/Query/eventVolunteersByEvent.ts | 20 - .../Query/eventsByOrganizationConnection.ts | 19 +- .../Query/getEventVolunteerGroups.ts | 123 ++++- src/resolvers/Query/getEventVolunteers.ts | 61 +++ src/resolvers/Query/getVolunteerMembership.ts | 129 +++++ src/resolvers/Query/getVolunteerRanks.ts | 146 ++++++ .../Query/helperFunctions/getSort.ts | 16 + .../Query/helperFunctions/getWhere.ts | 17 +- src/resolvers/Query/index.ts | 8 + src/resolvers/index.ts | 2 - src/typeDefs/enums.ts | 16 + src/typeDefs/inputs.ts | 60 ++- src/typeDefs/mutations.ts | 9 +- src/typeDefs/queries.ts | 25 +- src/typeDefs/types.ts | 51 +- src/types/generatedGraphQLTypes.ts | 254 ++++++++-- src/utilities/adminCheck.ts | 19 +- src/utilities/checks.ts | 153 ++++++ tests/helpers/actionItem.ts | 5 + tests/helpers/events.ts | 18 +- tests/helpers/volunteers.ts | 299 +++++++++++ tests/resolvers/Event/actionItems.spec.ts | 2 +- .../resolvers/EventVolunteer/creator.spec.ts | 46 -- tests/resolvers/EventVolunteer/event.spec.ts | 38 -- tests/resolvers/EventVolunteer/group.spec.ts | 72 --- tests/resolvers/EventVolunteer/user.spec.ts | 52 -- .../EventVolunteerGroup/creator.spec.ts | 45 -- .../EventVolunteerGroup/event.spec.ts | 45 -- .../EventVolunteerGroup/leader.spec.ts | 45 -- .../Mutation/createActionItem.spec.ts | 414 ++++++---------- .../Mutation/createEventVolunteer.spec.ts | 28 +- .../createEventVolunteerGroup.spec.ts | 14 +- .../createVolunteerMembership.spec.ts | 165 +++++++ .../Mutation/removeEventVolunteer.spec.ts | 134 ++--- .../removeEventVolunteerGroup.spec.ts | 104 ++-- .../Mutation/removeOrganization.spec.ts | 1 + .../Mutation/updateActionItem.spec.ts | 466 +++++++++++++++--- .../Mutation/updateEventVolunteer.spec.ts | 225 +++------ .../updateEventVolunteerGroup.spec.ts | 109 ++-- .../updateVolunteerMembership.spec.ts | 175 +++++++ .../Query/actionItemsByOrganization.spec.ts | 310 +++--------- .../resolvers/Query/actionItemsByUser.spec.ts | 98 ++++ .../Query/eventVolunteersByEvent.spec.ts | 40 -- .../eventsByOrganizationConnection.spec.ts | 55 ++- .../Query/getEventVolunteerGroups.spec.ts | 107 +++- .../Query/getEventVolunteers.spec.ts | 62 +++ .../Query/getVolunteerMembership.spec.ts | 170 +++++++ .../resolvers/Query/getVolunteerRanks.spec.ts | 100 ++++ .../Query/helperFunctions/getSort.spec.ts | 10 +- .../Query/helperFunctions/getWhere.spec.ts | 15 +- tests/utilities/adminCheck.spec.ts | 23 +- tests/utilities/checks.spec.ts | 156 ++++++ 85 files changed, 4498 insertions(+), 2233 deletions(-) create mode 100644 src/models/VolunteerMembership.ts delete mode 100644 src/resolvers/EventVolunteer/creator.ts delete mode 100644 src/resolvers/EventVolunteer/event.ts delete mode 100644 src/resolvers/EventVolunteer/group.ts delete mode 100644 src/resolvers/EventVolunteer/index.ts delete mode 100644 src/resolvers/EventVolunteer/user.ts delete mode 100644 src/resolvers/EventVolunteerGroup/creator.ts delete mode 100644 src/resolvers/EventVolunteerGroup/event.ts delete mode 100644 src/resolvers/EventVolunteerGroup/index.ts delete mode 100644 src/resolvers/EventVolunteerGroup/leader.ts create mode 100644 src/resolvers/Mutation/createVolunteerMembership.ts create mode 100644 src/resolvers/Mutation/updateVolunteerMembership.ts create mode 100644 src/resolvers/Query/actionItemsByUser.ts delete mode 100644 src/resolvers/Query/eventVolunteersByEvent.ts create mode 100644 src/resolvers/Query/getEventVolunteers.ts create mode 100644 src/resolvers/Query/getVolunteerMembership.ts create mode 100644 src/resolvers/Query/getVolunteerRanks.ts create mode 100644 src/utilities/checks.ts create mode 100644 tests/helpers/volunteers.ts delete mode 100644 tests/resolvers/EventVolunteer/creator.spec.ts delete mode 100644 tests/resolvers/EventVolunteer/event.spec.ts delete mode 100644 tests/resolvers/EventVolunteer/group.spec.ts delete mode 100644 tests/resolvers/EventVolunteer/user.spec.ts delete mode 100644 tests/resolvers/EventVolunteerGroup/creator.spec.ts delete mode 100644 tests/resolvers/EventVolunteerGroup/event.spec.ts delete mode 100644 tests/resolvers/EventVolunteerGroup/leader.spec.ts create mode 100644 tests/resolvers/Mutation/createVolunteerMembership.spec.ts create mode 100644 tests/resolvers/Mutation/updateVolunteerMembership.spec.ts create mode 100644 tests/resolvers/Query/actionItemsByUser.spec.ts delete mode 100644 tests/resolvers/Query/eventVolunteersByEvent.spec.ts create mode 100644 tests/resolvers/Query/getEventVolunteers.spec.ts create mode 100644 tests/resolvers/Query/getVolunteerMembership.spec.ts create mode 100644 tests/resolvers/Query/getVolunteerRanks.spec.ts create mode 100644 tests/utilities/checks.spec.ts diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 844778ab40..44b5939049 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -37,7 +37,7 @@ jobs: - name: Count number of lines run: | chmod +x ./.github/workflows/countline.py - ./.github/workflows/countline.py --lines 600 --exclude_files src/types/generatedGraphQLTypes.ts tests src/typeDefs/types.ts src/constants.ts + ./.github/workflows/countline.py --lines 600 --exclude_files src/types/generatedGraphQLTypes.ts tests src/typeDefs/types.ts src/constants.ts src/typeDefs/inputs.ts - name: Check for TSDoc comments run: npm run check-tsdoc # Run the TSDoc check script diff --git a/codegen.ts b/codegen.ts index bc996c81da..9a34f09b9b 100644 --- a/codegen.ts +++ b/codegen.ts @@ -98,6 +98,9 @@ const config: CodegenConfig = { User: "../models/User#InterfaceUser", Venue: "../models/Venue#InterfaceVenue", + + VolunteerMembership: + "../models/VolunteerMembership#InterfaceVolunteerMembership", }, useTypeImports: true, diff --git a/schema.graphql b/schema.graphql index 8fcaa7d503..9ce8443540 100644 --- a/schema.graphql +++ b/schema.graphql @@ -5,8 +5,11 @@ directive @role(requires: UserType) on FIELD_DEFINITION type ActionItem { _id: ID! actionItemCategory: ActionItemCategory - allotedHours: Float - assignee: User + allottedHours: Float + assignee: EventVolunteer + assigneeGroup: EventVolunteerGroup + assigneeType: String! + assigneeUser: User assigner: User assignmentDate: Date! completionDate: Date! @@ -41,6 +44,7 @@ input ActionItemWhereInput { categoryName: String event_id: ID is_completed: Boolean + orgId: ID } enum ActionItemsOrderByInput { @@ -310,8 +314,9 @@ interface ConnectionPageInfo { scalar CountryCode input CreateActionItemInput { - allotedHours: Float + allottedHours: Float assigneeId: ID! + assigneeType: String! dueDate: Date eventId: ID preCompletionNotes: String @@ -675,6 +680,8 @@ type Event { startTime: Time title: String! updatedAt: DateTime! + volunteerGroups: [EventVolunteerGroup] + volunteers: [EventVolunteer] } type EventAttendee { @@ -739,21 +746,25 @@ enum EventOrderByInput { type EventVolunteer { _id: ID! + assignments: [ActionItem] createdAt: DateTime! creator: User event: Event - group: EventVolunteerGroup - isAssigned: Boolean - isInvited: Boolean - response: String + groups: [EventVolunteerGroup] + hasAccepted: Boolean! + hoursHistory: [HoursHistory] + hoursVolunteered: Float! + isPublic: Boolean! updatedAt: DateTime! user: User! } type EventVolunteerGroup { _id: ID! + assignments: [ActionItem] createdAt: DateTime! creator: User + description: String event: Event leader: User! name: String @@ -763,20 +774,32 @@ type EventVolunteerGroup { } input EventVolunteerGroupInput { + description: String eventId: ID! - name: String + leaderId: ID! + name: String! + volunteerUserIds: [ID!]! volunteersRequired: Int } +enum EventVolunteerGroupOrderByInput { + assignments_ASC + assignments_DESC + volunteers_ASC + volunteers_DESC +} + input EventVolunteerGroupWhereInput { eventId: ID + leaderName: String name_contains: String - volunteerId: ID + orgId: ID + userId: ID } input EventVolunteerInput { eventId: ID! - groupId: ID! + groupId: ID userId: ID! } @@ -785,6 +808,19 @@ enum EventVolunteerResponse { YES } +input EventVolunteerWhereInput { + eventId: ID + groupId: ID + hasAccepted: Boolean + id: ID + name_contains: String +} + +enum EventVolunteersOrderByInput { + hoursVolunteered_ASC + hoursVolunteered_DESC +} + input EventWhereInput { description: String description_contains: String @@ -942,6 +978,11 @@ type Group { updatedAt: DateTime! } +type HoursHistory { + date: Date! + hours: Float! +} + type InvalidCursor implements FieldError { message: String! path: [String!]! @@ -1093,6 +1134,7 @@ type Mutation { createUserFamily(data: createUserFamilyInput!): UserFamily! createUserTag(input: CreateUserTagInput!): UserTag createVenue(data: VenueInput!): Venue + createVolunteerMembership(data: VolunteerMembershipInput!): VolunteerMembership! deleteAdvertisement(id: ID!): DeleteAdvertisementPayload deleteAgendaCategory(id: ID!): ID! deleteDonationById(id: ID!): DeletePayload! @@ -1157,7 +1199,7 @@ type Mutation { updateCommunity(data: UpdateCommunityInput!): Boolean! updateEvent(data: UpdateEventInput!, id: ID!, recurrenceRuleData: RecurrenceRuleInput, recurringEventUpdateType: RecurringEventMutationType): Event! updateEventVolunteer(data: UpdateEventVolunteerInput, id: ID!): EventVolunteer! - updateEventVolunteerGroup(data: UpdateEventVolunteerGroupInput, id: ID!): EventVolunteerGroup! + updateEventVolunteerGroup(data: UpdateEventVolunteerGroupInput!, id: ID!): EventVolunteerGroup! updateFund(data: UpdateFundInput!, id: ID!): Fund! updateFundraisingCampaign(data: UpdateFundCampaignInput!, id: ID!): FundraisingCampaign! updateFundraisingCampaignPledge(data: UpdateFundCampaignPledgeInput!, id: ID!): FundraisingCampaignPledge! @@ -1171,6 +1213,7 @@ type Mutation { updateUserProfile(data: UpdateUserInput, file: String): User! updateUserRoleInOrganization(organizationId: ID!, role: String!, userId: ID!): Organization! updateUserTag(input: UpdateUserTagInput!): UserTag + updateVolunteerMembership(id: ID!, status: String!): VolunteerMembership! } type Note { @@ -1462,6 +1505,7 @@ type Query { actionItemCategoriesByOrganization(orderBy: ActionItemsOrderByInput, organizationId: ID!, where: ActionItemCategoryWhereInput): [ActionItemCategory] actionItemsByEvent(eventId: ID!): [ActionItem] actionItemsByOrganization(eventId: ID, orderBy: ActionItemsOrderByInput, organizationId: ID!, where: ActionItemWhereInput): [ActionItem] + actionItemsByUser(orderBy: ActionItemsOrderByInput, userId: ID!, where: ActionItemWhereInput): [ActionItem] adminPlugin(orgId: ID!): [Plugin] advertisementsConnection(after: String, before: String, first: PositiveInt, last: PositiveInt): AdvertisementsConnection agendaCategory(id: ID!): AgendaCategory! @@ -1474,9 +1518,8 @@ type Query { customDataByOrganization(organizationId: ID!): [UserCustomData!]! customFieldsByOrganization(id: ID!): [OrganizationCustomField] event(id: ID!): Event - eventVolunteersByEvent(id: ID!): [EventVolunteer] eventsByOrganization(id: ID, orderBy: EventOrderByInput): [Event] - eventsByOrganizationConnection(first: Int, orderBy: EventOrderByInput, skip: Int, where: EventWhereInput): [Event!]! + eventsByOrganizationConnection(first: Int, orderBy: EventOrderByInput, skip: Int, upcomingOnly: Boolean, where: EventWhereInput): [Event!]! fundsByOrganization(orderBy: FundOrderByInput, organizationId: ID!, where: FundWhereInput): [Fund] getAgendaItem(id: ID!): AgendaItem getAgendaSection(id: ID!): AgendaSection @@ -1489,7 +1532,8 @@ type Query { getEventAttendee(eventId: ID!, userId: ID!): EventAttendee getEventAttendeesByEventId(eventId: ID!): [EventAttendee] getEventInvitesByUserId(userId: ID!): [EventAttendee!]! - getEventVolunteerGroups(where: EventVolunteerGroupWhereInput): [EventVolunteerGroup]! + getEventVolunteerGroups(orderBy: EventVolunteerGroupOrderByInput, where: EventVolunteerGroupWhereInput!): [EventVolunteerGroup]! + getEventVolunteers(orderBy: EventVolunteersOrderByInput, where: EventVolunteerWhereInput!): [EventVolunteer]! getFundById(id: ID!, orderBy: CampaignOrderByInput, where: CampaignWhereInput): Fund! getFundraisingCampaignPledgeById(id: ID!): FundraisingCampaignPledge! getFundraisingCampaigns(campaignOrderby: CampaignOrderByInput, pledgeOrderBy: PledgeOrderByInput, where: CampaignWhereInput): [FundraisingCampaign]! @@ -1498,6 +1542,8 @@ type Query { getPlugins: [Plugin] getUserTag(id: ID!): UserTag getVenueByOrgId(first: Int, orderBy: VenueOrderByInput, orgId: ID!, skip: Int, where: VenueWhereInput): [Venue] + getVolunteerMembership(orderBy: VolunteerMembershipOrderByInput, where: VolunteerMembershipWhereInput!): [VolunteerMembership]! + getVolunteerRanks(orgId: ID!, where: VolunteerRankWhereInput!): [VolunteerRank]! getlanguage(lang_code: String!): [Translation] hasSubmittedFeedback(eventId: ID!, userId: ID!): Boolean isSampleOrganization(id: ID!): Boolean! @@ -1643,8 +1689,9 @@ input UpdateActionItemCategoryInput { } input UpdateActionItemInput { - allotedHours: Float + allottedHours: Float assigneeId: ID + assigneeType: String completionDate: Date dueDate: Date isCompleted: Boolean @@ -1714,16 +1761,16 @@ input UpdateEventInput { } input UpdateEventVolunteerGroupInput { - eventId: ID + description: String + eventId: ID! name: String volunteersRequired: Int } input UpdateEventVolunteerInput { - eventId: ID - isAssigned: Boolean - isInvited: Boolean - response: EventVolunteerResponse + assignments: [ID] + hasAccepted: Boolean + isPublic: Boolean } input UpdateFundCampaignInput { @@ -2050,6 +2097,53 @@ input VenueWhereInput { name_starts_with: String } +type VolunteerMembership { + _id: ID! + createdAt: DateTime! + createdBy: User + event: Event! + group: EventVolunteerGroup + status: String! + updatedAt: DateTime! + updatedBy: User + volunteer: EventVolunteer! +} + +input VolunteerMembershipInput { + event: ID! + group: ID + status: String! + userId: ID! +} + +enum VolunteerMembershipOrderByInput { + createdAt_ASC + createdAt_DESC +} + +input VolunteerMembershipWhereInput { + eventId: ID + eventTitle: String + filter: String + groupId: ID + status: String + userId: ID + userName: String +} + +type VolunteerRank { + hoursVolunteered: Float! + rank: Int! + user: User! +} + +input VolunteerRankWhereInput { + limit: Int + nameContains: String + orderBy: String! + timeFrame: String! +} + enum WeekDays { FRIDAY MONDAY diff --git a/src/constants.ts b/src/constants.ts index 71a5d0eab3..992b471a65 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -668,6 +668,13 @@ export const EVENT_VOLUNTEER_INVITE_USER_MISTMATCH = Object.freeze({ PARAM: "eventVolunteers", }); +export const EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR = Object.freeze({ + DESC: "Volunteer membership not found", + CODE: "volunteerMembership.notFound", + MESSAGE: "volunteerMembership.notFound", + PARAM: "volunteerMemberships", +}); + export const USER_ALREADY_CHECKED_IN = Object.freeze({ MESSAGE: "The user has already been checked in for this event.", CODE: "user.alreadyCheckedIn", diff --git a/src/models/ActionItem.ts b/src/models/ActionItem.ts index fe99964a0a..94904a25a7 100644 --- a/src/models/ActionItem.ts +++ b/src/models/ActionItem.ts @@ -5,13 +5,18 @@ import type { InterfaceEvent } from "./Event"; import type { InterfaceActionItemCategory } from "./ActionItemCategory"; import { MILLISECONDS_IN_A_WEEK } from "../constants"; import type { InterfaceOrganization } from "./Organization"; +import type { InterfaceEventVolunteerGroup } from "./EventVolunteerGroup"; +import type { InterfaceEventVolunteer } from "./EventVolunteer"; /** * Interface representing a database document for ActionItem in MongoDB. */ export interface InterfaceActionItem { _id: Types.ObjectId; - assignee: PopulatedDoc; + assignee: PopulatedDoc; + assigneeGroup: PopulatedDoc; + assigneeUser: PopulatedDoc; + assigneeType: "EventVolunteer" | "EventVolunteerGroup" | "User"; assigner: PopulatedDoc; actionItemCategory: PopulatedDoc< InterfaceActionItemCategory & Document @@ -22,7 +27,7 @@ export interface InterfaceActionItem { dueDate: Date; completionDate: Date; isCompleted: boolean; - allotedHours: number | null; + allottedHours: number | null; organization: PopulatedDoc; event: PopulatedDoc; creator: PopulatedDoc; @@ -33,6 +38,9 @@ export interface InterfaceActionItem { /** * Defines the schema for the ActionItem document. * @param assignee - User to whom the ActionItem is assigned. + * @param assigneeGroup - Group to whom the ActionItem is assigned. + * @param assigneeUser - Organization User to whom the ActionItem is assigned. + * @param assigneeType - Type of assignee (User or Group). * @param assigner - User who assigned the ActionItem. * @param actionItemCategory - ActionItemCategory to which the ActionItem belongs. * @param preCompletionNotes - Notes recorded before completion. @@ -41,7 +49,7 @@ export interface InterfaceActionItem { * @param dueDate - Due date for the ActionItem. * @param completionDate - Date when the ActionItem was completed. * @param isCompleted - Flag indicating if the ActionItem is completed. - * @param allotedHours - Optional: Number of hours alloted for the ActionItem. + * @param allottedHours - Optional: Number of hours allotted for the ActionItem. * @param event - Optional: Event to which the ActionItem is related. * @param organization - Organization to which the ActionItem belongs. * @param creator - User who created the ActionItem. @@ -51,9 +59,21 @@ export interface InterfaceActionItem { const actionItemSchema = new Schema( { assignee: { + type: Schema.Types.ObjectId, + ref: "EventVolunteer", + }, + assigneeGroup: { + type: Schema.Types.ObjectId, + ref: "EventVolunteerGroup", + }, + assigneeUser: { type: Schema.Types.ObjectId, ref: "User", + }, + assigneeType: { + type: String, required: true, + enum: ["EventVolunteer", "EventVolunteerGroup", "User"], }, assigner: { type: Schema.Types.ObjectId, @@ -91,7 +111,7 @@ const actionItemSchema = new Schema( required: true, default: false, }, - allotedHours: { + allottedHours: { type: Number, }, organization: { diff --git a/src/models/Event.ts b/src/models/Event.ts index 2a30077aad..f853682271 100644 --- a/src/models/Event.ts +++ b/src/models/Event.ts @@ -6,6 +6,7 @@ import { createLoggingMiddleware } from "../libraries/dbLogger"; import type { InterfaceEventVolunteerGroup } from "./EventVolunteerGroup"; import type { InterfaceRecurrenceRule } from "./RecurrenceRule"; import type { InterfaceAgendaItem } from "./AgendaItem"; +import type { InterfaceEventVolunteer } from "./EventVolunteer"; /** * Represents a document for an event in the MongoDB database. @@ -37,6 +38,7 @@ export interface InterfaceEvent { startTime: string | undefined; title: string; updatedAt: Date; + volunteers: PopulatedDoc[]; volunteerGroups: PopulatedDoc[]; agendaItems: PopulatedDoc[]; } @@ -66,6 +68,7 @@ export interface InterfaceEvent { * @param admins - Array of admins for the event. * @param organization - Reference to the organization hosting the event. * @param volunteerGroups - Array of volunteer groups associated with the event. + * @param volunteers - Array of volunteers associated with the event. * @param createdAt - Timestamp of when the event was created. * @param updatedAt - Timestamp of when the event was last updated. */ @@ -178,6 +181,14 @@ const eventSchema = new Schema( ref: "Organization", required: true, }, + volunteers: [ + { + type: Schema.Types.ObjectId, + ref: "EventVolunteer", + required: true, + default: [], + }, + ], volunteerGroups: [ { type: Schema.Types.ObjectId, diff --git a/src/models/EventVolunteer.ts b/src/models/EventVolunteer.ts index 0600f77b4c..72ccc24d62 100644 --- a/src/models/EventVolunteer.ts +++ b/src/models/EventVolunteer.ts @@ -4,6 +4,7 @@ import type { InterfaceUser } from "./User"; import type { InterfaceEvent } from "./Event"; import { createLoggingMiddleware } from "../libraries/dbLogger"; import type { InterfaceEventVolunteerGroup } from "./EventVolunteerGroup"; +import type { InterfaceActionItem } from "./ActionItem"; /** * Represents a document for an event volunteer in the MongoDB database. @@ -11,60 +12,95 @@ import type { InterfaceEventVolunteerGroup } from "./EventVolunteerGroup"; */ export interface InterfaceEventVolunteer { _id: Types.ObjectId; + creator: PopulatedDoc; + event: PopulatedDoc; + groups: PopulatedDoc[]; + user: PopulatedDoc; + hasAccepted: boolean; + isPublic: boolean; + hoursVolunteered: number; + assignments: PopulatedDoc[]; + hoursHistory: { + hours: number; + date: Date; + }[]; createdAt: Date; - creatorId: PopulatedDoc; - eventId: PopulatedDoc; - groupId: PopulatedDoc; - isAssigned: boolean; - isInvited: boolean; - response: string; updatedAt: Date; - userId: PopulatedDoc; } /** * Mongoose schema definition for an event volunteer document. * This schema defines how the data will be stored in the MongoDB database. * - * @param creatorId - Reference to the user who created the event volunteer entry. - * @param eventId - Reference to the event for which the user volunteers. - * @param groupId - Reference to the volunteer group associated with the event. - * @param response - Response status of the volunteer ("YES", "NO", null). - * @param isAssigned - Indicates if the volunteer is assigned to a specific role. - * @param isInvited - Indicates if the volunteer has been invited to participate. - * @param userId - Reference to the user who is volunteering for the event. + * @param creator - Reference to the user who created the event volunteer entry. + * @param event - Reference to the event for which the user volunteers. + * @param groups - Reference to the volunteer groups associated with the event. + * @param user - Reference to the user who is volunteering for the event. + * @param hasAccepted - Indicates if the volunteer has accepted invite. + * @param isPublic - Indicates if the volunteer is public. + * @param hoursVolunteered - Total hours volunteered by the user. + * @param assignments - List of action items assigned to the volunteer. * @param createdAt - Timestamp of when the event volunteer document was created. * @param updatedAt - Timestamp of when the event volunteer document was last updated. */ const eventVolunteerSchema = new Schema( { - creatorId: { + creator: { type: Schema.Types.ObjectId, ref: "User", required: true, }, - eventId: { + event: { type: Schema.Types.ObjectId, ref: "Event", }, - groupId: { + groups: [ + { + type: Schema.Types.ObjectId, + ref: "EventVolunteerGroup", + default: [], + }, + ], + user: { type: Schema.Types.ObjectId, - ref: "EventVolunteerGroup", - }, - response: { - type: String, - enum: ["YES", "NO", null], + ref: "User", + required: true, }, - isAssigned: { + hasAccepted: { type: Boolean, + required: true, + default: false, }, - isInvited: { + isPublic: { type: Boolean, - }, - userId: { - type: Schema.Types.ObjectId, - ref: "User", required: true, + default: true, + }, + hoursVolunteered: { + type: Number, + default: 0, + }, + assignments: [ + { + type: Schema.Types.ObjectId, + ref: "ActionItem", + default: [], + }, + ], + hoursHistory: { + type: [ + { + hours: { + type: Number, + required: true, + }, + date: { + type: Date, + required: true, + }, + }, + ], + default: [], }, }, { @@ -72,6 +108,9 @@ const eventVolunteerSchema = new Schema( }, ); +// Add index on hourHistory.date +eventVolunteerSchema.index({ "hourHistory.date": 1 }); + // Apply logging middleware to the schema createLoggingMiddleware(eventVolunteerSchema, "EventVolunteer"); diff --git a/src/models/EventVolunteerGroup.ts b/src/models/EventVolunteerGroup.ts index 8f8fc5072e..4bd9f5f438 100644 --- a/src/models/EventVolunteerGroup.ts +++ b/src/models/EventVolunteerGroup.ts @@ -4,6 +4,7 @@ import type { InterfaceUser } from "./User"; import type { InterfaceEvent } from "./Event"; import { createLoggingMiddleware } from "../libraries/dbLogger"; import type { InterfaceEventVolunteer } from "./EventVolunteer"; +import type { InterfaceActionItem } from "./ActionItem"; /** * Represents a document for an event volunteer group in the MongoDB database. @@ -11,42 +12,46 @@ import type { InterfaceEventVolunteer } from "./EventVolunteer"; */ export interface InterfaceEventVolunteerGroup { _id: Types.ObjectId; - createdAt: Date; - creatorId: PopulatedDoc; - eventId: PopulatedDoc; - leaderId: PopulatedDoc; + creator: PopulatedDoc; + event: PopulatedDoc; + leader: PopulatedDoc; name: string; - updatedAt: Date; + description?: string; volunteers: PopulatedDoc[]; volunteersRequired?: number; + assignments: PopulatedDoc[]; + createdAt: Date; + updatedAt: Date; } /** * Mongoose schema definition for an event volunteer group document. * This schema defines how the data will be stored in the MongoDB database. * - * @param creatorId - Reference to the user who created the event volunteer group entry. - * @param eventId - Reference to the event for which the volunteer group is created. - * @param leaderId - Reference to the leader of the volunteer group. + * @param creator - Reference to the user who created the event volunteer group entry. + * @param event - Reference to the event for which the volunteer group is created. + * @param leader - Reference to the leader of the volunteer group. * @param name - Name of the volunteer group. + * @param description - Description of the volunteer group (optional). * @param volunteers - List of volunteers in the group. * @param volunteersRequired - Number of volunteers required for the group (optional). + * @param assignments - List of action items assigned to the volunteer group. * @param createdAt - Timestamp of when the event volunteer group document was created. * @param updatedAt - Timestamp of when the event volunteer group document was last updated. */ const eventVolunteerGroupSchema = new Schema( { - creatorId: { + creator: { type: Schema.Types.ObjectId, ref: "User", required: true, }, - eventId: { + event: { type: Schema.Types.ObjectId, ref: "Event", required: true, }, - leaderId: { + leader: { type: Schema.Types.ObjectId, ref: "User", required: true, @@ -55,6 +60,9 @@ const eventVolunteerGroupSchema = new Schema( type: String, required: true, }, + description: { + type: String, + }, volunteers: [ { type: Schema.Types.ObjectId, @@ -65,6 +73,13 @@ const eventVolunteerGroupSchema = new Schema( volunteersRequired: { type: Number, }, + assignments: [ + { + type: Schema.Types.ObjectId, + ref: "ActionItem", + default: [], + }, + ], }, { timestamps: true, // Automatically manage `createdAt` and `updatedAt` fields diff --git a/src/models/VolunteerMembership.ts b/src/models/VolunteerMembership.ts new file mode 100644 index 0000000000..3342129d10 --- /dev/null +++ b/src/models/VolunteerMembership.ts @@ -0,0 +1,94 @@ +import type { PopulatedDoc, Document, Model, Types } from "mongoose"; +import { Schema, model, models } from "mongoose"; +import type { InterfaceEvent } from "./Event"; +import type { InterfaceEventVolunteer } from "./EventVolunteer"; +import type { InterfaceEventVolunteerGroup } from "./EventVolunteerGroup"; +import { createLoggingMiddleware } from "../libraries/dbLogger"; +import type { InterfaceUser } from "./User"; + +/** + * Represents a document for a volunteer membership in the MongoDB database. + * This interface defines the structure and types of data that a volunteer membership document will hold. + */ +export interface InterfaceVolunteerMembership { + _id: Types.ObjectId; + volunteer: PopulatedDoc; + group: PopulatedDoc; + event: PopulatedDoc; + status: "invited" | "requested" | "accepted" | "rejected"; + createdBy: PopulatedDoc; + updatedBy: PopulatedDoc; + createdAt: Date; + updatedAt: Date; +} + +/** + * Mongoose schema definition for a volunteer group membership document. + * This schema defines how the data will be stored in the MongoDB database. + * + * @param volunteer - Reference to the event volunteer involved in the group membership. + * @param group - Reference to the event volunteer group. Absence denotes a request for individual volunteer request. + * @param event - Reference to the event that the group is part of. + * @param status - Current status of the membership (invited, requested, accepted, rejected). + * @param createdBy - Reference to the user who created the group membership document. + * @param updatedBy - Reference to the user who last updated the group membership document. + * @param createdAt - Timestamp of when the group membership document was created. + * @param updatedAt - Timestamp of when the group membership document was last updated. + */ +const volunteerMembershipSchema = new Schema( + { + volunteer: { + type: Schema.Types.ObjectId, + ref: "EventVolunteer", + required: true, + }, + group: { + type: Schema.Types.ObjectId, + ref: "EventVolunteerGroup", + }, + event: { + type: Schema.Types.ObjectId, + ref: "Event", + required: true, + }, + status: { + type: String, + enum: ["invited", "requested", "accepted", "rejected"], + required: true, + default: "invited", + }, + createdBy: { + type: Schema.Types.ObjectId, + ref: "User", + }, + updatedBy: { + type: Schema.Types.ObjectId, + ref: "User", + }, + }, + { + timestamps: true, // Automatically manage `createdAt` and `updatedAt` fields + }, +); + +// Enable logging on changes in VolunteerMembership collection +createLoggingMiddleware(volunteerMembershipSchema, "VolunteerMembership"); + +/** + * Creates a Mongoose model for the volunteer group membership schema. + * This function ensures that we don't create multiple models during testing, which can cause errors. + * + * @returns The VolunteerMembership model. + */ +const volunteerMembershipModel = (): Model => + model( + "VolunteerMembership", + volunteerMembershipSchema, + ); + +/** + * Export the VolunteerMembership model. + * This syntax ensures we don't get an OverwriteModelError while running tests. + */ +export const VolunteerMembership = (models.VolunteerMembership || + volunteerMembershipModel()) as ReturnType; diff --git a/src/models/index.ts b/src/models/index.ts index 1292a2ac41..2da9481412 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -33,6 +33,7 @@ export * from "./RecurrenceRule"; export * from "./SampleData"; export * from "./TagUser"; export * from "./Venue"; +export * from "./VolunteerMembership"; export * from "./User"; export * from "./Note"; export * from "./Chat"; diff --git a/src/resolvers/Event/actionItems.ts b/src/resolvers/Event/actionItems.ts index b40c8a703c..5099572fde 100644 --- a/src/resolvers/Event/actionItems.ts +++ b/src/resolvers/Event/actionItems.ts @@ -15,6 +15,6 @@ import type { EventResolvers } from "../../types/generatedGraphQLTypes"; */ export const actionItems: EventResolvers["actionItems"] = async (parent) => { return await ActionItem.find({ - eventId: parent._id, + event: parent._id, }).lean(); }; diff --git a/src/resolvers/EventVolunteer/creator.ts b/src/resolvers/EventVolunteer/creator.ts deleted file mode 100644 index 59d13f2b57..0000000000 --- a/src/resolvers/EventVolunteer/creator.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { User } from "../../models"; -import type { EventVolunteerResolvers } from "../../types/generatedGraphQLTypes"; - -/** - * Resolver function for the `creator` field of an `EventVolunteer`. - * - * This function retrieves the user who created a specific event volunteer. - * - * @param parent - The parent object representing the event volunteer. It contains information about the event volunteer, including the ID of the user who created it. - * @returns A promise that resolves to the user document found in the database. This document represents the user who created the event volunteer. - * - * @see User - The User model used to interact with the users collection in the database. - * @see EventVolunteerResolvers - The type definition for the resolvers of the EventVolunteer fields. - * - */ -export const creator: EventVolunteerResolvers["creator"] = async (parent) => { - return await User.findOne({ - _id: parent.creatorId, - }).lean(); -}; diff --git a/src/resolvers/EventVolunteer/event.ts b/src/resolvers/EventVolunteer/event.ts deleted file mode 100644 index 438754aa91..0000000000 --- a/src/resolvers/EventVolunteer/event.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Event } from "../../models"; -import type { EventVolunteerResolvers } from "../../types/generatedGraphQLTypes"; - -/** - * Resolver function for the `event` field of an `EventVolunteer`. - * - * This function retrieves the event associated with a specific event volunteer. - * - * @param parent - The parent object representing the event volunteer. It contains information about the event volunteer, including the ID of the event associated with it. - * @returns A promise that resolves to the event document found in the database. This document represents the event associated with the event volunteer. - * - * @see Event - The Event model used to interact with the events collection in the database. - * @see EventVolunteerResolvers - The type definition for the resolvers of the EventVolunteer fields. - * - */ -export const event: EventVolunteerResolvers["event"] = async (parent) => { - return await Event.findOne({ - _id: parent.eventId, - }).lean(); -}; diff --git a/src/resolvers/EventVolunteer/group.ts b/src/resolvers/EventVolunteer/group.ts deleted file mode 100644 index 4fa94b9ee6..0000000000 --- a/src/resolvers/EventVolunteer/group.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { EventVolunteerGroup } from "../../models"; -import type { EventVolunteerResolvers } from "../../types/generatedGraphQLTypes"; - -/** - * Resolver function for the `group` field of an `EventVolunteer`. - * - * This function retrieves the group associated with a specific event volunteer. - * - * @param parent - The parent object representing the event volunteer. It contains information about the event volunteer, including the ID of the group associated with it. - * @returns A promise that resolves to the group document found in the database. This document represents the group associated with the event volunteer. - * - * @see EventVolunteerGroup - The EventVolunteerGroup model used to interact with the event volunteer groups collection in the database. - * @see EventVolunteerResolvers - The type definition for the resolvers of the EventVolunteer fields. - * - */ -export const group: EventVolunteerResolvers["group"] = async (parent) => { - return await EventVolunteerGroup.findOne({ - _id: parent.groupId, - }).lean(); -}; diff --git a/src/resolvers/EventVolunteer/index.ts b/src/resolvers/EventVolunteer/index.ts deleted file mode 100644 index 108e57c712..0000000000 --- a/src/resolvers/EventVolunteer/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { EventVolunteerResolvers } from "../../types/generatedGraphQLTypes"; -import { event } from "./event"; -import { creator } from "./creator"; -import { user } from "./user"; -import { group } from "./group"; - -export const EventVolunteer: EventVolunteerResolvers = { - creator, - event, - group, - user, -}; diff --git a/src/resolvers/EventVolunteer/user.ts b/src/resolvers/EventVolunteer/user.ts deleted file mode 100644 index 5059bd1dcb..0000000000 --- a/src/resolvers/EventVolunteer/user.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { User } from "../../models"; -import type { EventVolunteerResolvers } from "../../types/generatedGraphQLTypes"; - -/** - * Resolver function for the `user` field of an `EventVolunteer`. - * - * This function retrieves the user who created a specific event volunteer. - * - * @param parent - The parent object representing the event volunteer. It contains information about the event volunteer, including the ID of the user who created it. - * @returns A promise that resolves to the user document found in the database. This document represents the user who created the event volunteer. - * - * @see User - The User model used to interact with the users collection in the database. - * @see EventVolunteerResolvers - The type definition for the resolvers of the EventVolunteer fields. - * - */ -export const user: EventVolunteerResolvers["user"] = async (parent) => { - const result = await User.findOne({ - _id: parent.userId, - }).lean(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return result!; -}; diff --git a/src/resolvers/EventVolunteerGroup/creator.ts b/src/resolvers/EventVolunteerGroup/creator.ts deleted file mode 100644 index 7924de2115..0000000000 --- a/src/resolvers/EventVolunteerGroup/creator.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { User } from "../../models"; -import type { EventVolunteerGroupResolvers } from "../../types/generatedGraphQLTypes"; - -/** - * Resolver function for the `creator` field of an `EventVolunteerGroup`. - * - * This function retrieves the user who created a specific event volunteer group. - * - * @param parent - The parent object representing the event volunteer group. It contains information about the event volunteer group, including the ID of the user who created it. - * @returns A promise that resolves to the user document found in the database. This document represents the user who created the event volunteer group. - * - * @see User - The User model used to interact with the users collection in the database. - * @see EventVolunteerGroupResolvers - The type definition for the resolvers of the EventVolunteerGroup fields. - * - */ -export const creator: EventVolunteerGroupResolvers["creator"] = async ( - parent, -) => { - return await User.findOne({ - _id: parent.creatorId, - }).lean(); -}; diff --git a/src/resolvers/EventVolunteerGroup/event.ts b/src/resolvers/EventVolunteerGroup/event.ts deleted file mode 100644 index b758191d0a..0000000000 --- a/src/resolvers/EventVolunteerGroup/event.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Event } from "../../models"; -import type { EventVolunteerGroupResolvers } from "../../types/generatedGraphQLTypes"; - -/** - * Resolver function for the `event` field of an `EventVolunteerGroup`. - * - * This function retrieves the event associated with a specific event volunteer group. - * - * @param parent - The parent object representing the event volunteer group. It contains information about the event volunteer group, including the ID of the event associated with it. - * @returns A promise that resolves to the event document found in the database. This document represents the event associated with the event volunteer group. - * - * @see Event - The Event model used to interact with the events collection in the database. - * @see EventVolunteerGroupResolvers - The type definition for the resolvers of the EventVolunteerGroup fields. - * - */ -export const event: EventVolunteerGroupResolvers["event"] = async (parent) => { - return await Event.findOne({ - _id: parent.eventId, - }).lean(); -}; diff --git a/src/resolvers/EventVolunteerGroup/index.ts b/src/resolvers/EventVolunteerGroup/index.ts deleted file mode 100644 index 9c34f29560..0000000000 --- a/src/resolvers/EventVolunteerGroup/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { EventVolunteerGroupResolvers } from "../../types/generatedGraphQLTypes"; -import { leader } from "./leader"; -import { creator } from "./creator"; -import { event } from "./event"; - -export const EventVolunteerGroup: EventVolunteerGroupResolvers = { - creator, - leader, - event, -}; diff --git a/src/resolvers/EventVolunteerGroup/leader.ts b/src/resolvers/EventVolunteerGroup/leader.ts deleted file mode 100644 index 93a47b3eab..0000000000 --- a/src/resolvers/EventVolunteerGroup/leader.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { User } from "../../models"; -import type { InterfaceUser } from "../../models"; -import type { EventVolunteerGroupResolvers } from "../../types/generatedGraphQLTypes"; - -/** - * Resolver function for the `leader` field of an `EventVolunteerGroup`. - * - * This function retrieves the user who is the leader of a specific event volunteer group. - * - * @param parent - The parent object representing the event volunteer group. It contains information about the event volunteer group, including the ID of the user who is the leader. - * @returns A promise that resolves to the user document found in the database. This document represents the user who is the leader of the event volunteer group. - * - * @see User - The User model used to interact with the users collection in the database. - * @see EventVolunteerGroupResolvers - The type definition for the resolvers of the EventVolunteerGroup fields. - * - */ -export const leader: EventVolunteerGroupResolvers["leader"] = async ( - parent, -) => { - const groupLeader = await User.findOne({ - _id: parent.leaderId, - }).lean(); - return groupLeader as InterfaceUser; -}; diff --git a/src/resolvers/Mutation/createActionItem.ts b/src/resolvers/Mutation/createActionItem.ts index c38357a282..718bf1caba 100644 --- a/src/resolvers/Mutation/createActionItem.ts +++ b/src/resolvers/Mutation/createActionItem.ts @@ -3,31 +3,31 @@ import { ACTION_ITEM_CATEGORY_IS_DISABLED, ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR, EVENT_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, - USER_NOT_FOUND_ERROR, - USER_NOT_MEMBER_FOR_ORGANIZATION, } from "../../constants"; import { errors, requestContext } from "../../libraries"; import type { InterfaceActionItem, - InterfaceAppUserProfile, InterfaceEvent, - InterfaceUser, + InterfaceEventVolunteer, + InterfaceEventVolunteerGroup, } from "../../models"; import { ActionItem, ActionItemCategory, - AppUserProfile, Event, - User, + EventVolunteer, + EventVolunteerGroup, } from "../../models"; -import { cacheAppUserProfile } from "../../services/AppUserProfileCache/cacheAppUserProfile"; -import { findAppUserProfileCache } from "../../services/AppUserProfileCache/findAppUserProfileCache"; import { cacheEvents } from "../../services/EventCache/cacheEvents"; import { findEventsInCache } from "../../services/EventCache/findEventInCache"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { + checkAppUserProfileExists, + checkUserExists, +} from "../../utilities/checks"; /** * Creates a new action item and assigns it to a user. @@ -38,18 +38,18 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; * 2. Ensures that the current user has an associated app user profile. * 3. Checks if the assignee exists. * 4. Validates if the action item category exists and is not disabled. - * 5. Confirms that the assignee is a member of the organization associated with the action item category. - * 6. If the action item is related to an event, checks if the event exists and whether the current user is an admin of that event. - * 7. Verifies if the current user is an admin of the organization or a superadmin. + * 5. If the action item is related to an event, checks if the event exists and whether the current user is an admin of that event. + * 6. Verifies if the current user is an admin of the organization or a superadmin. * * @param _parent - The parent object for the mutation (not used in this function). * @param args - The arguments provided with the request, including: * - `data`: An object containing: * - `assigneeId`: The ID of the user to whom the action item is assigned. + * - `assigneeType`: The type of the assignee (EventVolunteer or EventVolunteerGroup). * - `preCompletionNotes`: Notes to be added before the action item is completed. * - `dueDate`: The due date for the action item. * - `eventId` (optional): The ID of the event associated with the action item. - * - `actionItemCategoryId`: The ID of the action item category. + * - `actionItemCategoryId`: The ID of the action item category. * @param context - The context of the entire application, including user information and other context-specific data. * * @returns A promise that resolves to the created action item object. @@ -60,62 +60,41 @@ export const createActionItem: MutationResolvers["createActionItem"] = async ( args, context, ): Promise => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); + const currentUser = await checkUserExists(context.userId); + const currentUserAppProfile = await checkAppUserProfileExists(currentUser); + + const { + assigneeId, + assigneeType, + preCompletionNotes, + allottedHours, + dueDate, + eventId, + } = args.data; + + let assignee: InterfaceEventVolunteer | InterfaceEventVolunteerGroup | null; + if (assigneeType === "EventVolunteer") { + assignee = await EventVolunteer.findById(assigneeId) + .populate("user") + .lean(); + if (!assignee) { + throw new errors.NotFoundError( + requestContext.translate(EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE), + EVENT_VOLUNTEER_NOT_FOUND_ERROR.CODE, + EVENT_VOLUNTEER_NOT_FOUND_ERROR.PARAM, + ); } - } - - // Checks whether currentUser with _id === context.userId exists. - if (currentUser === null) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, - ); - } - - let currentUserAppProfile: InterfaceAppUserProfile | null; - const appUserProfileFoundInCache = await findAppUserProfileCache([ - currentUser.appUserProfileId?.toString(), - ]); - currentUserAppProfile = appUserProfileFoundInCache[0]; - if (currentUserAppProfile === null) { - currentUserAppProfile = await AppUserProfile.findOne({ - userId: currentUser._id, - }).lean(); - if (currentUserAppProfile !== null) { - await cacheAppUserProfile([currentUserAppProfile]); + } else if (assigneeType === "EventVolunteerGroup") { + assignee = await EventVolunteerGroup.findById(assigneeId).lean(); + if (!assignee) { + throw new errors.NotFoundError( + requestContext.translate(EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE), + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.CODE, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.PARAM, + ); } } - if (!currentUserAppProfile) { - throw new errors.UnauthorizedError( - requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), - USER_NOT_AUTHORIZED_ERROR.CODE, - USER_NOT_AUTHORIZED_ERROR.PARAM, - ); - } - - const assignee = await User.findOne({ - _id: args.data.assigneeId, - }); - - // Checks whether the assignee exists. - if (assignee === null) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, - ); - } - const actionItemCategory = await ActionItemCategory.findOne({ _id: args.actionItemCategoryId, }).lean(); @@ -138,36 +117,17 @@ export const createActionItem: MutationResolvers["createActionItem"] = async ( ); } - let asigneeIsOrganizationMember = false; - asigneeIsOrganizationMember = assignee.joinedOrganizations.some( - (organizationId) => - organizationId === actionItemCategory.organizationId || - new mongoose.Types.ObjectId(organizationId.toString()).equals( - actionItemCategory.organizationId, - ), - ); - - // Checks if the asignee is a member of the organization - if (!asigneeIsOrganizationMember) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_MEMBER_FOR_ORGANIZATION.MESSAGE), - USER_NOT_MEMBER_FOR_ORGANIZATION.CODE, - USER_NOT_MEMBER_FOR_ORGANIZATION.PARAM, - ); - } - let currentUserIsEventAdmin = false; - - if (args.data.eventId) { + if (eventId) { let currEvent: InterfaceEvent | null; - const eventFoundInCache = await findEventsInCache([args.data.eventId]); + const eventFoundInCache = await findEventsInCache([eventId]); currEvent = eventFoundInCache[0]; if (eventFoundInCache[0] === null) { currEvent = await Event.findOne({ - _id: args.data.eventId, + _id: eventId, }).lean(); if (currEvent !== null) { @@ -203,6 +163,7 @@ export const createActionItem: MutationResolvers["createActionItem"] = async ( ); // Checks whether the currentUser is authorized for the operation. + /* c8 ignore start */ if ( currentUserIsEventAdmin === false && currentUserIsOrgAdmin === false && @@ -214,19 +175,41 @@ export const createActionItem: MutationResolvers["createActionItem"] = async ( USER_NOT_AUTHORIZED_ERROR.PARAM, ); } + /* c8 ignore stop */ // Creates and returns the new action item. const createActionItem = await ActionItem.create({ - assignee: args.data.assigneeId, + assignee: assigneeType === "EventVolunteer" ? assigneeId : undefined, + assigneeGroup: + assigneeType === "EventVolunteerGroup" ? assigneeId : undefined, + assigneeUser: assigneeType === "User" ? assigneeId : undefined, + assigneeType, assigner: context.userId, actionItemCategory: args.actionItemCategoryId, - preCompletionNotes: args.data.preCompletionNotes, - allotedHours: args.data.allotedHours, - dueDate: args.data.dueDate, - event: args.data.eventId, + preCompletionNotes, + allottedHours, + dueDate, + event: eventId, organization: actionItemCategory.organizationId, creator: context.userId, }); + if (assigneeType === "EventVolunteer") { + await EventVolunteer.findByIdAndUpdate(assigneeId, { + $addToSet: { assignments: createActionItem._id }, + }); + } else if (assigneeType === "EventVolunteerGroup") { + const newGrp = (await EventVolunteerGroup.findByIdAndUpdate( + assigneeId, + { $addToSet: { assignments: createActionItem._id } }, + { new: true }, + ).lean()) as InterfaceEventVolunteerGroup; + + await EventVolunteer.updateMany( + { _id: { $in: newGrp.volunteers } }, + { $addToSet: { assignments: createActionItem._id } }, + ); + } + return createActionItem.toObject(); }; diff --git a/src/resolvers/Mutation/createEventVolunteer.ts b/src/resolvers/Mutation/createEventVolunteer.ts index decaa931fc..987c88dfb2 100644 --- a/src/resolvers/Mutation/createEventVolunteer.ts +++ b/src/resolvers/Mutation/createEventVolunteer.ts @@ -1,29 +1,25 @@ import { EVENT_NOT_FOUND_ERROR, - EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, EVENT_VOLUNTEER_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, - USER_NOT_FOUND_ERROR, } from "../../constants"; import { errors, requestContext } from "../../libraries"; -import type { InterfaceUser } from "../../models"; -import { Event, EventVolunteerGroup, User } from "../../models"; +import { Event, User, VolunteerMembership } from "../../models"; import { EventVolunteer } from "../../models/EventVolunteer"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { adminCheck } from "../../utilities"; +import { checkUserExists } from "../../utilities/checks"; /** * Creates a new event volunteer entry. * * This function performs the following actions: - * 1. Verifies the existence of the current user. - * 2. Verifies the existence of the volunteer user. - * 3. Verifies the existence of the event. - * 4. Verifies the existence of the volunteer group. - * 5. Ensures that the current user is the leader of the volunteer group. - * 6. Creates a new event volunteer record. - * 7. Adds the newly created volunteer to the group's list of volunteers. + * 1. Validates the existence of the current user. + * 2. Checks if the specified user and event exist. + * 3. Verifies that the current user is an admin of the event. + * 4. Creates a new volunteer entry for the event. + * 5. Creates a volunteer membership record for the new volunteer. + * 6. Returns the created event volunteer record. * * @param _parent - The parent object for the mutation. This parameter is not used in this resolver. * @param args - The arguments for the mutation, including: @@ -38,25 +34,11 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; */ export const createEventVolunteer: MutationResolvers["createEventVolunteer"] = async (_parent, args, context) => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); - } - } - if (!currentUser) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, - ); - } - const volunteerUser = await User.findOne({ _id: args.data?.userId }).lean(); + const { eventId, userId } = args.data; + const currentUser = await checkUserExists(context.userId); + + // Check if the volunteer user exists + const volunteerUser = await User.findById(userId).lean(); if (!volunteerUser) { throw new errors.NotFoundError( requestContext.translate(EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE), @@ -64,7 +46,8 @@ export const createEventVolunteer: MutationResolvers["createEventVolunteer"] = EVENT_VOLUNTEER_NOT_FOUND_ERROR.PARAM, ); } - const event = await Event.findById(args.data.eventId); + // Check if the event exists + const event = await Event.findById(eventId).populate("organization").lean(); if (!event) { throw new errors.NotFoundError( requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE), @@ -72,18 +55,18 @@ export const createEventVolunteer: MutationResolvers["createEventVolunteer"] = EVENT_NOT_FOUND_ERROR.PARAM, ); } - const group = await EventVolunteerGroup.findOne({ - _id: args.data.groupId, - }).lean(); - if (!group) { - throw new errors.NotFoundError( - requestContext.translate(EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE), - EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.CODE, - EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.PARAM, - ); - } - if (group.leaderId.toString() !== currentUser._id.toString()) { + const userIsEventAdmin = event.admins.some( + (admin) => admin.toString() === currentUser?._id.toString(), + ); + + // Checks creator of the event or admin of the organization + const isAdmin = await adminCheck( + currentUser._id, + event.organization, + false, + ); + if (!isAdmin && !userIsEventAdmin) { throw new errors.UnauthorizedError( requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), USER_NOT_AUTHORIZED_ERROR.CODE, @@ -91,24 +74,21 @@ export const createEventVolunteer: MutationResolvers["createEventVolunteer"] = ); } + // create the volunteer const createdVolunteer = await EventVolunteer.create({ - userId: args.data.userId, - eventId: args.data.eventId, - groupId: args.data.groupId, - isAssigned: false, - isInvited: true, - creatorId: context.userId, + user: userId, + event: eventId, + creator: context.userId, + groups: [], + }); + + // create volunteer membership record + await VolunteerMembership.create({ + volunteer: createdVolunteer._id, + event: eventId, + status: "invited", + createdBy: context.userId, }); - await EventVolunteerGroup.findOneAndUpdate( - { - _id: args.data.groupId, - }, - { - $push: { - volunteers: createdVolunteer._id, - }, - }, - ); return createdVolunteer.toObject(); }; diff --git a/src/resolvers/Mutation/createEventVolunteerGroup.ts b/src/resolvers/Mutation/createEventVolunteerGroup.ts index 060a2dde49..d3c26adbd0 100644 --- a/src/resolvers/Mutation/createEventVolunteerGroup.ts +++ b/src/resolvers/Mutation/createEventVolunteerGroup.ts @@ -1,14 +1,17 @@ import { EVENT_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, - USER_NOT_FOUND_ERROR, } from "../../constants"; import { errors, requestContext } from "../../libraries"; -import type { InterfaceUser } from "../../models"; -import { Event, EventVolunteerGroup, User } from "../../models"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; +import { + Event, + EventVolunteer, + EventVolunteerGroup, + VolunteerMembership, +} from "../../models"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { adminCheck } from "../../utilities"; +import { checkUserExists } from "../../utilities/checks"; /** * Creates a new event volunteer group and associates it with an event. @@ -19,14 +22,19 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; * 2. Checks if the specified event exists. * 3. Verifies that the current user is an admin of the event. * 4. Creates a new volunteer group for the event. - * 5. Updates the event to include the newly created volunteer group. + * 5. Fetches or creates new volunteers for the group. + * 6. Creates volunteer group membership records for the new volunteers. + * 7. Updates the event to include the new volunteer group. * * @param _parent - The parent object, not used in this resolver. * @param args - The input arguments for the mutation, including: * - `data`: An object containing: - * - `eventId`: The ID of the event to associate the volunteer group with. - * - `name`: The name of the volunteer group. - * - `volunteersRequired`: The number of volunteers required for the group. + * - `eventId`: The ID of the event to associate the volunteer group with. + * - `name`: The name of the volunteer group. + * - `description`: A description of the volunteer group. + * - `leaderId`: The ID of the user who will lead the volunteer group. + * - `volunteerIds`: An array of user IDs for the volunteers in the group. + * - `volunteersRequired`: The number of volunteers required for the group. * @param context - The context object containing user information (context.userId). * * @returns A promise that resolves to the created event volunteer group object. @@ -35,25 +43,20 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; */ export const createEventVolunteerGroup: MutationResolvers["createEventVolunteerGroup"] = async (_parent, args, context) => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); - } - } - if (!currentUser) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, - ); - } - const event = await Event.findById(args.data.eventId); + const { + eventId, + name, + description, + leaderId, + volunteerUserIds, + volunteersRequired, + } = args.data; + // Validate the existence of the current user + const currentUser = await checkUserExists(context.userId); + + const event = await Event.findById(args.data.eventId) + .populate("organization") + .lean(); if (!event) { throw new errors.NotFoundError( requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE), @@ -66,7 +69,13 @@ export const createEventVolunteerGroup: MutationResolvers["createEventVolunteerG (admin) => admin.toString() === currentUser?._id.toString(), ); - if (!userIsEventAdmin) { + const isAdmin = await adminCheck( + currentUser._id, + event.organization, + false, + ); + // Checks if user is Event Admin or Admin of the organization + if (!isAdmin && !userIsEventAdmin) { throw new errors.UnauthorizedError( requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), USER_NOT_AUTHORIZED_ERROR.CODE, @@ -74,24 +83,57 @@ export const createEventVolunteerGroup: MutationResolvers["createEventVolunteerG ); } + // Create the new volunteer group const createdVolunteerGroup = await EventVolunteerGroup.create({ - eventId: args.data.eventId, - creatorId: context.userId, - leaderId: context.userId, - name: args.data.name, - volunteersRequired: args.data?.volunteersRequired, + creator: context.userId, + event: eventId, + leader: leaderId, + name, + description, + volunteers: [], + volunteersRequired, }); - await Event.findOneAndUpdate( - { - _id: args.data.eventId, - }, - { - $push: { - volunteerGroups: createdVolunteerGroup._id, - }, - }, + // Fetch Volunteers or Create New Ones if Necessary + const volunteers = await EventVolunteer.find({ + user: { $in: volunteerUserIds }, + event: eventId, + }).lean(); + + const existingVolunteerIds = volunteers.map((vol) => vol.user.toString()); + const newVolunteerUserIds = volunteerUserIds.filter( + (id) => !existingVolunteerIds.includes(id), ); + // Bulk Create New Volunteers if Needed + const newVolunteers = await EventVolunteer.insertMany( + newVolunteerUserIds.map((userId) => ({ + user: userId, + event: eventId, + creator: context.userId, + groups: [], + })), + ); + + const allVolunteerIds = [ + ...volunteers.map((v) => v._id.toString()), + ...newVolunteers.map((v) => v._id.toString()), + ]; + + // Bulk Create VolunteerMembership Records + await VolunteerMembership.insertMany( + allVolunteerIds.map((volunteerId) => ({ + volunteer: volunteerId, + group: createdVolunteerGroup._id, + event: eventId, + status: "invited", + createdBy: context.userId, + })), + ); + + await Event.findByIdAndUpdate(eventId, { + $push: { volunteerGroups: createdVolunteerGroup._id }, + }); + return createdVolunteerGroup.toObject(); }; diff --git a/src/resolvers/Mutation/createVolunteerMembership.ts b/src/resolvers/Mutation/createVolunteerMembership.ts new file mode 100644 index 0000000000..e46407c45f --- /dev/null +++ b/src/resolvers/Mutation/createVolunteerMembership.ts @@ -0,0 +1,81 @@ +import { EVENT_NOT_FOUND_ERROR, USER_NOT_FOUND_ERROR } from "../../constants"; +import { errors, requestContext } from "../../libraries"; +import { Event, User, VolunteerMembership } from "../../models"; +import { EventVolunteer } from "../../models/EventVolunteer"; +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { checkUserExists } from "../../utilities/checks"; + +/** + * Creates a new event volunteer membership entry. + * + * This function performs the following actions: + * 1. Validates the existence of the current user. + * 2. Checks if the specified user and event exist. + * 3. Creates a new volunteer entry for the event. + * 4. Creates a volunteer membership record for the new volunteer. + * 5. Returns the created vvolunteer membership record. + * + * @param _parent - The parent object for the mutation. This parameter is not used in this resolver. + * @param args - The arguments for the mutation, including: + * - `data.userId`: The ID of the user to be assigned as a volunteer. + * - `data.event`: The ID of the event for which the volunteer is being created. + * - `data.group`: The ID of the volunteer group to which the user is being added. + * - `data.status`: The status of the volunteer membership. + * + * @param context - The context for the mutation, including: + * - `userId`: The ID of the current user performing the operation. + * + * @returns The created event volunteer record. + * + */ +export const createVolunteerMembership: MutationResolvers["createVolunteerMembership"] = + async (_parent, args, context) => { + const { event: eventId, status, group, userId } = args.data; + await checkUserExists(context.userId); + + // Check if the volunteer user exists + const volunteerUser = await User.findById(userId).lean(); + if (!volunteerUser) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM, + ); + } + // Check if the event exists + const event = await Event.findById(eventId).populate("organization").lean(); + if (!event) { + throw new errors.NotFoundError( + requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE), + EVENT_NOT_FOUND_ERROR.CODE, + EVENT_NOT_FOUND_ERROR.PARAM, + ); + } + + // check if event volunteer exists + let eventVolunteer = await EventVolunteer.findOne({ + user: userId, + event: eventId, + }).lean(); + + if (!eventVolunteer) { + // create the volunteer + eventVolunteer = await EventVolunteer.create({ + user: userId, + event: eventId, + creator: context.userId, + groups: [], + }); + } + + // create volunteer membership record + const membership = await VolunteerMembership.create({ + volunteer: eventVolunteer._id, + event: eventId, + status: status, + ...(group && { group }), + createdBy: context.userId, + }); + + return membership.toObject(); + }; diff --git a/src/resolvers/Mutation/index.ts b/src/resolvers/Mutation/index.ts index 4c6359e4b4..1e97d33522 100644 --- a/src/resolvers/Mutation/index.ts +++ b/src/resolvers/Mutation/index.ts @@ -40,6 +40,7 @@ import { createSampleOrganization } from "./createSampleOrganization"; import { createUserFamily } from "./createUserFamily"; import { createUserTag } from "./createUserTag"; import { createVenue } from "./createVenue"; +import { createVolunteerMembership } from "./createVolunteerMembership"; import { deleteAdvertisement } from "./deleteAdvertisement"; import { deleteAgendaCategory } from "./deleteAgendaCategory"; import { deleteDonationById } from "./deleteDonationById"; @@ -114,6 +115,7 @@ import { updateUserPassword } from "./updateUserPassword"; import { updateUserProfile } from "./updateUserProfile"; import { updateUserRoleInOrganization } from "./updateUserRoleInOrganization"; import { updateUserTag } from "./updateUserTag"; +import { updateVolunteerMembership } from "./updateVolunteerMembership"; import { createNote } from "./createNote"; import { deleteNote } from "./deleteNote"; import { updateNote } from "./updateNote"; @@ -161,6 +163,7 @@ export const Mutation: MutationResolvers = { createActionItemCategory, createUserTag, createVenue, + createVolunteerMembership, deleteDonationById, deleteAdvertisement, deleteVenue, @@ -231,6 +234,7 @@ export const Mutation: MutationResolvers = { updateUserProfile, updateUserPassword, updateUserTag, + updateVolunteerMembership, updatePost, updateAdvertisement, updateFundraisingCampaign, diff --git a/src/resolvers/Mutation/removeEventVolunteer.ts b/src/resolvers/Mutation/removeEventVolunteer.ts index d0b42ebe9e..f375b1ae6a 100644 --- a/src/resolvers/Mutation/removeEventVolunteer.ts +++ b/src/resolvers/Mutation/removeEventVolunteer.ts @@ -1,14 +1,13 @@ import { - EVENT_VOLUNTEER_NOT_FOUND_ERROR, - USER_NOT_AUTHORIZED_ERROR, - USER_NOT_FOUND_ERROR, -} from "../../constants"; -import { errors, requestContext } from "../../libraries"; -import type { InterfaceUser } from "../../models"; -import { EventVolunteer, EventVolunteerGroup, User } from "../../models"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; + EventVolunteer, + EventVolunteerGroup, + VolunteerMembership, +} from "../../models"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { + checkEventVolunteerExists, + checkUserExists, +} from "../../utilities/checks"; /** * This function enables to remove an Event Volunteer. @@ -16,73 +15,33 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; * @param args - payload provided with the request * @param context - context of entire application * @remarks The following checks are done: - * 1. If the current user exists - * 2. If the Event volunteer to be removed exists. - * 3. If the current user is leader of the corresponding event volunteer group. + * 1. If the user exists. + * 2. If the Event Volunteer exists. + * 3. Remove the Event Volunteer from their groups and delete the volunteer. + * 4. Delete the volunteer and their memberships in a single operation. * @returns Event Volunteer. */ export const removeEventVolunteer: MutationResolvers["removeEventVolunteer"] = async (_parent, args, context) => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); - } - } - - if (!currentUser) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, - ); - } + await checkUserExists(context.userId); + const volunteer = await checkEventVolunteerExists(args.id); - const volunteer = await EventVolunteer.findOne({ - _id: args.id, - }); + // Remove volunteer from their groups and delete the volunteer + const groupIds = volunteer.groups; - if (!volunteer) { - throw new errors.NotFoundError( - requestContext.translate(EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE), - EVENT_VOLUNTEER_NOT_FOUND_ERROR.CODE, - EVENT_VOLUNTEER_NOT_FOUND_ERROR.PARAM, + if (groupIds.length > 0) { + await EventVolunteerGroup.updateMany( + { _id: { $in: groupIds } }, + { $pull: { volunteers: volunteer._id } }, ); } - const group = await EventVolunteerGroup.findById(volunteer.groupId); - - const userIsLeader = - group?.leaderId.toString() === currentUser._id.toString(); - - if (!userIsLeader) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), - USER_NOT_AUTHORIZED_ERROR.CODE, - USER_NOT_AUTHORIZED_ERROR.PARAM, - ); - } - - await EventVolunteer.deleteOne({ - _id: args.id, - }); - - await EventVolunteerGroup.updateOne( - { - _id: volunteer.groupId, - }, - { - $pull: { - volunteers: volunteer._id, - }, - }, - ); + // Delete the volunteer and their memberships in a single operation + await Promise.all([ + EventVolunteer.deleteOne({ _id: volunteer._id }), + VolunteerMembership.deleteMany({ volunteer: volunteer._id }), + ]); return volunteer; }; diff --git a/src/resolvers/Mutation/removeEventVolunteerGroup.ts b/src/resolvers/Mutation/removeEventVolunteerGroup.ts index 74b072c277..346ff8a4e9 100644 --- a/src/resolvers/Mutation/removeEventVolunteerGroup.ts +++ b/src/resolvers/Mutation/removeEventVolunteerGroup.ts @@ -1,14 +1,20 @@ import { - EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, + EVENT_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, - USER_NOT_FOUND_ERROR, } from "../../constants"; import { errors, requestContext } from "../../libraries"; -import type { InterfaceUser } from "../../models"; -import { Event, EventVolunteer, EventVolunteerGroup, User } from "../../models"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; +import { + Event, + EventVolunteer, + EventVolunteerGroup, + VolunteerMembership, +} from "../../models"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { adminCheck } from "../../utilities"; +import { + checkUserExists, + checkVolunteerGroupExists, +} from "../../utilities/checks"; /** * This function enables to remove an Event Volunteer Group. @@ -24,59 +30,57 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; export const removeEventVolunteerGroup: MutationResolvers["removeEventVolunteerGroup"] = async (_parent, args, context) => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); - } - } + const currentUser = await checkUserExists(context.userId); + const volunteerGroup = await checkVolunteerGroupExists(args.id); - if (!currentUser) { + const event = await Event.findById(volunteerGroup.event) + .populate("organization") + .lean(); + if (!event) { throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, + requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE), + EVENT_NOT_FOUND_ERROR.CODE, + EVENT_NOT_FOUND_ERROR.PARAM, ); } - const volunteerGroup = await EventVolunteerGroup.findOne({ - _id: args.id, - }); - - if (!volunteerGroup) { - throw new errors.NotFoundError( - requestContext.translate(EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE), - EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.CODE, - EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.PARAM, - ); - } - - const event = await Event.findById(volunteerGroup.eventId); - - const userIsEventAdmin = event?.admins.some( - (admin) => admin._id.toString() === currentUser?._id.toString(), + const userIsEventAdmin = event.admins.some( + (admin) => admin.toString() === currentUser?._id.toString(), ); - if (!userIsEventAdmin) { - throw new errors.NotFoundError( + const isAdmin = await adminCheck( + currentUser._id, + event.organization, + false, + ); + // Checks if user is Event Admin or Admin of the organization + if (!isAdmin && !userIsEventAdmin) { + throw new errors.UnauthorizedError( requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), USER_NOT_AUTHORIZED_ERROR.CODE, USER_NOT_AUTHORIZED_ERROR.PARAM, ); } - await EventVolunteerGroup.deleteOne({ - _id: args.id, - }); + await Promise.all([ + // Remove the volunteer group + EventVolunteerGroup.deleteOne({ _id: args.id }), + + // Remove the group from volunteers + EventVolunteer.updateMany( + { groups: { $in: args.id } }, + { $pull: { groups: args.id } }, + ), + + // Delete all associated volunteer group memberships + VolunteerMembership.deleteMany({ group: args.id }), - await EventVolunteer.deleteMany({ - groupId: args.id, - }); + // Remove the group from the event + Event.updateOne( + { _id: volunteerGroup.event }, + { $pull: { volunteerGroups: args.id } }, + ), + ]); return volunteerGroup; }; diff --git a/src/resolvers/Mutation/updateActionItem.ts b/src/resolvers/Mutation/updateActionItem.ts index 265a40104c..555cb82433 100644 --- a/src/resolvers/Mutation/updateActionItem.ts +++ b/src/resolvers/Mutation/updateActionItem.ts @@ -2,45 +2,54 @@ import mongoose from "mongoose"; import { ACTION_ITEM_NOT_FOUND_ERROR, EVENT_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, USER_NOT_FOUND_ERROR, - USER_NOT_MEMBER_FOR_ORGANIZATION, } from "../../constants"; import { errors, requestContext } from "../../libraries"; import type { - InterfaceAppUserProfile, InterfaceEvent, + InterfaceEventVolunteer, + InterfaceEventVolunteerGroup, InterfaceUser, } from "../../models"; -import { ActionItem, AppUserProfile, Event, User } from "../../models"; -import { cacheAppUserProfile } from "../../services/AppUserProfileCache/cacheAppUserProfile"; -import { findAppUserProfileCache } from "../../services/AppUserProfileCache/findAppUserProfileCache"; +import { + ActionItem, + Event, + EventVolunteer, + EventVolunteerGroup, + User, +} from "../../models"; import { cacheEvents } from "../../services/EventCache/cacheEvents"; import { findEventsInCache } from "../../services/EventCache/findEventInCache"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { + checkAppUserProfileExists, + checkUserExists, +} from "../../utilities/checks"; /** * This function enables to update an action item. * @param _parent - parent of current request * @param args - payload provided with the request * @param context - context of entire application * @remarks The following checks are done: - * 1. If the user exists. - * 2. If the new asignee exists. - * 2. If the action item exists. - * 4. If the new asignee is a member of the organization. - * 5. If the user is authorized. - * 6. If the user has appUserProfile. + * 1. Whether the user exists + * 2. Whether the user has an associated app user profile + * 3. Whether the action item exists + * 4. Whether the user is authorized to update the action item + * 5. Whether the user is an admin of the organization or a superadmin + * * @returns Updated action item. */ type UpdateActionItemInputType = { assigneeId: string; + assigneeType: string; preCompletionNotes: string; postCompletionNotes: string; dueDate: Date; - allotedHours: number; + allottedHours: number; completionDate: Date; isCompleted: boolean; }; @@ -50,46 +59,9 @@ export const updateActionItem: MutationResolvers["updateActionItem"] = async ( args, context, ) => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); - } - } - - // Checks if the user exists - if (currentUser === null) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, - ); - } - let currentUserAppProfile: InterfaceAppUserProfile | null; - const appUserProfileFoundInCache = await findAppUserProfileCache([ - currentUser.appUserProfileId?.toString(), - ]); - currentUserAppProfile = appUserProfileFoundInCache[0]; - if (currentUserAppProfile === null) { - currentUserAppProfile = await AppUserProfile.findOne({ - userId: currentUser._id, - }).lean(); - if (currentUserAppProfile !== null) { - await cacheAppUserProfile([currentUserAppProfile]); - } - } - if (!currentUserAppProfile) { - throw new errors.UnauthorizedError( - requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), - USER_NOT_AUTHORIZED_ERROR.CODE, - USER_NOT_AUTHORIZED_ERROR.PARAM, - ); - } + const currentUser = await checkUserExists(context.userId); + const currentUserAppProfile = await checkAppUserProfileExists(currentUser); + const { assigneeId, assigneeType, isCompleted } = args.data; const actionItem = await ActionItem.findOne({ _id: args.id, @@ -106,44 +78,54 @@ export const updateActionItem: MutationResolvers["updateActionItem"] = async ( ); } - let sameAssignedUser = false; + let sameAssignee = false; - if (args.data.assigneeId) { - sameAssignedUser = new mongoose.Types.ObjectId( - actionItem.assignee.toString(), - ).equals(args.data.assigneeId); + if (assigneeId) { + sameAssignee = new mongoose.Types.ObjectId( + assigneeType === "EventVolunteer" + ? actionItem.assignee.toString() + : assigneeType === "EventVolunteerGroup" + ? actionItem.assigneeGroup.toString() + : actionItem.assigneeUser.toString(), + ).equals(assigneeId); - if (!sameAssignedUser) { - const newAssignedUser = await User.findOne({ - _id: args.data.assigneeId, - }); - - // Checks if the new asignee exists - if (newAssignedUser === null) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, - ); - } - - let userIsOrganizationMember = false; - const currorganizationId = actionItem.actionItemCategory.organizationId; - userIsOrganizationMember = newAssignedUser.joinedOrganizations.some( - (organizationId) => - organizationId === currorganizationId || - new mongoose.Types.ObjectId(organizationId.toString()).equals( - currorganizationId, - ), - ); - - // Checks if the new asignee is a member of the organization - if (!userIsOrganizationMember) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_MEMBER_FOR_ORGANIZATION.MESSAGE), - USER_NOT_MEMBER_FOR_ORGANIZATION.CODE, - USER_NOT_MEMBER_FOR_ORGANIZATION.PARAM, - ); + if (!sameAssignee) { + let assignee: + | InterfaceEventVolunteer + | InterfaceEventVolunteerGroup + | InterfaceUser + | null; + if (assigneeType === "EventVolunteer") { + assignee = await EventVolunteer.findById(assigneeId) + .populate("user") + .lean(); + if (!assignee) { + throw new errors.NotFoundError( + requestContext.translate(EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE), + EVENT_VOLUNTEER_NOT_FOUND_ERROR.CODE, + EVENT_VOLUNTEER_NOT_FOUND_ERROR.PARAM, + ); + } + } else if (assigneeType === "EventVolunteerGroup") { + assignee = await EventVolunteerGroup.findById(assigneeId).lean(); + if (!assignee) { + throw new errors.NotFoundError( + requestContext.translate( + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE, + ), + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.CODE, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.PARAM, + ); + } + } else if (assigneeType === "User") { + assignee = await User.findById(assigneeId).lean(); + if (!assignee) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM, + ); + } } } } @@ -192,8 +174,9 @@ export const updateActionItem: MutationResolvers["updateActionItem"] = async ( ); } - // Checks if the user is authorized for the operation. + // Checks if the user is authorized for the operation. (Exception: when user updates the action item to complete or incomplete) if ( + isCompleted === undefined && currentUserIsEventAdmin === false && currentUserIsOrgAdmin === false && currentUserAppProfile.isSuperAdmin === false @@ -205,13 +188,102 @@ export const updateActionItem: MutationResolvers["updateActionItem"] = async ( ); } - const updatedAssignmentDate = sameAssignedUser + // checks if the assignee is an event volunteer then add allotted hours to the volunteer else if event volunteer group then add divided equal allotted hours to all volunteers in the group + + if (assigneeType === "EventVolunteer") { + const assignee = await EventVolunteer.findById(assigneeId).lean(); + if (assignee) { + if (isCompleted == true) { + await EventVolunteer.findByIdAndUpdate(assigneeId, { + $inc: { + hoursVolunteered: actionItem.allottedHours + ? actionItem.allottedHours + : 0, + }, + ...(actionItem.allottedHours + ? { + $push: { + hoursHistory: { + hours: actionItem.allottedHours, + date: new Date(), + }, + }, + } + : {}), + }); + } else if (isCompleted == false) { + await EventVolunteer.findByIdAndUpdate(assigneeId, { + $inc: { + hoursVolunteered: actionItem.allottedHours + ? -actionItem.allottedHours + : -0, + }, + ...(actionItem.allottedHours + ? { + $push: { + hoursHistory: { + hours: -actionItem.allottedHours, + date: new Date(), + }, + }, + } + : {}), + }); + } + } + } else if (assigneeType === "EventVolunteerGroup") { + const volunteerGroup = + await EventVolunteerGroup.findById(assigneeId).lean(); + if (volunteerGroup) { + const dividedHours = + (actionItem.allottedHours ?? 0) / volunteerGroup.volunteers.length; + if (isCompleted == true) { + await EventVolunteer.updateMany( + { _id: { $in: volunteerGroup.volunteers } }, + { + $inc: { + hoursVolunteered: dividedHours, + }, + ...(dividedHours + ? { + $push: { + hoursHistory: { + hours: dividedHours, + date: new Date(), + }, + }, + } + : {}), + }, + ); + } else if (isCompleted == false) { + await EventVolunteer.updateMany( + { _id: { $in: volunteerGroup.volunteers } }, + { + $inc: { + hoursVolunteered: -dividedHours, + }, + ...(dividedHours + ? { + $push: { + hoursHistory: { + hours: dividedHours, + date: new Date(), + }, + }, + } + : {}), + }, + ); + } + } + } + + const updatedAssignmentDate = sameAssignee ? actionItem.assignmentDate : new Date(); - const updatedAssigner = sameAssignedUser - ? actionItem.assigner - : context.userId; + const updatedAssigner = sameAssignee ? actionItem.assigner : context.userId; const updatedActionItem = await ActionItem.findOneAndUpdate( { @@ -219,7 +291,25 @@ export const updateActionItem: MutationResolvers["updateActionItem"] = async ( }, { ...(args.data as UpdateActionItemInputType), - assignee: args.data.assigneeId || actionItem.assignee, + assigneeType: assigneeType || actionItem.assigneeType, + assignee: + !sameAssignee && assigneeType === "EventVolunteer" + ? assigneeId || actionItem.assignee + : isCompleted === undefined + ? null + : actionItem.assignee, + assigneeGroup: + !sameAssignee && assigneeType === "EventVolunteerGroup" + ? assigneeId || actionItem.assigneeGroup + : isCompleted === undefined + ? null + : actionItem.assigneeGroup, + assigneeUser: + !sameAssignee && assigneeType === "User" + ? assigneeId || actionItem.assigneeUser + : isCompleted === undefined + ? null + : actionItem.assigneeUser, assignmentDate: updatedAssignmentDate, assigner: updatedAssigner, }, diff --git a/src/resolvers/Mutation/updateEventVolunteer.ts b/src/resolvers/Mutation/updateEventVolunteer.ts index 68d5cdbdbb..6aa7946b1f 100644 --- a/src/resolvers/Mutation/updateEventVolunteer.ts +++ b/src/resolvers/Mutation/updateEventVolunteer.ts @@ -1,15 +1,12 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; -import type { EventVolunteerResponse } from "../../constants"; -import { - EVENT_VOLUNTEER_INVITE_USER_MISTMATCH, - EVENT_VOLUNTEER_NOT_FOUND_ERROR, - USER_NOT_FOUND_ERROR, -} from "../../constants"; -import type { InterfaceEventVolunteer, InterfaceUser } from "../../models"; -import { User, EventVolunteer } from "../../models"; +import { EVENT_VOLUNTEER_INVITE_USER_MISTMATCH } from "../../constants"; +import type { InterfaceEventVolunteer } from "../../models"; +import { EventVolunteer } from "../../models"; import { errors, requestContext } from "../../libraries"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; +import { + checkEventVolunteerExists, + checkUserExists, +} from "../../utilities/checks"; /** * This function enables to update an Event Volunteer * @param _parent - parent of current request @@ -19,43 +16,14 @@ import { cacheUsers } from "../../services/UserCache/cacheUser"; * 1. Whether the user exists * 2. Whether the EventVolunteer exists * 3. Whether the current user is the user of EventVolunteer - * 4. Whether the EventVolunteer is invited + * 4. Update the EventVolunteer */ export const updateEventVolunteer: MutationResolvers["updateEventVolunteer"] = async (_parent, args, context) => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); - } - } - - if (!currentUser) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, - ); - } - - const eventVolunteer = await EventVolunteer.findOne({ - _id: args.id, - }).lean(); - - if (!eventVolunteer) { - throw new errors.NotFoundError( - requestContext.translate(EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE), - EVENT_VOLUNTEER_NOT_FOUND_ERROR.CODE, - EVENT_VOLUNTEER_NOT_FOUND_ERROR.PARAM, - ); - } + await checkUserExists(context.userId); + const volunteer = await checkEventVolunteerExists(args.id); - if (eventVolunteer.userId.toString() !== context.userId.toString()) { + if (volunteer.user.toString() !== context.userId.toString()) { throw new errors.ConflictError( requestContext.translate(EVENT_VOLUNTEER_INVITE_USER_MISTMATCH.MESSAGE), EVENT_VOLUNTEER_INVITE_USER_MISTMATCH.CODE, @@ -69,22 +37,18 @@ export const updateEventVolunteer: MutationResolvers["updateEventVolunteer"] = }, { $set: { - eventId: - args.data?.eventId === undefined - ? eventVolunteer.eventId - : (args?.data.eventId as string), - isAssigned: - args.data?.isAssigned === undefined - ? eventVolunteer.isAssigned - : (args.data?.isAssigned as boolean), - isInvited: - args.data?.isInvited === undefined - ? eventVolunteer.isInvited - : (args.data?.isInvited as boolean), - response: - args.data?.response === undefined - ? eventVolunteer.response - : (args.data?.response as EventVolunteerResponse), + assignments: + args.data?.assignments === undefined + ? volunteer.assignments + : (args.data?.assignments as string[]), + hasAccepted: + args.data?.hasAccepted === undefined + ? volunteer.hasAccepted + : (args.data?.hasAccepted as boolean), + isPublic: + args.data?.isPublic === undefined + ? volunteer.isPublic + : (args.data?.isPublic as boolean), }, }, { diff --git a/src/resolvers/Mutation/updateEventVolunteerGroup.ts b/src/resolvers/Mutation/updateEventVolunteerGroup.ts index e7c67290d4..1abb7112f9 100644 --- a/src/resolvers/Mutation/updateEventVolunteerGroup.ts +++ b/src/resolvers/Mutation/updateEventVolunteerGroup.ts @@ -1,14 +1,14 @@ import { + EVENT_NOT_FOUND_ERROR, EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, - USER_NOT_FOUND_ERROR, } from "../../constants"; import { errors, requestContext } from "../../libraries"; -import type { InterfaceEventVolunteerGroup, InterfaceUser } from "../../models"; -import { EventVolunteerGroup, User } from "../../models"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; +import type { InterfaceEventVolunteerGroup } from "../../models"; +import { Event, EventVolunteerGroup } from "../../models"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { adminCheck } from "../../utilities"; +import { checkUserExists } from "../../utilities/checks"; /** * This function enables to update the Event Volunteer Group * @param _parent - parent of current request @@ -21,26 +21,28 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; */ export const updateEventVolunteerGroup: MutationResolvers["updateEventVolunteerGroup"] = async (_parent, args, context) => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); - } - } - - if (!currentUser) { + const { eventId, description, name, volunteersRequired } = args.data; + const currentUser = await checkUserExists(context.userId); + const event = await Event.findById(eventId).populate("organization").lean(); + if (!event) { throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, + requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE), + EVENT_NOT_FOUND_ERROR.CODE, + EVENT_NOT_FOUND_ERROR.PARAM, ); } + const userIsEventAdmin = event.admins.some( + (admin: { toString: () => string }) => + admin.toString() === currentUser?._id.toString(), + ); + + const isAdmin = await adminCheck( + currentUser._id, + event.organization, + false, + ); + const group = await EventVolunteerGroup.findOne({ _id: args.id, }).lean(); @@ -53,7 +55,12 @@ export const updateEventVolunteerGroup: MutationResolvers["updateEventVolunteerG ); } - if (group.leaderId.toString() !== context.userId.toString()) { + // Checks if user is Event Admin or Admin of the organization or Leader of the group + if ( + !isAdmin && + !userIsEventAdmin && + group.leader.toString() !== currentUser._id.toString() + ) { throw new errors.UnauthorizedError( requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), USER_NOT_AUTHORIZED_ERROR.CODE, @@ -67,20 +74,13 @@ export const updateEventVolunteerGroup: MutationResolvers["updateEventVolunteerG }, { $set: { - eventId: - args.data?.eventId === undefined - ? group.eventId - : args?.data.eventId, - name: args.data?.name === undefined ? group.name : args?.data.name, - volunteersRequired: - args.data?.volunteersRequired === undefined - ? group.volunteersRequired - : args?.data.volunteersRequired, + description, + name, + volunteersRequired, }, }, { new: true, - runValidators: true, }, ).lean(); diff --git a/src/resolvers/Mutation/updateVolunteerMembership.ts b/src/resolvers/Mutation/updateVolunteerMembership.ts new file mode 100644 index 0000000000..18d28e483d --- /dev/null +++ b/src/resolvers/Mutation/updateVolunteerMembership.ts @@ -0,0 +1,137 @@ +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import type { + InterfaceEvent, + InterfaceEventVolunteerGroup, + InterfaceVolunteerMembership, +} from "../../models"; +import { + Event, + EventVolunteer, + EventVolunteerGroup, + VolunteerMembership, +} from "../../models"; +import { + checkUserExists, + checkVolunteerMembershipExists, +} from "../../utilities/checks"; +import { adminCheck } from "../../utilities"; +import { errors, requestContext } from "../../libraries"; +import { USER_NOT_AUTHORIZED_ERROR } from "../../constants"; + +/** + * Helper function to handle updates when status is accepted + */ +const handleAcceptedStatusUpdates = async ( + membership: InterfaceVolunteerMembership, +): Promise => { + const updatePromises = []; + + // Always update EventVolunteer to set hasAccepted to true + updatePromises.push( + EventVolunteer.findOneAndUpdate( + { _id: membership.volunteer, event: membership.event }, + { + $set: { hasAccepted: true }, + ...(membership.group && { $push: { groups: membership.group } }), + }, + ), + ); + + // Always update Event to add volunteer + updatePromises.push( + Event.findOneAndUpdate( + { _id: membership.event }, + { $addToSet: { volunteers: membership.volunteer } }, + ), + ); + + // If group exists, update the EventVolunteerGroup as well + if (membership.group) { + updatePromises.push( + EventVolunteerGroup.findOneAndUpdate( + { _id: membership.group }, + { $addToSet: { volunteers: membership.volunteer } }, + ), + ); + } + + // Execute all updates in parallel + await Promise.all(updatePromises); +}; + +/** + * This function enables to update an Volunteer Membership + * @param _parent - parent of current request + * @param args - payload provided with the request + * @param context - context of entire application + * @remarks The following checks are done: + * 1. Whether the user exists + * 2. Update the Volunteer Membership + * 3. update related fields of Volunteer Group & Volunteer + */ +export const updateVolunteerMembership: MutationResolvers["updateVolunteerMembership"] = + async (_parent, args, context) => { + const currentUser = await checkUserExists(context.userId); + const volunteerMembership = await checkVolunteerMembershipExists(args.id); + + const event = (await Event.findById(volunteerMembership.event) + .populate("organization") + .lean()) as InterfaceEvent; + + if (volunteerMembership.status != "invited") { + // Check if the user is authorized to update the volunteer membership + const isAdminOrSuperAdmin = await adminCheck( + currentUser._id, + event.organization, + false, + ); + const isEventAdmin = event.admins.some( + (admin) => admin.toString() == currentUser._id.toString(), + ); + let isGroupLeader = false; + if (volunteerMembership.group != undefined) { + // check if current user is group leader + const group = (await EventVolunteerGroup.findById( + volunteerMembership.group, + ).lean()) as InterfaceEventVolunteerGroup; + isGroupLeader = group.leader.toString() == currentUser._id.toString(); + } + + // If the user is not an admin or super admin, event admin, or group leader, throw an error + if (!isAdminOrSuperAdmin && !isEventAdmin && !isGroupLeader) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM, + ); + } + } + + const updatedVolunteerMembership = + (await VolunteerMembership.findOneAndUpdate( + { + _id: args.id, + }, + { + $set: { + status: args.status as + | "invited" + | "requested" + | "accepted" + | "rejected", + updatedBy: context.userId, + }, + }, + { + new: true, + runValidators: true, + }, + ).lean()) as InterfaceVolunteerMembership; + + // Handle additional updates if the status is accepted + if (args.status === "accepted") { + await handleAcceptedStatusUpdates(updatedVolunteerMembership); + } + + return updatedVolunteerMembership; + }; diff --git a/src/resolvers/Query/actionItemsByOrganization.ts b/src/resolvers/Query/actionItemsByOrganization.ts index 828cf58005..ac7b10f7cd 100644 --- a/src/resolvers/Query/actionItemsByOrganization.ts +++ b/src/resolvers/Query/actionItemsByOrganization.ts @@ -2,6 +2,7 @@ import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; import type { InterfaceActionItem, InterfaceActionItemCategory, + InterfaceEventVolunteer, InterfaceUser, } from "../../models"; import { ActionItem } from "../../models"; @@ -24,7 +25,14 @@ export const actionItemsByOrganization: QueryResolvers["actionItemsByOrganizatio ...where, }) .populate("creator") - .populate("assignee") + .populate({ + path: "assignee", + populate: { + path: "user", + }, + }) + .populate("assigneeUser") + .populate("assigneeGroup") .populate("assigner") .populate("actionItemCategory") .populate("organization") @@ -46,10 +54,24 @@ export const actionItemsByOrganization: QueryResolvers["actionItemsByOrganizatio // Filter the action items based on assignee name if (args.where?.assigneeName) { + const assigneeName = args.where.assigneeName.toLowerCase(); filteredActionItems = filteredActionItems.filter((item) => { - const tempItem = item as InterfaceActionItem; - const assignee = tempItem.assignee as InterfaceUser; - return assignee.firstName.includes(args?.where?.assigneeName as string); + const assigneeType = item.assigneeType; + + if (assigneeType === "EventVolunteer") { + const assignee = item.assignee as InterfaceEventVolunteer; + const assigneeUser = assignee.user as InterfaceUser; + const name = + `${assigneeUser.firstName} ${assigneeUser.lastName}`.toLowerCase(); + + return name.includes(assigneeName); + } else if (assigneeType === "EventVolunteerGroup") { + return item.assigneeGroup.name.toLowerCase().includes(assigneeName); + } else if (assigneeType === "User") { + const name = + `${item.assigneeUser.firstName} ${item.assigneeUser.lastName}`.toLowerCase(); + return name.includes(assigneeName); + } }); } diff --git a/src/resolvers/Query/actionItemsByUser.ts b/src/resolvers/Query/actionItemsByUser.ts new file mode 100644 index 0000000000..43f1af3b76 --- /dev/null +++ b/src/resolvers/Query/actionItemsByUser.ts @@ -0,0 +1,109 @@ +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import type { + InterfaceActionItem, + InterfaceActionItemCategory, + InterfaceEvent, + InterfaceEventVolunteer, + InterfaceUser, +} from "../../models"; +import { ActionItem, EventVolunteer } from "../../models"; + +/** + * This query will fetch all action items for an organization from database. + * @param _parent- + * @param args - An object that contains `organizationId` which is the _id of the Organization. + * @returns An `actionItems` object that holds all action items for the Event. + */ +export const actionItemsByUser: QueryResolvers["actionItemsByUser"] = async ( + _parent, + args, +) => { + const volunteerObjects = await EventVolunteer.find({ + user: args.userId, + }) + .populate({ + path: "assignments", + populate: [ + { path: "creator" }, + { + path: "assignee", + populate: { path: "user" }, + }, + { path: "assigneeGroup" }, + { path: "assigner" }, + { path: "actionItemCategory" }, + { path: "organization" }, + { path: "event" }, + ], + }) + .populate("event") + .lean(); + + const userActionItems = await ActionItem.find({ + assigneeType: "User", + assigneeUser: args.userId, + organization: args.where?.orgId, + }) + .populate("creator") + .populate("assigner") + .populate("actionItemCategory") + .populate("organization") + .populate("assigneeUser") + .lean(); + + const actionItems: InterfaceActionItem[] = []; + volunteerObjects.forEach((volunteer) => { + const tempEvent = volunteer.event as InterfaceEvent; + if (tempEvent.organization._id.toString() === args.where?.orgId) + actionItems.push(...volunteer.assignments); + }); + + actionItems.push(...userActionItems); + + let filteredActionItems: InterfaceActionItem[] = actionItems; + + // filtering based on category name + if (args.where?.categoryName) { + const categoryName = args.where.categoryName.toLowerCase(); + filteredActionItems = filteredActionItems.filter((item) => { + const category = item.actionItemCategory as InterfaceActionItemCategory; + return category.name.toLowerCase().includes(categoryName); + }); + } + + // filtering based on assignee name + if (args.where?.assigneeName) { + const assigneeName = args.where.assigneeName.toLowerCase(); + + filteredActionItems = filteredActionItems.filter((item) => { + const assigneeType = item.assigneeType; + + if (assigneeType === "EventVolunteer") { + const assignee = item.assignee as InterfaceEventVolunteer; + const assigneeUser = assignee.user as InterfaceUser; + const name = + `${assigneeUser.firstName} ${assigneeUser.lastName}`.toLowerCase(); + + return name.includes(assigneeName); + } else if (assigneeType === "EventVolunteerGroup") { + return item.assigneeGroup.name.toLowerCase().includes(assigneeName); + } else if (assigneeType === "User") { + const name = + `${item.assigneeUser.firstName} ${item.assigneeUser.lastName}`.toLowerCase(); + return name.includes(assigneeName); + } + }); + } + + if (args.orderBy === "dueDate_DESC") { + filteredActionItems.sort((a, b) => { + return new Date(b.dueDate).getTime() - new Date(a.dueDate).getTime(); + }); + } else if (args.orderBy === "dueDate_ASC") { + filteredActionItems.sort((a, b) => { + return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime(); + }); + } + + return filteredActionItems as InterfaceActionItem[]; +}; diff --git a/src/resolvers/Query/eventVolunteersByEvent.ts b/src/resolvers/Query/eventVolunteersByEvent.ts deleted file mode 100644 index e982f58e18..0000000000 --- a/src/resolvers/Query/eventVolunteersByEvent.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; -import { EventVolunteer } from "../../models"; -/** - * This query will fetch all events volunteers for the given eventId from database. - * @param _parent- - * @param args - An object that contains `id` of the Event. - * @returns An object that holds all Event Volunteers for the given Event - */ -export const eventVolunteersByEvent: QueryResolvers["eventVolunteersByEvent"] = - async (_parent, args) => { - const eventId = args.id; - - const volunteers = EventVolunteer.find({ - eventId: eventId, - }) - .populate("userId", "-password") - .lean(); - - return volunteers; - }; diff --git a/src/resolvers/Query/eventsByOrganizationConnection.ts b/src/resolvers/Query/eventsByOrganizationConnection.ts index e184a2bc47..0991d0a8af 100644 --- a/src/resolvers/Query/eventsByOrganizationConnection.ts +++ b/src/resolvers/Query/eventsByOrganizationConnection.ts @@ -4,6 +4,7 @@ import { Event } from "../../models"; import { getSort } from "./helperFunctions/getSort"; import { getWhere } from "./helperFunctions/getWhere"; import { createRecurringEventInstancesDuringQuery } from "../../helpers/event/createEventHelpers"; + /** * Retrieves events for a specific organization based on the provided query parameters. * @@ -26,10 +27,19 @@ export const eventsByOrganizationConnection: QueryResolvers["eventsByOrganizatio // get the where and sort let where = getWhere(args.where); const sort = getSort(args.orderBy); - + const currentDate = new Date(); where = { ...where, isBaseRecurringEvent: false, + ...(args.upcomingOnly && { + $or: [ + { endDate: { $gt: currentDate } }, // Future dates + { + endDate: { $eq: currentDate.toISOString().split("T")[0] }, // Events today + endTime: { $gt: currentDate }, // But start time is after current time + }, + ], + }), }; // find all the events according to the requirements @@ -39,6 +49,13 @@ export const eventsByOrganizationConnection: QueryResolvers["eventsByOrganizatio .skip(args.skip ?? 0) .populate("creatorId", "-password") .populate("admins", "-password") + .populate("volunteerGroups") + .populate({ + path: "volunteers", + populate: { + path: "user", + }, + }) .lean(); return events; diff --git a/src/resolvers/Query/getEventVolunteerGroups.ts b/src/resolvers/Query/getEventVolunteerGroups.ts index bb5e7d558e..f4f6913d3e 100644 --- a/src/resolvers/Query/getEventVolunteerGroups.ts +++ b/src/resolvers/Query/getEventVolunteerGroups.ts @@ -1,4 +1,10 @@ -import { EventVolunteerGroup } from "../../models"; +import type { + InterfaceEvent, + InterfaceEventVolunteer, + InterfaceEventVolunteerGroup, + InterfaceUser, +} from "../../models"; +import { EventVolunteer, EventVolunteerGroup } from "../../models"; import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; import { getWhere } from "./helperFunctions/getWhere"; /** @@ -9,14 +15,111 @@ import { getWhere } from "./helperFunctions/getWhere"; */ export const getEventVolunteerGroups: QueryResolvers["getEventVolunteerGroups"] = async (_parent, args) => { - const where = getWhere(args.where); - const eventVolunteerGroups = await EventVolunteerGroup.find({ - ...where, - }) - .populate("eventId") - .populate("creatorId") - .populate("leaderId") - .populate("volunteers"); + const { eventId, leaderName, userId, orgId } = args.where; + let eventVolunteerGroups: InterfaceEventVolunteerGroup[] = []; + if (eventId) { + const where = getWhere({ name_contains: args.where.name_contains }); + eventVolunteerGroups = await EventVolunteerGroup.find({ + event: eventId, + ...where, + }) + .populate("event") + .populate("creator") + .populate("leader") + .populate({ + path: "volunteers", + populate: { + path: "user", + }, + }) + .populate({ + path: "assignments", + populate: { + path: "actionItemCategory", + }, + }) + .lean(); + } else if (userId && orgId) { + const volunteerProfiles = (await EventVolunteer.find({ + user: userId, + }) + .populate({ + path: "groups", + populate: [ + { + path: "event", + }, + { + path: "creator", + }, + { + path: "leader", + }, + { + path: "volunteers", + populate: { + path: "user", + }, + }, + { + path: "assignments", + populate: { + path: "actionItemCategory", + }, + }, + ], + }) + .populate("event") + .lean()) as InterfaceEventVolunteer[]; + volunteerProfiles.forEach((volunteer) => { + const tempEvent = volunteer.event as InterfaceEvent; + if (tempEvent.organization.toString() == orgId) + eventVolunteerGroups.push(...volunteer.groups); + }); + } - return eventVolunteerGroups; + let filteredEventVolunteerGroups: InterfaceEventVolunteerGroup[] = + eventVolunteerGroups; + + if (leaderName) { + const tempName = leaderName.toLowerCase(); + filteredEventVolunteerGroups = filteredEventVolunteerGroups.filter( + (group) => { + const tempGroup = group as InterfaceEventVolunteerGroup; + const tempLeader = tempGroup.leader as InterfaceUser; + const { firstName, lastName } = tempLeader; + const name = `${firstName} ${lastName}`.toLowerCase(); + return name.includes(tempName); + }, + ); + } + + const sortConfigs = { + /* c8 ignore start */ + volunteers_ASC: ( + a: InterfaceEventVolunteerGroup, + b: InterfaceEventVolunteerGroup, + ): number => a.volunteers.length - b.volunteers.length, + /* c8 ignore stop */ + volunteers_DESC: ( + a: InterfaceEventVolunteerGroup, + b: InterfaceEventVolunteerGroup, + ): number => b.volunteers.length - a.volunteers.length, + assignments_ASC: ( + a: InterfaceEventVolunteerGroup, + b: InterfaceEventVolunteerGroup, + ): number => a.assignments.length - b.assignments.length, + assignments_DESC: ( + a: InterfaceEventVolunteerGroup, + b: InterfaceEventVolunteerGroup, + ): number => b.assignments.length - a.assignments.length, + }; + + if (args.orderBy && args.orderBy in sortConfigs) { + filteredEventVolunteerGroups.sort( + sortConfigs[args.orderBy as keyof typeof sortConfigs], + ); + } + + return filteredEventVolunteerGroups; }; diff --git a/src/resolvers/Query/getEventVolunteers.ts b/src/resolvers/Query/getEventVolunteers.ts new file mode 100644 index 0000000000..ccb461f70c --- /dev/null +++ b/src/resolvers/Query/getEventVolunteers.ts @@ -0,0 +1,61 @@ +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import type { InterfaceEventVolunteer, InterfaceUser } from "../../models"; +import { EventVolunteer } from "../../models"; +import { getSort } from "./helperFunctions/getSort"; +import { getWhere } from "./helperFunctions/getWhere"; + +/** + * This query will fetch all events volunteers for the given eventId from database. + * @param _parent- + * @param args - An object that contains `id` of the Event. + * @returns An object that holds all Event Volunteers for the given Event + */ +export const getEventVolunteers: QueryResolvers["getEventVolunteers"] = async ( + _parent, + args, +) => { + const sort = getSort(args.orderBy); + const { + id, + name_contains: nameContains, + hasAccepted, + eventId, + groupId, + } = args.where; + const where = getWhere({ id, hasAccepted }); + + const volunteers = await EventVolunteer.find({ + event: eventId, + ...(groupId && { + groups: { + $in: groupId, + }, + }), + ...where, + }) + .populate("user", "-password") + .populate("event") + .populate("groups") + .populate({ + path: "assignments", + populate: { + path: "actionItemCategory", + }, + }) + .sort(sort) + .lean(); + + let filteredVolunteers: InterfaceEventVolunteer[] = volunteers; + + if (nameContains) { + filteredVolunteers = filteredVolunteers.filter((volunteer) => { + const tempVolunteer = volunteer as InterfaceEventVolunteer; + const tempUser = tempVolunteer.user as InterfaceUser; + const { firstName, lastName } = tempUser; + const name = `${firstName} ${lastName}`.toLowerCase(); + return name.includes(nameContains.toLowerCase()); + }); + } + + return filteredVolunteers; +}; diff --git a/src/resolvers/Query/getVolunteerMembership.ts b/src/resolvers/Query/getVolunteerMembership.ts new file mode 100644 index 0000000000..f9dc8f1831 --- /dev/null +++ b/src/resolvers/Query/getVolunteerMembership.ts @@ -0,0 +1,129 @@ +import type { + InputMaybe, + QueryResolvers, + VolunteerMembershipOrderByInput, +} from "../../types/generatedGraphQLTypes"; +import type { InterfaceVolunteerMembership } from "../../models"; +import { EventVolunteer, VolunteerMembership } from "../../models"; +import { getSort } from "./helperFunctions/getSort"; + +/** + * Helper function to fetch volunteer memberships by userId + */ +const getVolunteerMembershipsByUserId = async ( + userId: string, + orderBy: InputMaybe | undefined, + status?: string, +): Promise => { + const sort = getSort(orderBy); + const volunteerInstance = await EventVolunteer.find({ user: userId }).lean(); + const volunteerIds = volunteerInstance.map((volunteer) => volunteer._id); + + return await VolunteerMembership.find({ + volunteer: { $in: volunteerIds }, + ...(status && { status }), + }) + .sort(sort) + .populate("event") + .populate("group") + .populate({ + path: "volunteer", + populate: { + path: "user", + }, + }) + .lean(); +}; + +/** + * Helper function to fetch volunteer memberships by eventId + */ +const getVolunteerMembershipsByEventId = async ( + eventId: string, + orderBy: InputMaybe | undefined, + status?: string, + group?: string, +): Promise => { + const sort = getSort(orderBy); + + return await VolunteerMembership.find({ + event: eventId, + ...(status && { status }), + ...(group && { group: group }), + }) + .sort(sort) + .populate("event") + .populate("group") + .populate({ + path: "volunteer", + populate: { + path: "user", + }, + }) + .lean(); +}; + +/** + * Helper function to filter memberships based on various criteria + */ +const filterMemberships = ( + memberships: InterfaceVolunteerMembership[], + filter?: string, + eventTitle?: string, + userName?: string, +): InterfaceVolunteerMembership[] => { + return memberships.filter((membership) => { + const filterCondition = filter + ? filter === "group" + ? !!membership.group + : !membership.group + : true; + + const eventTitleCondition = eventTitle + ? membership.event.title.includes(eventTitle) + : true; + + const userNameCondition = userName + ? ( + membership.volunteer.user.firstName + + membership.volunteer.user.lastName + ).includes(userName) + : true; + + return filterCondition && eventTitleCondition && userNameCondition; + }); +}; + +export const getVolunteerMembership: QueryResolvers["getVolunteerMembership"] = + async (_parent, args) => { + const { status, userId, filter, eventTitle, eventId, userName, groupId } = + args.where; + + let volunteerMemberships: InterfaceVolunteerMembership[] = []; + + if (userId) { + volunteerMemberships = await getVolunteerMembershipsByUserId( + userId, + args.orderBy, + status ?? undefined, + ); + } else if (eventId) { + volunteerMemberships = await getVolunteerMembershipsByEventId( + eventId, + args.orderBy, + status ?? undefined, + groupId ?? undefined, + ); + } + + if (filter || eventTitle || userName) { + return filterMemberships( + volunteerMemberships, + filter ?? undefined, + eventTitle ?? undefined, + userName ?? undefined, + ); + } + + return volunteerMemberships; + }; diff --git a/src/resolvers/Query/getVolunteerRanks.ts b/src/resolvers/Query/getVolunteerRanks.ts new file mode 100644 index 0000000000..a117652f62 --- /dev/null +++ b/src/resolvers/Query/getVolunteerRanks.ts @@ -0,0 +1,146 @@ +import { startOfWeek, startOfMonth, startOfYear, endOfDay } from "date-fns"; +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import type { InterfaceEvent, InterfaceUser } from "../../models"; +import { Event, EventVolunteer } from "../../models"; + +/** + * This query will fetch volunteer ranks based on the provided time frame (allTime, weekly, monthly, yearly), + * and it will filter the results based on an array of volunteer IDs. + * @param _parent - parent of the current request + * @param args - An object that contains where object for volunteer ranks. + * + * @returns An array of `VolunteerRank` object. + */ +export const getVolunteerRanks: QueryResolvers["getVolunteerRanks"] = async ( + _parent, + args, +) => { + const { orgId } = args; + const { timeFrame, orderBy, nameContains, limit } = args.where; + + const volunteerIds: string[] = []; + const events = (await Event.find({ + organization: orgId, + }).lean()) as InterfaceEvent[]; + + // Get all volunteer IDs from the events + events.forEach((event) => { + volunteerIds.push( + ...event.volunteers.map((volunteer) => volunteer.toString()), + ); + }); + + // Fetch all volunteers + const volunteers = await EventVolunteer.find({ + _id: { $in: volunteerIds }, + }) + .populate("user") + .lean(); + + const now = new Date(); + let startDate: Date | null = null; + let endDate: Date | null = null; + + // Determine the date range based on the timeframe + switch (timeFrame) { + case "weekly": + startDate = startOfWeek(now); + endDate = endOfDay(now); + break; + case "monthly": + startDate = startOfMonth(now); + endDate = endOfDay(now); + break; + case "yearly": + startDate = startOfYear(now); + endDate = endOfDay(now); + break; + case "allTime": + default: + startDate = null; // No filtering for "allTime" + endDate = null; + break; + } + + // Accumulate total hours per user + const userHoursMap = new Map< + string, + { hoursVolunteered: number; user: InterfaceUser } + >(); + + volunteers.forEach((volunteer) => { + const userId = volunteer.user._id.toString(); + let totalHours = 0; + + // Filter hoursHistory based on the time frame + if (startDate && endDate) { + totalHours = volunteer.hoursHistory.reduce((sum, record) => { + const recordDate = new Date(record.date); + // Check if the record date is within the specified range + if (recordDate >= startDate && recordDate <= endDate) { + return sum + record.hours; + } + return sum; + }, 0); + } else { + // If "allTime", use hoursVolunteered + totalHours = volunteer.hoursVolunteered; + } + + // Accumulate hours for each user + /* c8 ignore start */ + const existingRecord = userHoursMap.get(userId); + if (existingRecord) { + existingRecord.hoursVolunteered += totalHours; + } else { + userHoursMap.set(userId, { + hoursVolunteered: totalHours, + user: volunteer.user, + }); + } + /* c8 ignore stop */ + }); + + // Convert the accumulated map to an array + const volunteerRanks = Array.from(userHoursMap.values()); + + volunteerRanks.sort((a, b) => b.hoursVolunteered - a.hoursVolunteered); + + // Assign ranks, accounting for ties + const rankedVolunteers = []; + let currentRank = 1; + let lastHours = -1; + + for (const volunteer of volunteerRanks) { + if (volunteer.hoursVolunteered !== lastHours) { + currentRank = rankedVolunteers.length + 1; // New rank + } + + rankedVolunteers.push({ + rank: currentRank, + user: volunteer.user, + hoursVolunteered: volunteer.hoursVolunteered, + }); + + lastHours = volunteer.hoursVolunteered; // Update lastHours + } + + // Sort the ranked volunteers based on the orderBy field + + if (orderBy === "hours_ASC") { + rankedVolunteers.sort((a, b) => a.hoursVolunteered - b.hoursVolunteered); + } else if (orderBy === "hours_DESC") { + rankedVolunteers.sort((a, b) => b.hoursVolunteered - a.hoursVolunteered); + } + + // Filter by name + if (nameContains) { + return rankedVolunteers.filter((volunteer) => { + const fullName = + `${volunteer.user.firstName} ${volunteer.user.lastName}`.toLowerCase(); + return fullName.includes(nameContains.toLowerCase()); + }); + } + + return limit ? rankedVolunteers.slice(0, limit) : rankedVolunteers; +}; diff --git a/src/resolvers/Query/helperFunctions/getSort.ts b/src/resolvers/Query/helperFunctions/getSort.ts index d3f68a704c..a00bde9a21 100644 --- a/src/resolvers/Query/helperFunctions/getSort.ts +++ b/src/resolvers/Query/helperFunctions/getSort.ts @@ -10,6 +10,8 @@ import type { CampaignOrderByInput, FundOrderByInput, ActionItemsOrderByInput, + EventVolunteersOrderByInput, + VolunteerMembershipOrderByInput, } from "../../../types/generatedGraphQLTypes"; export const getSort = ( @@ -24,6 +26,8 @@ export const getSort = ( | CampaignOrderByInput | PledgeOrderByInput | ActionItemsOrderByInput + | EventVolunteersOrderByInput + | VolunteerMembershipOrderByInput > | undefined, ): @@ -335,6 +339,18 @@ export const getSort = ( }; break; + case "hoursVolunteered_ASC": + sortPayload = { + hoursVolunteered: 1, + }; + break; + + case "hoursVolunteered_DESC": + sortPayload = { + hoursVolunteered: -1, + }; + break; + default: break; } diff --git a/src/resolvers/Query/helperFunctions/getWhere.ts b/src/resolvers/Query/helperFunctions/getWhere.ts index e2288ee6cb..56207568e4 100644 --- a/src/resolvers/Query/helperFunctions/getWhere.ts +++ b/src/resolvers/Query/helperFunctions/getWhere.ts @@ -13,6 +13,7 @@ import type { CampaignWhereInput, PledgeWhereInput, ActionItemCategoryWhereInput, + EventVolunteerWhereInput, } from "../../../types/generatedGraphQLTypes"; /** @@ -43,7 +44,8 @@ export const getWhere = ( CampaignWhereInput & FundWhereInput & PledgeWhereInput & - VenueWhereInput + VenueWhereInput & + EventVolunteerWhereInput > > | undefined, @@ -764,21 +766,18 @@ export const getWhere = ( }; } - // Returns objects where volunteerId is present in volunteers list - if (where.volunteerId) { + // Returns object with provided is_disabled condition + if (where.is_disabled !== undefined) { wherePayload = { ...wherePayload, - volunteers: { - $in: [where.volunteerId], - }, + isDisabled: where.is_disabled, }; } - // Returns object with provided is_disabled condition - if (where.is_disabled !== undefined) { + if (where.hasAccepted !== undefined) { wherePayload = { ...wherePayload, - isDisabled: where.is_disabled, + hasAccepted: where.hasAccepted, }; } diff --git a/src/resolvers/Query/index.ts b/src/resolvers/Query/index.ts index 8474b60298..a96026ce71 100644 --- a/src/resolvers/Query/index.ts +++ b/src/resolvers/Query/index.ts @@ -2,6 +2,7 @@ import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; import { isSampleOrganization } from "../Query/organizationIsSample"; import { actionItemCategoriesByOrganization } from "./actionItemCategoriesByOrganization"; import { actionItemsByEvent } from "./actionItemsByEvent"; +import { actionItemsByUser } from "./actionItemsByUser"; import { actionItemsByOrganization } from "./actionItemsByOrganization"; import { advertisementsConnection } from "./advertisementsConnection"; import { agendaCategory } from "./agendaCategory"; @@ -18,6 +19,7 @@ import { chatsByUserId } from "./chatsByUserId"; import { event } from "./event"; import { eventsByOrganization } from "./eventsByOrganization"; import { eventsByOrganizationConnection } from "./eventsByOrganizationConnection"; +import { getEventVolunteers } from "./getEventVolunteers"; import { getEventVolunteerGroups } from "./getEventVolunteerGroups"; import { fundsByOrganization } from "./fundsByOrganization"; import { getAllAgendaItems } from "./getAllAgendaItems"; @@ -49,8 +51,11 @@ import { getEventAttendeesByEventId } from "./getEventAttendeesByEventId"; import { getVenueByOrgId } from "./getVenueByOrgId"; import { getAllNotesForAgendaItem } from "./getAllNotesForAgendaItem"; import { getNoteById } from "./getNoteById"; +import { getVolunteerMembership } from "./getVolunteerMembership"; +import { getVolunteerRanks } from "./getVolunteerRanks"; export const Query: QueryResolvers = { actionItemsByEvent, + actionItemsByUser, agendaCategory, getAgendaItem, getAgendaSection, @@ -74,6 +79,7 @@ export const Query: QueryResolvers = { getDonationByOrgId, getDonationByOrgIdConnection, getEventInvitesByUserId, + getEventVolunteers, getEventVolunteerGroups, getAllNotesForAgendaItem, getNoteById, @@ -100,4 +106,6 @@ export const Query: QueryResolvers = { getEventAttendee, getEventAttendeesByEventId, getVenueByOrgId, + getVolunteerMembership, + getVolunteerRanks, }; diff --git a/src/resolvers/index.ts b/src/resolvers/index.ts index 5319a9fc70..ef021d86ef 100644 --- a/src/resolvers/index.ts +++ b/src/resolvers/index.ts @@ -21,7 +21,6 @@ import { Comment } from "./Comment"; import { Chat } from "./Chat"; import { ChatMessage } from "./ChatMessage"; import { Event } from "./Event"; -import { EventVolunteer } from "./EventVolunteer"; import { Feedback } from "./Feedback"; import { Fund } from "./Fund"; import { MembershipRequest } from "./MembershipRequest"; @@ -51,7 +50,6 @@ const resolvers: Resolvers = { Chat, ChatMessage, Event, - EventVolunteer, Feedback, Fund, UserFamily, diff --git a/src/typeDefs/enums.ts b/src/typeDefs/enums.ts index b3e0a8b4c2..f08f9e06bd 100644 --- a/src/typeDefs/enums.ts +++ b/src/typeDefs/enums.ts @@ -141,6 +141,22 @@ export const enums = gql` endDate_DESC } + enum EventVolunteersOrderByInput { + hoursVolunteered_ASC + hoursVolunteered_DESC + } + + enum EventVolunteerGroupOrderByInput { + volunteers_ASC + volunteers_DESC + assignments_ASC + assignments_DESC + } + enum VolunteerMembershipOrderByInput { + createdAt_ASC + createdAt_DESC + } + enum WeekDays { MONDAY TUESDAY diff --git a/src/typeDefs/inputs.ts b/src/typeDefs/inputs.ts index a16daeda49..ae0958f69a 100644 --- a/src/typeDefs/inputs.ts +++ b/src/typeDefs/inputs.ts @@ -38,8 +38,9 @@ export const inputs = gql` input CreateActionItemInput { assigneeId: ID! + assigneeType: String! preCompletionNotes: String - allotedHours: Float + allottedHours: Float dueDate: Date eventId: ID } @@ -70,6 +71,7 @@ export const inputs = gql` } input ActionItemWhereInput { + orgId: ID actionItemCategory_id: ID event_id: ID categoryName: String @@ -146,31 +148,54 @@ export const inputs = gql` input EventVolunteerInput { userId: ID! eventId: ID! - groupId: ID! + groupId: ID + } + + input EventVolunteerWhereInput { + id: ID + eventId: ID + groupId: ID + hasAccepted: Boolean + name_contains: String } input EventVolunteerGroupInput { - name: String + name: String! + description: String eventId: ID! + leaderId: ID! volunteersRequired: Int + volunteerUserIds: [ID!]! } input EventVolunteerGroupWhereInput { eventId: ID - volunteerId: ID + userId: ID + orgId: ID + leaderName: String name_contains: String } - input UpdateEventVolunteerInput { + input VolunteerMembershipWhereInput { + eventTitle: String + userName: String + status: String + userId: ID eventId: ID - isAssigned: Boolean - isInvited: Boolean - response: EventVolunteerResponse + groupId: ID + filter: String + } + + input UpdateEventVolunteerInput { + assignments: [ID] + hasAccepted: Boolean + isPublic: Boolean } input UpdateEventVolunteerGroupInput { - eventId: ID + eventId: ID! name: String + description: String volunteersRequired: Int } @@ -445,11 +470,12 @@ export const inputs = gql` input UpdateActionItemInput { assigneeId: ID + assigneeType: String preCompletionNotes: String postCompletionNotes: String dueDate: Date completionDate: Date - allotedHours: Float + allottedHours: Float isCompleted: Boolean } @@ -662,6 +688,20 @@ export const inputs = gql` file: String } + input VolunteerMembershipInput { + event: ID! + group: ID + status: String! + userId: ID! + } + + input VolunteerRankWhereInput { + nameContains: String + orderBy: String! + timeFrame: String! + limit: Int + } + input VenueWhereInput { name_contains: String name_starts_with: String diff --git a/src/typeDefs/mutations.ts b/src/typeDefs/mutations.ts index b022b72be1..5253da9783 100644 --- a/src/typeDefs/mutations.ts +++ b/src/typeDefs/mutations.ts @@ -139,6 +139,10 @@ export const mutations = gql` createVenue(data: VenueInput!): Venue @auth + createVolunteerMembership( + data: VolunteerMembershipInput! + ): VolunteerMembership! @auth + deleteAdvertisement(id: ID!): DeleteAdvertisementPayload deleteAgendaCategory(id: ID!): ID! @auth @@ -306,9 +310,12 @@ export const mutations = gql` updateEventVolunteerGroup( id: ID! - data: UpdateEventVolunteerGroupInput + data: UpdateEventVolunteerGroupInput! ): EventVolunteerGroup! @auth + updateVolunteerMembership(id: ID!, status: String!): VolunteerMembership! + @auth + updateFundraisingCampaign( id: ID! data: UpdateFundCampaignInput! diff --git a/src/typeDefs/queries.ts b/src/typeDefs/queries.ts index 7572b3387c..43d6f880af 100644 --- a/src/typeDefs/queries.ts +++ b/src/typeDefs/queries.ts @@ -16,6 +16,12 @@ export const queries = gql` orderBy: ActionItemsOrderByInput ): [ActionItem] + actionItemsByUser( + userId: ID! + where: ActionItemWhereInput + orderBy: ActionItemsOrderByInput + ): [ActionItem] + actionItemCategoriesByOrganization( organizationId: ID! where: ActionItemCategoryWhereInput @@ -54,17 +60,32 @@ export const queries = gql` eventsByOrganizationConnection( where: EventWhereInput + upcomingOnly: Boolean first: Int skip: Int orderBy: EventOrderByInput ): [Event!]! - eventVolunteersByEvent(id: ID!): [EventVolunteer] + getEventVolunteers( + where: EventVolunteerWhereInput! + orderBy: EventVolunteersOrderByInput + ): [EventVolunteer]! getEventVolunteerGroups( - where: EventVolunteerGroupWhereInput + where: EventVolunteerGroupWhereInput! + orderBy: EventVolunteerGroupOrderByInput ): [EventVolunteerGroup]! + getVolunteerMembership( + where: VolunteerMembershipWhereInput! + orderBy: VolunteerMembershipOrderByInput + ): [VolunteerMembership]! + + getVolunteerRanks( + orgId: ID! + where: VolunteerRankWhereInput! + ): [VolunteerRank]! + fundsByOrganization( organizationId: ID! where: FundWhereInput diff --git a/src/typeDefs/types.ts b/src/typeDefs/types.ts index 99c1d43720..6018d229e3 100644 --- a/src/typeDefs/types.ts +++ b/src/typeDefs/types.ts @@ -67,15 +67,19 @@ export const types = gql` createdBy: User updatedBy: User } + # Action Item for a ActionItemCategory type ActionItem { _id: ID! - assignee: User + assignee: EventVolunteer + assigneeGroup: EventVolunteerGroup + assigneeUser: User + assigneeType: String! assigner: User actionItemCategory: ActionItemCategory preCompletionNotes: String postCompletionNotes: String - allotedHours: Float + allottedHours: Float assignmentDate: Date! dueDate: Date! completionDate: Date! @@ -252,21 +256,36 @@ export const types = gql` feedback: [Feedback!]! averageFeedbackScore: Float agendaItems: [AgendaItem] + volunteers: [EventVolunteer] + volunteerGroups: [EventVolunteerGroup] } type EventVolunteer { _id: ID! - createdAt: DateTime! + user: User! creator: User event: Event - group: EventVolunteerGroup - isAssigned: Boolean - isInvited: Boolean - response: String - user: User! + groups: [EventVolunteerGroup] + hasAccepted: Boolean! + isPublic: Boolean! + hoursVolunteered: Float! + assignments: [ActionItem] + hoursHistory: [HoursHistory] + createdAt: DateTime! updatedAt: DateTime! } + type HoursHistory { + hours: Float! + date: Date! + } + + type VolunteerRank { + rank: Int! + user: User! + hoursVolunteered: Float! + } + type EventAttendee { _id: ID! userId: ID! @@ -283,14 +302,28 @@ export const types = gql` type EventVolunteerGroup { _id: ID! - createdAt: DateTime! creator: User event: Event leader: User! name: String + description: String + createdAt: DateTime! updatedAt: DateTime! volunteers: [EventVolunteer] volunteersRequired: Int + assignments: [ActionItem] + } + + type VolunteerMembership { + _id: ID! + status: String! + volunteer: EventVolunteer! + event: Event! + group: EventVolunteerGroup + createdBy: User + updatedBy: User + createdAt: DateTime! + updatedAt: DateTime! } type Feedback { diff --git a/src/types/generatedGraphQLTypes.ts b/src/types/generatedGraphQLTypes.ts index a802e174be..f7995820cb 100644 --- a/src/types/generatedGraphQLTypes.ts +++ b/src/types/generatedGraphQLTypes.ts @@ -34,6 +34,7 @@ import type { InterfaceRecurrenceRule as InterfaceRecurrenceRuleModel } from '.. import type { InterfaceOrganizationTagUser as InterfaceOrganizationTagUserModel } from '../models/OrganizationTagUser'; import type { InterfaceUser as InterfaceUserModel } from '../models/User'; import type { InterfaceVenue as InterfaceVenueModel } from '../models/Venue'; +import type { InterfaceVolunteerMembership as InterfaceVolunteerMembershipModel } from '../models/VolunteerMembership'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -69,8 +70,11 @@ export type ActionItem = { __typename?: 'ActionItem'; _id: Scalars['ID']['output']; actionItemCategory?: Maybe; - allotedHours?: Maybe; - assignee?: Maybe; + allottedHours?: Maybe; + assignee?: Maybe; + assigneeGroup?: Maybe; + assigneeType: Scalars['String']['output']; + assigneeUser?: Maybe; assigner?: Maybe; assignmentDate: Scalars['Date']['output']; completionDate: Scalars['Date']['output']; @@ -106,6 +110,7 @@ export type ActionItemWhereInput = { categoryName?: InputMaybe; event_id?: InputMaybe; is_completed?: InputMaybe; + orgId?: InputMaybe; }; export type ActionItemsOrderByInput = @@ -383,8 +388,9 @@ export type ConnectionPageInfo = { }; export type CreateActionItemInput = { - allotedHours?: InputMaybe; + allottedHours?: InputMaybe; assigneeId: Scalars['ID']['input']; + assigneeType: Scalars['String']['input']; dueDate?: InputMaybe; eventId?: InputMaybe; preCompletionNotes?: InputMaybe; @@ -748,6 +754,8 @@ export type Event = { startTime?: Maybe; title: Scalars['String']['output']; updatedAt: Scalars['DateTime']['output']; + volunteerGroups?: Maybe>>; + volunteers?: Maybe>>; }; @@ -818,13 +826,15 @@ export type EventOrderByInput = export type EventVolunteer = { __typename?: 'EventVolunteer'; _id: Scalars['ID']['output']; + assignments?: Maybe>>; createdAt: Scalars['DateTime']['output']; creator?: Maybe; event?: Maybe; - group?: Maybe; - isAssigned?: Maybe; - isInvited?: Maybe; - response?: Maybe; + groups?: Maybe>>; + hasAccepted: Scalars['Boolean']['output']; + hoursHistory?: Maybe>>; + hoursVolunteered: Scalars['Float']['output']; + isPublic: Scalars['Boolean']['output']; updatedAt: Scalars['DateTime']['output']; user: User; }; @@ -832,8 +842,10 @@ export type EventVolunteer = { export type EventVolunteerGroup = { __typename?: 'EventVolunteerGroup'; _id: Scalars['ID']['output']; + assignments?: Maybe>>; createdAt: Scalars['DateTime']['output']; creator?: Maybe; + description?: Maybe; event?: Maybe; leader: User; name?: Maybe; @@ -843,20 +855,31 @@ export type EventVolunteerGroup = { }; export type EventVolunteerGroupInput = { + description?: InputMaybe; eventId: Scalars['ID']['input']; - name?: InputMaybe; + leaderId: Scalars['ID']['input']; + name: Scalars['String']['input']; + volunteerUserIds: Array; volunteersRequired?: InputMaybe; }; +export type EventVolunteerGroupOrderByInput = + | 'assignments_ASC' + | 'assignments_DESC' + | 'volunteers_ASC' + | 'volunteers_DESC'; + export type EventVolunteerGroupWhereInput = { eventId?: InputMaybe; + leaderName?: InputMaybe; name_contains?: InputMaybe; - volunteerId?: InputMaybe; + orgId?: InputMaybe; + userId?: InputMaybe; }; export type EventVolunteerInput = { eventId: Scalars['ID']['input']; - groupId: Scalars['ID']['input']; + groupId?: InputMaybe; userId: Scalars['ID']['input']; }; @@ -864,6 +887,18 @@ export type EventVolunteerResponse = | 'NO' | 'YES'; +export type EventVolunteerWhereInput = { + eventId?: InputMaybe; + groupId?: InputMaybe; + hasAccepted?: InputMaybe; + id?: InputMaybe; + name_contains?: InputMaybe; +}; + +export type EventVolunteersOrderByInput = + | 'hoursVolunteered_ASC' + | 'hoursVolunteered_DESC'; + export type EventWhereInput = { description?: InputMaybe; description_contains?: InputMaybe; @@ -1024,6 +1059,12 @@ export type Group = { updatedAt: Scalars['DateTime']['output']; }; +export type HoursHistory = { + __typename?: 'HoursHistory'; + date: Scalars['Date']['output']; + hours: Scalars['Float']['output']; +}; + export type InvalidCursor = FieldError & { __typename?: 'InvalidCursor'; message: Scalars['String']['output']; @@ -1178,6 +1219,7 @@ export type Mutation = { createUserFamily: UserFamily; createUserTag?: Maybe; createVenue?: Maybe; + createVolunteerMembership: VolunteerMembership; deleteAdvertisement?: Maybe; deleteAgendaCategory: Scalars['ID']['output']; deleteDonationById: DeletePayload; @@ -1256,6 +1298,7 @@ export type Mutation = { updateUserProfile: User; updateUserRoleInOrganization: Organization; updateUserTag?: Maybe; + updateVolunteerMembership: VolunteerMembership; }; @@ -1493,6 +1536,11 @@ export type MutationCreateVenueArgs = { }; +export type MutationCreateVolunteerMembershipArgs = { + data: VolunteerMembershipInput; +}; + + export type MutationDeleteAdvertisementArgs = { id: Scalars['ID']['input']; }; @@ -1806,7 +1854,7 @@ export type MutationUpdateEventVolunteerArgs = { export type MutationUpdateEventVolunteerGroupArgs = { - data?: InputMaybe; + data: UpdateEventVolunteerGroupInput; id: Scalars['ID']['input']; }; @@ -1886,6 +1934,12 @@ export type MutationUpdateUserTagArgs = { input: UpdateUserTagInput; }; + +export type MutationUpdateVolunteerMembershipArgs = { + id: Scalars['ID']['input']; + status: Scalars['String']['input']; +}; + export type Note = { __typename?: 'Note'; _id: Scalars['ID']['output']; @@ -2218,6 +2272,7 @@ export type Query = { actionItemCategoriesByOrganization?: Maybe>>; actionItemsByEvent?: Maybe>>; actionItemsByOrganization?: Maybe>>; + actionItemsByUser?: Maybe>>; adminPlugin?: Maybe>>; advertisementsConnection?: Maybe; agendaCategory: AgendaCategory; @@ -2230,7 +2285,6 @@ export type Query = { customDataByOrganization: Array; customFieldsByOrganization?: Maybe>>; event?: Maybe; - eventVolunteersByEvent?: Maybe>>; eventsByOrganization?: Maybe>>; eventsByOrganizationConnection: Array; fundsByOrganization?: Maybe>>; @@ -2246,6 +2300,7 @@ export type Query = { getEventAttendeesByEventId?: Maybe>>; getEventInvitesByUserId: Array; getEventVolunteerGroups: Array>; + getEventVolunteers: Array>; getFundById: Fund; getFundraisingCampaignPledgeById: FundraisingCampaignPledge; getFundraisingCampaigns: Array>; @@ -2254,6 +2309,8 @@ export type Query = { getPlugins?: Maybe>>; getUserTag?: Maybe; getVenueByOrgId?: Maybe>>; + getVolunteerMembership: Array>; + getVolunteerRanks: Array>; getlanguage?: Maybe>>; hasSubmittedFeedback?: Maybe; isSampleOrganization: Scalars['Boolean']['output']; @@ -2295,6 +2352,13 @@ export type QueryActionItemsByOrganizationArgs = { }; +export type QueryActionItemsByUserArgs = { + orderBy?: InputMaybe; + userId: Scalars['ID']['input']; + where?: InputMaybe; +}; + + export type QueryAdminPluginArgs = { orgId: Scalars['ID']['input']; }; @@ -2353,11 +2417,6 @@ export type QueryEventArgs = { }; -export type QueryEventVolunteersByEventArgs = { - id: Scalars['ID']['input']; -}; - - export type QueryEventsByOrganizationArgs = { id?: InputMaybe; orderBy?: InputMaybe; @@ -2368,6 +2427,7 @@ export type QueryEventsByOrganizationConnectionArgs = { first?: InputMaybe; orderBy?: InputMaybe; skip?: InputMaybe; + upcomingOnly?: InputMaybe; where?: InputMaybe; }; @@ -2429,7 +2489,14 @@ export type QueryGetEventInvitesByUserIdArgs = { export type QueryGetEventVolunteerGroupsArgs = { - where?: InputMaybe; + orderBy?: InputMaybe; + where: EventVolunteerGroupWhereInput; +}; + + +export type QueryGetEventVolunteersArgs = { + orderBy?: InputMaybe; + where: EventVolunteerWhereInput; }; @@ -2478,6 +2545,18 @@ export type QueryGetVenueByOrgIdArgs = { }; +export type QueryGetVolunteerMembershipArgs = { + orderBy?: InputMaybe; + where: VolunteerMembershipWhereInput; +}; + + +export type QueryGetVolunteerRanksArgs = { + orgId: Scalars['ID']['input']; + where: VolunteerRankWhereInput; +}; + + export type QueryGetlanguageArgs = { lang_code: Scalars['String']['input']; }; @@ -2703,8 +2782,9 @@ export type UpdateActionItemCategoryInput = { }; export type UpdateActionItemInput = { - allotedHours?: InputMaybe; + allottedHours?: InputMaybe; assigneeId?: InputMaybe; + assigneeType?: InputMaybe; completionDate?: InputMaybe; dueDate?: InputMaybe; isCompleted?: InputMaybe; @@ -2775,16 +2855,16 @@ export type UpdateEventInput = { }; export type UpdateEventVolunteerGroupInput = { - eventId?: InputMaybe; + description?: InputMaybe; + eventId: Scalars['ID']['input']; name?: InputMaybe; volunteersRequired?: InputMaybe; }; export type UpdateEventVolunteerInput = { - eventId?: InputMaybe; - isAssigned?: InputMaybe; - isInvited?: InputMaybe; - response?: InputMaybe; + assignments?: InputMaybe>>; + hasAccepted?: InputMaybe; + isPublic?: InputMaybe; }; export type UpdateFundCampaignInput = { @@ -3160,6 +3240,54 @@ export type VenueWhereInput = { name_starts_with?: InputMaybe; }; +export type VolunteerMembership = { + __typename?: 'VolunteerMembership'; + _id: Scalars['ID']['output']; + createdAt: Scalars['DateTime']['output']; + createdBy?: Maybe; + event: Event; + group?: Maybe; + status: Scalars['String']['output']; + updatedAt: Scalars['DateTime']['output']; + updatedBy?: Maybe; + volunteer: EventVolunteer; +}; + +export type VolunteerMembershipInput = { + event: Scalars['ID']['input']; + group?: InputMaybe; + status: Scalars['String']['input']; + userId: Scalars['ID']['input']; +}; + +export type VolunteerMembershipOrderByInput = + | 'createdAt_ASC' + | 'createdAt_DESC'; + +export type VolunteerMembershipWhereInput = { + eventId?: InputMaybe; + eventTitle?: InputMaybe; + filter?: InputMaybe; + groupId?: InputMaybe; + status?: InputMaybe; + userId?: InputMaybe; + userName?: InputMaybe; +}; + +export type VolunteerRank = { + __typename?: 'VolunteerRank'; + hoursVolunteered: Scalars['Float']['output']; + rank: Scalars['Int']['output']; + user: User; +}; + +export type VolunteerRankWhereInput = { + limit?: InputMaybe; + nameContains?: InputMaybe; + orderBy: Scalars['String']['input']; + timeFrame: Scalars['String']['input']; +}; + export type WeekDays = | 'FRIDAY' | 'MONDAY' @@ -3332,9 +3460,12 @@ export type ResolversTypes = { EventVolunteer: ResolverTypeWrapper; EventVolunteerGroup: ResolverTypeWrapper; EventVolunteerGroupInput: EventVolunteerGroupInput; + EventVolunteerGroupOrderByInput: EventVolunteerGroupOrderByInput; EventVolunteerGroupWhereInput: EventVolunteerGroupWhereInput; EventVolunteerInput: EventVolunteerInput; EventVolunteerResponse: EventVolunteerResponse; + EventVolunteerWhereInput: EventVolunteerWhereInput; + EventVolunteersOrderByInput: EventVolunteersOrderByInput; EventWhereInput: EventWhereInput; ExtendSession: ResolverTypeWrapper; Feedback: ResolverTypeWrapper; @@ -3353,6 +3484,7 @@ export type ResolversTypes = { FundraisingCampaignPledge: ResolverTypeWrapper; Gender: Gender; Group: ResolverTypeWrapper; + HoursHistory: ResolverTypeWrapper; ID: ResolverTypeWrapper; Int: ResolverTypeWrapper; InvalidCursor: ResolverTypeWrapper; @@ -3474,6 +3606,12 @@ export type ResolversTypes = { VenueInput: VenueInput; VenueOrderByInput: VenueOrderByInput; VenueWhereInput: VenueWhereInput; + VolunteerMembership: ResolverTypeWrapper; + VolunteerMembershipInput: VolunteerMembershipInput; + VolunteerMembershipOrderByInput: VolunteerMembershipOrderByInput; + VolunteerMembershipWhereInput: VolunteerMembershipWhereInput; + VolunteerRank: ResolverTypeWrapper & { user: ResolversTypes['User'] }>; + VolunteerRankWhereInput: VolunteerRankWhereInput; WeekDays: WeekDays; chatInput: ChatInput; createUserFamilyInput: CreateUserFamilyInput; @@ -3546,6 +3684,7 @@ export type ResolversParentTypes = { EventVolunteerGroupInput: EventVolunteerGroupInput; EventVolunteerGroupWhereInput: EventVolunteerGroupWhereInput; EventVolunteerInput: EventVolunteerInput; + EventVolunteerWhereInput: EventVolunteerWhereInput; EventWhereInput: EventWhereInput; ExtendSession: ExtendSession; Feedback: InterfaceFeedbackModel; @@ -3561,6 +3700,7 @@ export type ResolversParentTypes = { FundraisingCampaign: InterfaceFundraisingCampaignModel; FundraisingCampaignPledge: InterfaceFundraisingCampaignPledgesModel; Group: InterfaceGroupModel; + HoursHistory: HoursHistory; ID: Scalars['ID']['output']; Int: Scalars['Int']['output']; InvalidCursor: InvalidCursor; @@ -3669,6 +3809,11 @@ export type ResolversParentTypes = { Venue: InterfaceVenueModel; VenueInput: VenueInput; VenueWhereInput: VenueWhereInput; + VolunteerMembership: InterfaceVolunteerMembershipModel; + VolunteerMembershipInput: VolunteerMembershipInput; + VolunteerMembershipWhereInput: VolunteerMembershipWhereInput; + VolunteerRank: Omit & { user: ResolversParentTypes['User'] }; + VolunteerRankWhereInput: VolunteerRankWhereInput; chatInput: ChatInput; createUserFamilyInput: CreateUserFamilyInput; }; @@ -3686,8 +3831,11 @@ export type RoleDirectiveResolver = { _id?: Resolver; actionItemCategory?: Resolver, ParentType, ContextType>; - allotedHours?: Resolver, ParentType, ContextType>; - assignee?: Resolver, ParentType, ContextType>; + allottedHours?: Resolver, ParentType, ContextType>; + assignee?: Resolver, ParentType, ContextType>; + assigneeGroup?: Resolver, ParentType, ContextType>; + assigneeType?: Resolver; + assigneeUser?: Resolver, ParentType, ContextType>; assigner?: Resolver, ParentType, ContextType>; assignmentDate?: Resolver; completionDate?: Resolver; @@ -4039,6 +4187,8 @@ export type EventResolvers, ParentType, ContextType>; title?: Resolver; updatedAt?: Resolver; + volunteerGroups?: Resolver>>, ParentType, ContextType>; + volunteers?: Resolver>>, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -4059,13 +4209,15 @@ export type EventAttendeeResolvers = { _id?: Resolver; + assignments?: Resolver>>, ParentType, ContextType>; createdAt?: Resolver; creator?: Resolver, ParentType, ContextType>; event?: Resolver, ParentType, ContextType>; - group?: Resolver, ParentType, ContextType>; - isAssigned?: Resolver, ParentType, ContextType>; - isInvited?: Resolver, ParentType, ContextType>; - response?: Resolver, ParentType, ContextType>; + groups?: Resolver>>, ParentType, ContextType>; + hasAccepted?: Resolver; + hoursHistory?: Resolver>>, ParentType, ContextType>; + hoursVolunteered?: Resolver; + isPublic?: Resolver; updatedAt?: Resolver; user?: Resolver; __isTypeOf?: IsTypeOfResolverFn; @@ -4073,8 +4225,10 @@ export type EventVolunteerResolvers = { _id?: Resolver; + assignments?: Resolver>>, ParentType, ContextType>; createdAt?: Resolver; creator?: Resolver, ParentType, ContextType>; + description?: Resolver, ParentType, ContextType>; event?: Resolver, ParentType, ContextType>; leader?: Resolver; name?: Resolver, ParentType, ContextType>; @@ -4158,6 +4312,12 @@ export type GroupResolvers; }; +export type HoursHistoryResolvers = { + date?: Resolver; + hours?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type InvalidCursorResolvers = { message?: Resolver; path?: Resolver, ParentType, ContextType>; @@ -4286,6 +4446,7 @@ export type MutationResolvers>; createUserTag?: Resolver, ParentType, ContextType, RequireFields>; createVenue?: Resolver, ParentType, ContextType, RequireFields>; + createVolunteerMembership?: Resolver>; deleteAdvertisement?: Resolver, ParentType, ContextType, RequireFields>; deleteAgendaCategory?: Resolver>; deleteDonationById?: Resolver>; @@ -4350,7 +4511,7 @@ export type MutationResolvers>; updateEvent?: Resolver>; updateEventVolunteer?: Resolver>; - updateEventVolunteerGroup?: Resolver>; + updateEventVolunteerGroup?: Resolver>; updateFund?: Resolver>; updateFundraisingCampaign?: Resolver>; updateFundraisingCampaignPledge?: Resolver>; @@ -4364,6 +4525,7 @@ export type MutationResolvers>; updateUserRoleInOrganization?: Resolver>; updateUserTag?: Resolver, ParentType, ContextType, RequireFields>; + updateVolunteerMembership?: Resolver>; }; export type NoteResolvers = { @@ -4515,6 +4677,7 @@ export type QueryResolvers>>, ParentType, ContextType, RequireFields>; actionItemsByEvent?: Resolver>>, ParentType, ContextType, RequireFields>; actionItemsByOrganization?: Resolver>>, ParentType, ContextType, RequireFields>; + actionItemsByUser?: Resolver>>, ParentType, ContextType, RequireFields>; adminPlugin?: Resolver>>, ParentType, ContextType, RequireFields>; advertisementsConnection?: Resolver, ParentType, ContextType, Partial>; agendaCategory?: Resolver>; @@ -4527,7 +4690,6 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; customFieldsByOrganization?: Resolver>>, ParentType, ContextType, RequireFields>; event?: Resolver, ParentType, ContextType, RequireFields>; - eventVolunteersByEvent?: Resolver>>, ParentType, ContextType, RequireFields>; eventsByOrganization?: Resolver>>, ParentType, ContextType, Partial>; eventsByOrganizationConnection?: Resolver, ParentType, ContextType, Partial>; fundsByOrganization?: Resolver>>, ParentType, ContextType, RequireFields>; @@ -4542,7 +4704,8 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; getEventAttendeesByEventId?: Resolver>>, ParentType, ContextType, RequireFields>; getEventInvitesByUserId?: Resolver, ParentType, ContextType, RequireFields>; - getEventVolunteerGroups?: Resolver>, ParentType, ContextType, Partial>; + getEventVolunteerGroups?: Resolver>, ParentType, ContextType, RequireFields>; + getEventVolunteers?: Resolver>, ParentType, ContextType, RequireFields>; getFundById?: Resolver>; getFundraisingCampaignPledgeById?: Resolver>; getFundraisingCampaigns?: Resolver>, ParentType, ContextType, Partial>; @@ -4551,6 +4714,8 @@ export type QueryResolvers>>, ParentType, ContextType>; getUserTag?: Resolver, ParentType, ContextType, RequireFields>; getVenueByOrgId?: Resolver>>, ParentType, ContextType, RequireFields>; + getVolunteerMembership?: Resolver>, ParentType, ContextType, RequireFields>; + getVolunteerRanks?: Resolver>, ParentType, ContextType, RequireFields>; getlanguage?: Resolver>>, ParentType, ContextType, RequireFields>; hasSubmittedFeedback?: Resolver, ParentType, ContextType, RequireFields>; isSampleOrganization?: Resolver>; @@ -4766,6 +4931,26 @@ export type VenueResolvers; }; +export type VolunteerMembershipResolvers = { + _id?: Resolver; + createdAt?: Resolver; + createdBy?: Resolver, ParentType, ContextType>; + event?: Resolver; + group?: Resolver, ParentType, ContextType>; + status?: Resolver; + updatedAt?: Resolver; + updatedBy?: Resolver, ParentType, ContextType>; + volunteer?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type VolunteerRankResolvers = { + hoursVolunteered?: Resolver; + rank?: Resolver; + user?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type Resolvers = { ActionItem?: ActionItemResolvers; ActionItemCategory?: ActionItemCategoryResolvers; @@ -4817,6 +5002,7 @@ export type Resolvers = { FundraisingCampaign?: FundraisingCampaignResolvers; FundraisingCampaignPledge?: FundraisingCampaignPledgeResolvers; Group?: GroupResolvers; + HoursHistory?: HoursHistoryResolvers; InvalidCursor?: InvalidCursorResolvers; JSON?: GraphQLScalarType; Language?: LanguageResolvers; @@ -4873,6 +5059,8 @@ export type Resolvers = { UsersConnection?: UsersConnectionResolvers; UsersConnectionEdge?: UsersConnectionEdgeResolvers; Venue?: VenueResolvers; + VolunteerMembership?: VolunteerMembershipResolvers; + VolunteerRank?: VolunteerRankResolvers; }; export type DirectiveResolvers = { diff --git a/src/utilities/adminCheck.ts b/src/utilities/adminCheck.ts index 3d49abcd83..8f1b7ac80d 100644 --- a/src/utilities/adminCheck.ts +++ b/src/utilities/adminCheck.ts @@ -12,12 +12,14 @@ import { AppUserProfile } from "../models"; * This is a utility method. * @param userId - The ID of the current user. It can be a string or a Types.ObjectId. * @param organization - The organization data of `InterfaceOrganization` type. + * @param throwError - A boolean value to determine if the function should throw an error. Default is `true`. * @returns `True` or `False`. */ export const adminCheck = async ( userId: string | Types.ObjectId, organization: InterfaceOrganization, -): Promise => { + throwError: boolean = true, +): Promise => { /** * Check if the user is listed as an admin in the organization. * Compares the user ID with the admin IDs in the organization. @@ -55,10 +57,15 @@ export const adminCheck = async ( * If the user is neither an organization admin nor a super admin, throw an UnauthorizedError. */ if (!userIsOrganizationAdmin && !isUserSuperAdmin) { - throw new errors.UnauthorizedError( - requestContext.translate(`${USER_NOT_AUTHORIZED_ADMIN.MESSAGE}`), - USER_NOT_AUTHORIZED_ADMIN.CODE, - USER_NOT_AUTHORIZED_ADMIN.PARAM, - ); + if (throwError) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ADMIN.MESSAGE), + USER_NOT_AUTHORIZED_ADMIN.CODE, + USER_NOT_AUTHORIZED_ADMIN.PARAM, + ); + } else { + return false; + } } + return true; }; diff --git a/src/utilities/checks.ts b/src/utilities/checks.ts new file mode 100644 index 0000000000..bac1389537 --- /dev/null +++ b/src/utilities/checks.ts @@ -0,0 +1,153 @@ +import { + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, + USER_NOT_FOUND_ERROR, +} from "../constants"; +import { errors, requestContext } from "../libraries"; +import type { + InterfaceAppUserProfile, + InterfaceEventVolunteer, + InterfaceEventVolunteerGroup, + InterfaceUser, + InterfaceVolunteerMembership, +} from "../models"; +import { + AppUserProfile, + EventVolunteer, + EventVolunteerGroup, + User, + VolunteerMembership, +} from "../models"; +import { cacheAppUserProfile } from "../services/AppUserProfileCache/cacheAppUserProfile"; +import { findAppUserProfileCache } from "../services/AppUserProfileCache/findAppUserProfileCache"; +import { cacheUsers } from "../services/UserCache/cacheUser"; +import { findUserInCache } from "../services/UserCache/findUserInCache"; + +/** + * This function checks if the user exists. + * @param userId - user id + * @returns User + */ + +export const checkUserExists = async ( + userId: string, +): Promise => { + let currentUser: InterfaceUser | null; + const userFoundInCache = await findUserInCache([userId]); + currentUser = userFoundInCache[0]; + + if (currentUser === null) { + currentUser = await User.findById(userId).lean(); + if (currentUser !== null) await cacheUsers([currentUser]); + } + + if (!currentUser) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM, + ); + } + return currentUser; +}; + +/** + * This function checks if the user has an app profile. + * @param user - user object + * @returns AppUserProfile + */ +export const checkAppUserProfileExists = async ( + user: InterfaceUser, +): Promise => { + let currentUserAppProfile: InterfaceAppUserProfile | null; + const appUserProfileFoundInCache = await findAppUserProfileCache([ + user.appUserProfileId?.toString(), + ]); + currentUserAppProfile = appUserProfileFoundInCache[0]; + if (currentUserAppProfile === null) { + currentUserAppProfile = await AppUserProfile.findOne({ + userId: user._id, + }).lean(); + if (currentUserAppProfile !== null) { + await cacheAppUserProfile([currentUserAppProfile]); + } + } + if (!currentUserAppProfile) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM, + ); + } + return currentUserAppProfile; +}; + +/** + * This function checks if the event volunteer exists. + * @param volunteerId - event volunteer id + * @returns EventVolunteer + */ +export const checkEventVolunteerExists = async ( + volunteerId: string, +): Promise => { + const volunteer = await EventVolunteer.findById(volunteerId); + + if (!volunteer) { + throw new errors.NotFoundError( + requestContext.translate(EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE), + EVENT_VOLUNTEER_NOT_FOUND_ERROR.CODE, + EVENT_VOLUNTEER_NOT_FOUND_ERROR.PARAM, + ); + } + + return volunteer; +}; + +/** + * This function checks if the volunteer group exists. + * @param groupId - event volunteer group id + * @returns EventVolunteerGroup + */ + +export const checkVolunteerGroupExists = async ( + groupId: string, +): Promise => { + const volunteerGroup = await EventVolunteerGroup.findOne({ + _id: groupId, + }); + + if (!volunteerGroup) { + throw new errors.NotFoundError( + requestContext.translate(EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE), + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.CODE, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.PARAM, + ); + } + return volunteerGroup; +}; + +/** + * This function checks if the volunteerMembership exists. + * @param membershipId - id + * @returns VolunteerMembership + */ +export const checkVolunteerMembershipExists = async ( + membershipId: string, +): Promise => { + const volunteerMembership = await VolunteerMembership.findOne({ + _id: membershipId, + }); + + if (!volunteerMembership) { + throw new errors.NotFoundError( + requestContext.translate( + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR.MESSAGE, + ), + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR.CODE, + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR.PARAM, + ); + } + return volunteerMembership; +}; diff --git a/tests/helpers/actionItem.ts b/tests/helpers/actionItem.ts index 86eab57629..b1897f717e 100644 --- a/tests/helpers/actionItem.ts +++ b/tests/helpers/actionItem.ts @@ -35,6 +35,7 @@ export const createTestActionItem = async (): Promise< const testActionItem = await ActionItem.create({ creator: testUser?._id, assignee: randomUser?._id, + assigneeType: "EventVolunteer", assigner: testUser?._id, actionItemCategory: testCategory?._id, organization: testOrganization?._id, @@ -59,6 +60,7 @@ export const createNewTestActionItem = async ({ const newTestActionItem = await ActionItem.create({ creator: currUserId, assignee: assignedUserId, + assigneeType: "EventVolunteer", assigner: currUserId, actionItemCategory: actionItemCategoryId, organization: organizationId, @@ -82,6 +84,7 @@ export const createTestActionItems = async (): Promise< const testActionItem1 = await ActionItem.create({ creator: testUser?._id, assignee: randomUser?._id, + assigneeType: "EventVolunteer", assigner: testUser?._id, actionItemCategory: testCategory?._id, organization: testOrganization?._id, @@ -91,6 +94,7 @@ export const createTestActionItems = async (): Promise< const testActionItem2 = await ActionItem.create({ creator: testUser?._id, assignee: randomUser?._id, + assigneeType: "EventVolunteer", assigner: testUser?._id, actionItemCategory: testCategory?._id, organization: testOrganization?._id, @@ -100,6 +104,7 @@ export const createTestActionItems = async (): Promise< await ActionItem.create({ creator: testUser?._id, assignee: randomUser?._id, + assigneeType: "EventVolunteer", assigner: testUser?._id, actionItemCategory: testCategory2?._id, organization: testOrganization?._id, diff --git a/tests/helpers/events.ts b/tests/helpers/events.ts index 75298ee74a..13130906a8 100644 --- a/tests/helpers/events.ts +++ b/tests/helpers/events.ts @@ -1,6 +1,5 @@ import type { Document } from "mongoose"; import { nanoid } from "nanoid"; -import { EventVolunteerResponse } from "../../src/constants"; import type { InterfaceEvent, InterfaceEventVolunteer, @@ -132,12 +131,11 @@ export const createTestEventAndVolunteer = async (): Promise< const [creatorUser, , testEvent] = await createTestEvent(); const volunteerUser = await createTestUser(); const testEventVolunteer = await EventVolunteer.create({ - userId: volunteerUser?._id, - eventId: testEvent?._id, - isInvited: true, - isAssigned: false, - creatorId: creatorUser?._id, - response: EventVolunteerResponse.NO, + user: volunteerUser?._id, + event: testEvent?._id, + creator: creatorUser?._id, + hasAccepted: false, + isPublic: false, }); return [volunteerUser, creatorUser, testEvent, testEventVolunteer]; @@ -157,9 +155,9 @@ export const createTestEventVolunteerGroup = async (): Promise< const testEventVolunteerGroup = await EventVolunteerGroup.create({ name: "testEventVolunteerGroup", volunteersRequired: 1, - eventId: testEvent?._id, - creatorId: creatorUser?._id, - leaderId: creatorUser?._id, + event: testEvent?._id, + creator: creatorUser?._id, + leader: creatorUser?._id, volunteers: [testEventVolunteer?._id], }); diff --git a/tests/helpers/volunteers.ts b/tests/helpers/volunteers.ts new file mode 100644 index 0000000000..026dc4313a --- /dev/null +++ b/tests/helpers/volunteers.ts @@ -0,0 +1,299 @@ +import type { + InterfaceEventVolunteer, + InterfaceEventVolunteerGroup, + InterfaceVolunteerMembership, +} from "../../src/models"; +import { + ActionItem, + ActionItemCategory, + Event, + EventVolunteer, + EventVolunteerGroup, +} from "../../src/models"; +import type { Document } from "mongoose"; +import { + createTestUser, + createTestUserAndOrganization, + type TestOrganizationType, + type TestUserType, +} from "./userAndOrg"; +import { nanoid } from "nanoid"; +import type { TestEventType } from "./events"; +import type { TestActionItemType } from "./actionItem"; + +export type TestVolunteerType = InterfaceEventVolunteer & Document; +export type TestVolunteerGroupType = InterfaceEventVolunteerGroup & Document; +export type TestVolunteerMembership = InterfaceVolunteerMembership & Document; + +export const createTestVolunteerAndGroup = async (): Promise< + [ + TestUserType, + TestOrganizationType, + TestEventType, + TestVolunteerType, + TestVolunteerGroupType, + ] +> => { + const [testUser, testOrganization] = await createTestUserAndOrganization(); + const randomUser = await createTestUser(); + + const testEvent = await Event.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + allDay: true, + startDate: new Date(), + recurring: false, + isPublic: true, + isRegisterable: true, + creatorId: testUser?._id, + admins: [testUser?._id], + organization: testOrganization?._id, + volunteers: [], + volunteerGroups: [], + }); + + const testVolunteer = await EventVolunteer.create({ + creator: randomUser?._id, + event: testEvent?._id, + user: testUser?._id, + groups: [], + assignments: [], + }); + + // create a volunteer group with testVolunteer as a member & leader + const testVolunteerGroup = await EventVolunteerGroup.create({ + creator: randomUser?._id, + event: testEvent?._id, + volunteers: [testVolunteer?._id], + leader: testVolunteer?._id, + assignments: [], + name: "Test Volunteer Group 1", + }); + + // add volunteer & group to event + await Event.updateOne( + { + _id: testEvent?._id, + }, + { + $push: { + volunteers: testVolunteer?._id, + volunteerGroups: testVolunteerGroup?._id, + }, + }, + ); + + // add group to volunteer + await EventVolunteer.updateOne( + { + _id: testVolunteer?._id, + }, + { + $push: { + groups: testVolunteerGroup?._id, + }, + }, + ); + + return [ + testUser, + testOrganization, + testEvent, + testVolunteer, + testVolunteerGroup, + ]; +}; + +export const createVolunteerAndActions = async (): Promise< + [ + TestOrganizationType, + TestEventType, + TestUserType, + TestUserType, + TestVolunteerType, + TestVolunteerType, + TestVolunteerGroupType, + TestActionItemType, + TestActionItemType, + ] +> => { + const [testUser, testOrganization] = await createTestUserAndOrganization(); + const testUser2 = await createTestUser(); + + const randomUser = await createTestUser(); + + const testEvent = await Event.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + allDay: true, + startDate: new Date(), + recurring: false, + isPublic: true, + isRegisterable: true, + creatorId: testUser?._id, + admins: [testUser?._id], + organization: testOrganization?._id, + volunteers: [], + volunteerGroups: [], + }); + + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); + const twoWeeksAgo = new Date(today); + twoWeeksAgo.setDate(today.getDate() - 14); + const twoMonthsAgo = new Date(today); + twoMonthsAgo.setMonth(today.getMonth() - 2); + const twoYearsAgo = new Date(today); + twoYearsAgo.setFullYear(today.getFullYear() - 2); + + const testVolunteer1 = await EventVolunteer.create({ + creator: randomUser?._id, + event: testEvent?._id, + user: testUser?._id, + groups: [], + assignments: [], + hasAccepted: true, + hoursVolunteered: 10, + hoursHistory: [ + { + hours: 2, + date: yesterday, + }, + { + hours: 4, + date: twoWeeksAgo, + }, + { + hours: 2, + date: twoMonthsAgo, + }, + { + hours: 2, + date: twoYearsAgo, + }, + ], + }); + + const testVolunteer2 = await EventVolunteer.create({ + creator: randomUser?._id, + event: testEvent?._id, + user: testUser2?._id, + groups: [], + assignments: [], + hasAccepted: true, + hoursVolunteered: 8, + hoursHistory: [ + { + hours: 1, + date: yesterday, + }, + { + hours: 2, + date: twoWeeksAgo, + }, + { + hours: 3, + date: twoMonthsAgo, + }, + { + hours: 2, + date: twoYearsAgo, + }, + ], + }); + + // create a volunteer group with testVolunteer1 as a member & leader + const testVolunteerGroup = await EventVolunteerGroup.create({ + creator: randomUser?._id, + event: testEvent?._id, + volunteers: [testVolunteer1?._id, testVolunteer2?._id], + leader: testUser?._id, + assignments: [], + name: "Test Volunteer Group 1", + }); + + // add volunteer & group to event + await Event.updateOne( + { + _id: testEvent?._id, + }, + { + $addToSet: { + volunteers: { $each: [testVolunteer1?._id, testVolunteer2?._id] }, + volunteerGroups: testVolunteerGroup?._id, + }, + }, + ); + + const testActionItemCategory = await ActionItemCategory.create({ + creatorId: randomUser?._id, + organizationId: testOrganization?._id, + name: "Test Action Item Category 1", + isDisabled: false, + }); + + const testActionItem1 = await ActionItem.create({ + creator: randomUser?._id, + assigner: randomUser?._id, + assignee: testVolunteer1?._id, + assigneeType: "EventVolunteer", + assigneeGroup: null, + assigneeUser: null, + actionItemCategory: testActionItemCategory?._id, + event: testEvent?._id, + organization: testOrganization?._id, + allottedHours: 2, + assignmentDate: new Date(), + dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + isCompleted: false, + }); + + const testActionItem2 = await ActionItem.create({ + creator: randomUser?._id, + assigner: randomUser?._id, + assigneeType: "EventVolunteerGroup", + assigneeGroup: testVolunteerGroup?._id, + assignee: null, + assigneeUser: null, + actionItemCategory: testActionItemCategory?._id, + event: testEvent?._id, + organization: testOrganization?._id, + allottedHours: 4, + assignmentDate: new Date(), + dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 2000), + isCompleted: false, + }); + + await EventVolunteer.findByIdAndUpdate(testVolunteer1?._id, { + $push: { + assignments: testActionItem1?._id, + }, + }); + + await EventVolunteer.updateMany( + { _id: { $in: [testVolunteer1?._id, testVolunteer2?._id] } }, + { + $push: { + groups: testVolunteerGroup?._id, + assignments: testActionItem2?._id, + }, + }, + ); + + await EventVolunteerGroup.findByIdAndUpdate(testVolunteerGroup?._id, { + $addToSet: { assignments: testActionItem2 }, + }); + + return [ + testOrganization, + testEvent, + testUser, + testUser2, + testVolunteer1, + testVolunteer2, + testVolunteerGroup, + testActionItem1, + testActionItem2, + ]; +}; diff --git a/tests/resolvers/Event/actionItems.spec.ts b/tests/resolvers/Event/actionItems.spec.ts index 716f511d96..ad06fbb673 100644 --- a/tests/resolvers/Event/actionItems.spec.ts +++ b/tests/resolvers/Event/actionItems.spec.ts @@ -26,7 +26,7 @@ describe("resolvers -> Organization -> actionItems", () => { const actionItemsPayload = await actionItemsResolver?.(parent, {}, {}); const actionItems = await ActionItem.find({ - eventId: testEvent?._id, + event: testEvent?._id, }).lean(); expect(actionItemsPayload).toEqual(actionItems); diff --git a/tests/resolvers/EventVolunteer/creator.spec.ts b/tests/resolvers/EventVolunteer/creator.spec.ts deleted file mode 100644 index 88e07a8722..0000000000 --- a/tests/resolvers/EventVolunteer/creator.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import "dotenv/config"; -import { creator as creatorResolver } from "../../../src/resolvers/EventVolunteer/creator"; -import { connect, disconnect } from "../../helpers/db"; -import type mongoose from "mongoose"; -import { - beforeAll, - afterAll, - describe, - it, - expect, - beforeEach, - vi, -} from "vitest"; -import type { TestEventVolunteerType } from "../../helpers/events"; -import { createTestEventAndVolunteer } from "../../helpers/events"; -import type { TestUserType } from "../../helpers/userAndOrg"; -import type { InterfaceEventVolunteer } from "../../../src/models"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let testEventVolunteer: TestEventVolunteerType; -let creatorUser: TestUserType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - [, creatorUser, , testEventVolunteer] = await createTestEventAndVolunteer(); -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> EventVolunteer -> creator", () => { - beforeEach(() => { - vi.resetModules(); - }); - it(`returns the correct creator object for parent event volunteer`, async () => { - const parent = testEventVolunteer?.toObject(); - const creatorPayload = await creatorResolver?.( - parent as InterfaceEventVolunteer, - {}, - {}, - ); - - expect(creatorPayload?._id).toEqual(creatorUser?._id); - }); -}); diff --git a/tests/resolvers/EventVolunteer/event.spec.ts b/tests/resolvers/EventVolunteer/event.spec.ts deleted file mode 100644 index 7a0b04ab5b..0000000000 --- a/tests/resolvers/EventVolunteer/event.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import "dotenv/config"; -import { event as eventResolver } from "../../../src/resolvers/EventVolunteer/event"; -import { connect, disconnect } from "../../helpers/db"; -import type mongoose from "mongoose"; -import { beforeAll, afterAll, describe, it, expect } from "vitest"; -import type { - TestEventType, - TestEventVolunteerType, -} from "../../helpers/events"; -import { createTestEventAndVolunteer } from "../../helpers/events"; -import type { InterfaceEventVolunteer } from "../../../src/models"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let testEvent: TestEventType; -let testEventVolunteer: TestEventVolunteerType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - [, , testEvent, testEventVolunteer] = await createTestEventAndVolunteer(); -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> EventVolunteer -> event", () => { - it(`returns the correct event object for parent event volunteer`, async () => { - const parent = testEventVolunteer?.toObject(); - - const eventPayload = await eventResolver?.( - parent as InterfaceEventVolunteer, - {}, - {}, - ); - - expect(eventPayload).toEqual(testEvent?.toObject()); - }); -}); diff --git a/tests/resolvers/EventVolunteer/group.spec.ts b/tests/resolvers/EventVolunteer/group.spec.ts deleted file mode 100644 index 856641b624..0000000000 --- a/tests/resolvers/EventVolunteer/group.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import "dotenv/config"; -import { group as groupResolver } from "../../../src/resolvers/EventVolunteer/group"; -import { connect, disconnect } from "../../helpers/db"; -import type mongoose from "mongoose"; -import { - beforeAll, - afterAll, - describe, - it, - expect, - beforeEach, - vi, -} from "vitest"; -import type { - TestEventType, - TestEventVolunteerType, -} from "../../helpers/events"; -import { createTestEvent } from "../../helpers/events"; -import type { TestUserType } from "../../helpers/userAndOrg"; -import type { InterfaceEventVolunteer } from "../../../src/models"; -import { EventVolunteer, EventVolunteerGroup } from "../../../src/models"; -import type { TestEventVolunteerGroupType } from "../Mutation/createEventVolunteer.spec"; -import { createTestUser } from "../../helpers/user"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let testEventVolunteer: TestEventVolunteerType; -let eventAdminUser: TestUserType; -let testUser: TestUserType; -let testEvent: TestEventType; -let testGroup: TestEventVolunteerGroupType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - [eventAdminUser, , testEvent] = await createTestEvent(); - testUser = await createTestUser(); - testGroup = await EventVolunteerGroup.create({ - name: "test", - creatorId: eventAdminUser?._id, - leaderId: eventAdminUser?._id, - eventId: testEvent?._id, - }); - testEventVolunteer = await EventVolunteer.create({ - eventId: testEvent?._id, - userId: testUser?._id, - creatorId: eventAdminUser?._id, - groupId: testGroup?._id, - isAssigned: false, - isInvited: true, - }); -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> EventVolunteer -> group", () => { - beforeEach(() => { - vi.resetModules(); - }); - it(`returns the correct event volunteer group object for parent event volunteer`, async () => { - const parent = testEventVolunteer?.toObject(); - const groupPayload = await groupResolver?.( - parent as InterfaceEventVolunteer, - {}, - {}, - ); - console.log(groupPayload); - console.log(testGroup); - - expect(groupPayload?._id).toEqual(testGroup?._id); - }); -}); diff --git a/tests/resolvers/EventVolunteer/user.spec.ts b/tests/resolvers/EventVolunteer/user.spec.ts deleted file mode 100644 index 56ac382f6b..0000000000 --- a/tests/resolvers/EventVolunteer/user.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import "dotenv/config"; -import { user as userResolver } from "../../../src/resolvers/EventVolunteer/user"; -import { connect, disconnect } from "../../helpers/db"; -import type mongoose from "mongoose"; -import { - beforeAll, - afterAll, - describe, - it, - expect, - beforeEach, - vi, -} from "vitest"; -import type { TestEventVolunteerType } from "../../helpers/events"; -import { createTestEventAndVolunteer } from "../../helpers/events"; -import type { TestUserType } from "../../helpers/userAndOrg"; -import type { InterfaceEventVolunteer } from "../../../src/models"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let testUser: TestUserType; -let testEventVolunteer: TestEventVolunteerType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - [testUser, , , testEventVolunteer] = await createTestEventAndVolunteer(); -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> EventVolunteer -> user", () => { - beforeEach(() => { - vi.resetModules(); - }); - it(`returns the correct user object for parent event volunteer`, async () => { - const parent = testEventVolunteer?.toObject(); - console.log(testEventVolunteer?.userId); - console.log(testUser?._id); - - const userPayload = await userResolver?.( - parent as InterfaceEventVolunteer, - {}, - {}, - ); - - expect(userPayload).toEqual({ - ...testUser?.toObject(), - updatedAt: expect.anything(), - }); - }); -}); diff --git a/tests/resolvers/EventVolunteerGroup/creator.spec.ts b/tests/resolvers/EventVolunteerGroup/creator.spec.ts deleted file mode 100644 index e9bd6502bf..0000000000 --- a/tests/resolvers/EventVolunteerGroup/creator.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import "dotenv/config"; -import { creator as creatorResolver } from "../../../src/resolvers/EventVolunteerGroup/creator"; -import { connect, disconnect } from "../../helpers/db"; -import type mongoose from "mongoose"; -import { beforeAll, afterAll, describe, it, expect } from "vitest"; -import type { TestEventType } from "../../helpers/events"; -import { createTestEvent } from "../../helpers/events"; -import type { InterfaceEventVolunteerGroup } from "../../../src/models"; -import { EventVolunteerGroup } from "../../../src/models"; -import type { TestUserType } from "../../helpers/user"; -import type { TestEventVolunteerGroupType } from "../Mutation/createEventVolunteer.spec"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let eventAdminUser: TestUserType; -let testEvent: TestEventType; -let testGroup: TestEventVolunteerGroupType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - [eventAdminUser, , testEvent] = await createTestEvent(); - testGroup = await EventVolunteerGroup.create({ - name: "test", - creatorId: eventAdminUser?._id, - leaderId: eventAdminUser?._id, - eventId: testEvent?._id, - }); -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> EventVolunteer -> creator", () => { - it(`returns the correct creator user object for parent event volunteer group`, async () => { - const parent = testGroup?.toObject(); - - const creatorPayload = await creatorResolver?.( - parent as InterfaceEventVolunteerGroup, - {}, - {}, - ); - - expect(creatorPayload?._id).toEqual(eventAdminUser?._id); - }); -}); diff --git a/tests/resolvers/EventVolunteerGroup/event.spec.ts b/tests/resolvers/EventVolunteerGroup/event.spec.ts deleted file mode 100644 index 5cfaeb7450..0000000000 --- a/tests/resolvers/EventVolunteerGroup/event.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import "dotenv/config"; -import { event as eventResolver } from "../../../src/resolvers/EventVolunteerGroup/event"; -import { connect, disconnect } from "../../helpers/db"; -import type mongoose from "mongoose"; -import { beforeAll, afterAll, describe, it, expect } from "vitest"; -import type { TestEventType } from "../../helpers/events"; -import { createTestEvent } from "../../helpers/events"; -import type { InterfaceEventVolunteerGroup } from "../../../src/models"; -import { EventVolunteerGroup } from "../../../src/models"; -import type { TestUserType } from "../../helpers/user"; -import type { TestEventVolunteerGroupType } from "../Mutation/createEventVolunteer.spec"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let eventAdminUser: TestUserType; -let testEvent: TestEventType; -let testGroup: TestEventVolunteerGroupType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - [eventAdminUser, , testEvent] = await createTestEvent(); - testGroup = await EventVolunteerGroup.create({ - name: "test", - creatorId: eventAdminUser?._id, - leaderId: eventAdminUser?._id, - eventId: testEvent?._id, - }); -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> EventVolunteer -> event", () => { - it(`returns the correct event object for parent event volunteer group`, async () => { - const parent = testGroup?.toObject(); - - const eventPayload = await eventResolver?.( - parent as InterfaceEventVolunteerGroup, - {}, - {}, - ); - - expect(eventPayload).toEqual(testEvent?.toObject()); - }); -}); diff --git a/tests/resolvers/EventVolunteerGroup/leader.spec.ts b/tests/resolvers/EventVolunteerGroup/leader.spec.ts deleted file mode 100644 index 07481e41b5..0000000000 --- a/tests/resolvers/EventVolunteerGroup/leader.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import "dotenv/config"; -import { leader as leaderResolver } from "../../../src/resolvers/EventVolunteerGroup/leader"; -import { connect, disconnect } from "../../helpers/db"; -import type mongoose from "mongoose"; -import { beforeAll, afterAll, describe, it, expect } from "vitest"; -import type { TestEventType } from "../../helpers/events"; -import { createTestEvent } from "../../helpers/events"; -import type { InterfaceEventVolunteerGroup } from "../../../src/models"; -import { EventVolunteerGroup } from "../../../src/models"; -import type { TestUserType } from "../../helpers/user"; -import type { TestEventVolunteerGroupType } from "../Mutation/createEventVolunteer.spec"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let eventAdminUser: TestUserType; -let testEvent: TestEventType; -let testGroup: TestEventVolunteerGroupType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - [eventAdminUser, , testEvent] = await createTestEvent(); - testGroup = await EventVolunteerGroup.create({ - name: "test", - creatorId: eventAdminUser?._id, - leaderId: eventAdminUser?._id, - eventId: testEvent?._id, - }); -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> EventVolunteer -> leader", () => { - it(`returns the correct leader user object for parent event volunteer group`, async () => { - const parent = testGroup?.toObject(); - - const leaderPayload = await leaderResolver?.( - parent as InterfaceEventVolunteerGroup, - {}, - {}, - ); - - expect(leaderPayload?._id).toEqual(eventAdminUser?._id); - }); -}); diff --git a/tests/resolvers/Mutation/createActionItem.spec.ts b/tests/resolvers/Mutation/createActionItem.spec.ts index e1093a4975..c8b35b5285 100644 --- a/tests/resolvers/Mutation/createActionItem.spec.ts +++ b/tests/resolvers/Mutation/createActionItem.spec.ts @@ -1,85 +1,57 @@ -import "dotenv/config"; import type mongoose from "mongoose"; -import { Types } from "mongoose"; -import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { connect, disconnect } from "../../helpers/db"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import type { + TestEventType, + TestEventVolunteerGroupType, + TestEventVolunteerType, +} from "../../helpers/events"; +import type { TestUserType } from "../../helpers/user"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import type { InterfaceActionItem } from "../../../src/models"; +import { ActionItemCategory } from "../../../src/models"; import { ACTION_ITEM_CATEGORY_IS_DISABLED, ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR, EVENT_NOT_FOUND_ERROR, - USER_NOT_AUTHORIZED_ERROR, - USER_NOT_FOUND_ERROR, - USER_NOT_MEMBER_FOR_ORGANIZATION, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_NOT_FOUND_ERROR, } from "../../../src/constants"; -import { createActionItem as createActionItemResolver } from "../../../src/resolvers/Mutation/createActionItem"; -import type { MutationCreateActionItemArgs } from "../../../src/types/generatedGraphQLTypes"; -import { connect, disconnect } from "../../helpers/db"; -import type { - TestOrganizationType, - TestUserType, -} from "../../helpers/userAndOrg"; -import { createTestUser } from "../../helpers/userAndOrg"; - -import { nanoid } from "nanoid"; -import { - ActionItemCategory, - AppUserProfile, - Event, - User, -} from "../../../src/models"; -import type { TestActionItemCategoryType } from "../../helpers/actionItemCategory"; -import { createTestCategory } from "../../helpers/actionItemCategory"; -import type { TestEventType } from "../../helpers/events"; +import { requestContext } from "../../../src/libraries"; +import { createActionItem } from "../../../src/resolvers/Mutation/createActionItem"; +import type { TestOrganizationType } from "../../helpers/userAndOrg"; +import type { TestActionItemType } from "../../helpers/actionItem"; -let randomUser: TestUserType; -let randomUser2: TestUserType; -// let superAdminTestUserAppProfile: TestAppUserProfileType; -let testUser: TestUserType; +let MONGOOSE_INSTANCE: typeof mongoose; let testOrganization: TestOrganizationType; -let testCategory: TestActionItemCategoryType; -let testDisabledCategory: TestActionItemCategoryType; let testEvent: TestEventType; -let MONGOOSE_INSTANCE: typeof mongoose; +let testUser1: TestUserType; +let testActionItem1: TestActionItemType; +let testEventVolunteer1: TestEventVolunteerType; +let testEventVolunteerGroup: TestEventVolunteerGroupType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); - const { requestContext } = await import("../../../src/libraries"); vi.spyOn(requestContext, "translate").mockImplementation( (message) => message, ); - - randomUser = await createTestUser(); - randomUser2 = await createTestUser(); - - await AppUserProfile.updateOne( - { - userId: randomUser2?._id, - }, - { - isSuperAdmin: true, - }, - ); - - [testUser, testOrganization, testCategory] = await createTestCategory(); - - testDisabledCategory = await ActionItemCategory.create({ - name: "a disabled category", - organizationId: testOrganization?._id, - isDisabled: true, - creatorId: testUser?._id, - }); - - testEvent = await Event.create({ - title: `title${nanoid().toLowerCase()}`, - description: `description${nanoid().toLowerCase()}`, - allDay: true, - startDate: new Date(), - recurring: false, - isPublic: true, - isRegisterable: true, - creatorId: randomUser?._id, - admins: [randomUser?._id], - organization: testOrganization?._id, - }); + const [ + organization, + event, + user1, + , + volunteer1, + , + volunteerGroup, + actionItem1, + ] = await createVolunteerAndActions(); + + testOrganization = organization; + testEvent = event; + testUser1 = user1; + testEventVolunteer1 = volunteer1; + testEventVolunteerGroup = volunteerGroup; + testActionItem1 = actionItem1; }); afterAll(async () => { @@ -87,255 +59,161 @@ afterAll(async () => { }); describe("resolvers -> Mutation -> createActionItem", () => { - it(`throws NotFoundError if no user exists with _id === context.userId`, async () => { - try { - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: randomUser?._id, - }, - actionItemCategoryId: testCategory?._id, - }; - - const context = { - userId: new Types.ObjectId().toString(), - }; - - await createActionItemResolver?.({}, args, context); - } catch (error: unknown) { - expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); - } - }); - - it(`throws NotFoundError if no actionItemCategory exists with _id === args.actionItemCategoryId`, async () => { + it(`throws EventVolunteer Not Found`, async () => { try { - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: randomUser?._id, + (await createActionItem?.( + {}, + { + data: { + assigneeId: testEvent?._id, + assigneeType: "EventVolunteer", + }, + actionItemCategoryId: testEventVolunteer1?._id, }, - actionItemCategoryId: new Types.ObjectId().toString(), - }; - - const context = { - userId: testUser?._id, - }; - - await createActionItemResolver?.({}, args, context); + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; } catch (error: unknown) { expect((error as Error).message).toEqual( - ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.MESSAGE, + EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE, ); } }); - it(`throws ConflictError if the actionItemCategory is disabled`, async () => { + it(`throws EventVolunteerGroup Not Found`, async () => { try { - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: randomUser?._id, + (await createActionItem?.( + {}, + { + data: { + assigneeId: testEvent?._id, + assigneeType: "EventVolunteerGroup", + }, + actionItemCategoryId: testEventVolunteer1?._id, }, - actionItemCategoryId: testDisabledCategory._id, - }; - - const context = { - userId: testUser?._id, - }; - - await createActionItemResolver?.({}, args, context); + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; } catch (error: unknown) { expect((error as Error).message).toEqual( - ACTION_ITEM_CATEGORY_IS_DISABLED.MESSAGE, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE, ); } }); - it(`throws NotFoundError if no user exists with _id === args.data.assigneeId`, async () => { + it(`throws ActionItemCategory Not Found`, async () => { try { - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: new Types.ObjectId().toString(), + (await createActionItem?.( + {}, + { + data: { + assigneeId: testEventVolunteer1?._id, + assigneeType: "EventVolunteer", + }, + actionItemCategoryId: testEventVolunteer1?._id, }, - actionItemCategoryId: testCategory?._id, - }; - - const context = { - userId: testUser?._id, - }; - - await createActionItemResolver?.({}, args, context); + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; } catch (error: unknown) { - expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.MESSAGE, + ); } }); - it(`throws NotFoundError new assignee is not a member of the organization`, async () => { + it(`throws ActionItemCategory is Disabled`, async () => { + const disabledCategory = await ActionItemCategory.create({ + creatorId: testUser1?._id, + organizationId: testOrganization?._id, + name: "Disabled Category", + isDisabled: true, + }); try { - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: randomUser?._id, + (await createActionItem?.( + {}, + { + data: { + assigneeId: testEventVolunteer1?._id, + assigneeType: "EventVolunteer", + }, + actionItemCategoryId: disabledCategory?._id.toString(), }, - actionItemCategoryId: testCategory?._id, - }; - - const context = { - userId: testUser?._id, - }; - - await createActionItemResolver?.({}, args, context); + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; } catch (error: unknown) { expect((error as Error).message).toEqual( - USER_NOT_MEMBER_FOR_ORGANIZATION.MESSAGE, + ACTION_ITEM_CATEGORY_IS_DISABLED.MESSAGE, ); } }); - it(`throws NotFoundError if no event exists with _id === args.data.eventId`, async () => { - await User.findOneAndUpdate( - { - _id: randomUser?._id, - }, - { - $push: { joinedOrganizations: testOrganization?._id }, - }, - ); - + it(`throws Event Not Found`, async () => { try { - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: randomUser?._id, - eventId: new Types.ObjectId().toString(), + (await createActionItem?.( + {}, + { + data: { + assigneeId: testEventVolunteer1?._id, + assigneeType: "EventVolunteer", + eventId: testUser1?._id.toString(), + }, + actionItemCategoryId: testActionItem1.actionItemCategory.toString(), }, - actionItemCategoryId: testCategory?._id, - }; - - const context = { - userId: randomUser?._id, - }; - - await createActionItemResolver?.({}, args, context); + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; } catch (error: unknown) { expect((error as Error).message).toEqual(EVENT_NOT_FOUND_ERROR.MESSAGE); } }); - it(`throws NotAuthorizedError if the user is not authorized for performing the operation`, async () => { - try { - const args: MutationCreateActionItemArgs = { + it(`Create Action Item (EventVolunteer) `, async () => { + const createdItem = (await createActionItem?.( + {}, + { data: { - assigneeId: randomUser?._id, + assigneeId: testEventVolunteer1?._id, + assigneeType: "EventVolunteer", + eventId: testEvent?._id.toString(), }, - actionItemCategoryId: testCategory?._id, - }; - - const context = { - userId: randomUser?._id, - }; - - await createActionItemResolver?.({}, args, context); - } catch (error: unknown) { - expect((error as Error).message).toEqual( - USER_NOT_AUTHORIZED_ERROR.MESSAGE, - ); - } - }); - - it(`creates the actionItem when user is authorized as an eventAdmin`, async () => { - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: randomUser?._id, - eventId: testEvent?._id.toString() ?? "", + actionItemCategoryId: testActionItem1.actionItemCategory.toString(), }, - actionItemCategoryId: testCategory?._id, - }; - - const context = { - userId: randomUser?._id, - }; - - const createActionItemPayload = await createActionItemResolver?.( - {}, - args, - context, - ); + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; - expect(createActionItemPayload).toEqual( - expect.objectContaining({ - actionItemCategory: testCategory?._id, - }), - ); + expect(createdItem).toBeDefined(); + expect(createdItem.creator).toEqual(testUser1?._id); }); - it(`creates the actionItem when user is authorized as an orgAdmin`, async () => { - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: randomUser?._id, - }, - actionItemCategoryId: testCategory?._id, - }; - - const context = { - userId: testUser?._id, - }; - - const createActionItemPayload = await createActionItemResolver?.( + it(`Create Action Item (EventVolunteerGroup) `, async () => { + const createdItem = (await createActionItem?.( {}, - args, - context, - ); - - expect(createActionItemPayload).toEqual( - expect.objectContaining({ - actionItemCategory: testCategory?._id, - }), - ); - }); - - it(`creates the actionItem when user is authorized as superadmin`, async () => { - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: randomUser?._id, + { + data: { + assigneeId: testEventVolunteerGroup?._id, + assigneeType: "EventVolunteerGroup", + eventId: testEvent?._id.toString(), + }, + actionItemCategoryId: testActionItem1.actionItemCategory.toString(), }, - actionItemCategoryId: testCategory?._id, - }; + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; - const context = { - userId: randomUser2?._id, - }; - // const superAdmin = await AppUserProfile.findOne({ - // userId: randomUser2?._id, - // }); - // console.log(superAdmin) + expect(createdItem).toBeDefined(); + expect(createdItem.creator).toEqual(testUser1?._id); + }); - const createActionItemPayload = await createActionItemResolver?.( + it(`Create Action Item (User) `, async () => { + const createdItem = (await createActionItem?.( {}, - args, - context, - ); - - expect(createActionItemPayload).toEqual( - expect.objectContaining({ - actionItemCategory: testCategory?._id, - }), - ); - }); - it("throws error if the user does not have appUserProfile", async () => { - await AppUserProfile.deleteOne({ - userId: randomUser?._id, - }); - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: randomUser?._id, + { + data: { + assigneeId: testUser1?._id.toString() as string, + assigneeType: "User", + }, + actionItemCategoryId: testActionItem1.actionItemCategory.toString(), }, - actionItemCategoryId: testCategory?._id, - }; - const context = { - userId: randomUser?._id, - }; - try { - await createActionItemResolver?.({}, args, context); - } catch (error: unknown) { - expect((error as Error).message).toEqual( - USER_NOT_AUTHORIZED_ERROR.MESSAGE, - ); - } + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; + + expect(createdItem).toBeDefined(); + expect(createdItem.creator).toEqual(testUser1?._id); }); }); diff --git a/tests/resolvers/Mutation/createEventVolunteer.spec.ts b/tests/resolvers/Mutation/createEventVolunteer.spec.ts index 8d339b4370..7734361eef 100644 --- a/tests/resolvers/Mutation/createEventVolunteer.spec.ts +++ b/tests/resolvers/Mutation/createEventVolunteer.spec.ts @@ -45,10 +45,11 @@ beforeAll(async () => { [eventAdminUser, , testEvent] = await createTestEvent(); testGroup = await EventVolunteerGroup.create({ - creatorId: eventAdminUser?._id, - eventId: testEvent?._id, - leaderId: eventAdminUser?._id, + creator: eventAdminUser?._id, + event: testEvent?._id, + leader: eventAdminUser?._id, name: "Test group", + volunteers: [eventAdminUser?._id, testUser2?._id, testUser1?._id], }); }); @@ -230,22 +231,15 @@ describe("resolvers -> Mutation -> createEventVolunteer", () => { context, ); - const updatedGroup = await EventVolunteerGroup.findOne({ - _id: testGroup?._id, - }); - - expect(updatedGroup?.volunteers.toString()).toEqual( - [createdVolunteer?._id.toString()].toString(), - ); - expect(createdVolunteer).toEqual( expect.objectContaining({ - eventId: new Types.ObjectId(testEvent?.id), - userId: testUser2?._id, - groupId: testGroup?._id, - creatorId: eventAdminUser?._id, - isInvited: true, - isAssigned: false, + event: new Types.ObjectId(testEvent?.id), + user: testUser2?._id, + groups: [], + creator: eventAdminUser?._id, + hasAccepted: false, + isPublic: true, + hoursVolunteered: 0, }), ); }); diff --git a/tests/resolvers/Mutation/createEventVolunteerGroup.spec.ts b/tests/resolvers/Mutation/createEventVolunteerGroup.spec.ts index f41663b29b..5604676eac 100644 --- a/tests/resolvers/Mutation/createEventVolunteerGroup.spec.ts +++ b/tests/resolvers/Mutation/createEventVolunteerGroup.spec.ts @@ -54,6 +54,8 @@ describe("resolvers -> Mutation -> createEventVolunteerGroup", () => { const args: MutationCreateEventVolunteerGroupArgs = { data: { name: "Test group", + leaderId: testUser?._id, + volunteerUserIds: [testUser?._id], eventId: testEvent?._id, }, }; @@ -82,6 +84,8 @@ describe("resolvers -> Mutation -> createEventVolunteerGroup", () => { const args: MutationCreateEventVolunteerGroupArgs = { data: { name: "Test group", + leaderId: testUser?._id, + volunteerUserIds: [testUser?._id], eventId: new Types.ObjectId().toString(), }, }; @@ -111,6 +115,8 @@ describe("resolvers -> Mutation -> createEventVolunteerGroup", () => { const args: MutationCreateEventVolunteerGroupArgs = { data: { name: "Test group", + leaderId: testUser?._id, + volunteerUserIds: [testUser?._id], eventId: testEvent?._id, }, }; @@ -137,6 +143,8 @@ describe("resolvers -> Mutation -> createEventVolunteerGroup", () => { const args: MutationCreateEventVolunteerGroupArgs = { data: { name: "Test group", + leaderId: eventAdminUser?._id, + volunteerUserIds: [testUser?._id], eventId: testEvent?._id, }, }; @@ -163,9 +171,9 @@ describe("resolvers -> Mutation -> createEventVolunteerGroup", () => { expect(createdGroup).toEqual( expect.objectContaining({ name: "Test group", - eventId: new Types.ObjectId(testEvent?.id), - creatorId: eventAdminUser?._id, - leaderId: eventAdminUser?._id, + event: new Types.ObjectId(testEvent?.id), + creator: eventAdminUser?._id, + leader: eventAdminUser?._id, }), ); }); diff --git a/tests/resolvers/Mutation/createVolunteerMembership.spec.ts b/tests/resolvers/Mutation/createVolunteerMembership.spec.ts new file mode 100644 index 0000000000..6ef779bbc6 --- /dev/null +++ b/tests/resolvers/Mutation/createVolunteerMembership.spec.ts @@ -0,0 +1,165 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import type { Document } from "mongoose"; +import { Types } from "mongoose"; +import type { MutationCreateVolunteerMembershipArgs } from "../../../src/types/generatedGraphQLTypes"; +import { connect, disconnect } from "../../helpers/db"; + +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + it, + vi, +} from "vitest"; +import { + EVENT_NOT_FOUND_ERROR, + USER_NOT_FOUND_ERROR, +} from "../../../src/constants"; +import type { TestUserType } from "../../helpers/userAndOrg"; +import { createTestEvent } from "../../helpers/events"; +import type { TestEventType } from "../../helpers/events"; +import { createTestUser } from "../../helpers/user"; +import type { InterfaceEventVolunteerGroup } from "../../../src/models"; +import { createVolunteerMembership } from "../../../src/resolvers/Mutation/createVolunteerMembership"; +import type { + TestVolunteerGroupType, + TestVolunteerType, +} from "../../helpers/volunteers"; +import { createTestVolunteerAndGroup } from "../../helpers/volunteers"; + +export type TestEventVolunteerGroupType = + | (InterfaceEventVolunteerGroup & Document) + | null; + +let testUser1: TestUserType; +let testEvent: TestEventType; +let tUser: TestUserType; +let tEvent: TestEventType; +let tVolunteer: TestVolunteerType; +let tVolunteerGroup: TestVolunteerGroupType; +let MONGOOSE_INSTANCE: typeof mongoose; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const { requestContext } = await import("../../../src/libraries"); + vi.spyOn(requestContext, "translate").mockImplementation( + (message) => message, + ); + testUser1 = await createTestUser(); + [, , testEvent] = await createTestEvent(); + + [tUser, , tEvent, tVolunteer, tVolunteerGroup] = + await createTestVolunteerAndGroup(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Mutation -> createVolunteerMembership", () => { + afterEach(() => { + vi.doUnmock("../../../src/constants"); + vi.resetModules(); + }); + + it(`throws NotFoundError if no user exists with _id === context.userId`, async () => { + try { + const args: MutationCreateVolunteerMembershipArgs = { + data: { + event: tEvent?._id, + group: tVolunteerGroup?._id, + status: "invited", + userId: tUser?._id, + }, + }; + + const context = { + userId: new Types.ObjectId().toString(), + }; + + await createVolunteerMembership?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws NotFoundError if no volunteer user exists with _id === args.data.userId`, async () => { + try { + const args: MutationCreateVolunteerMembershipArgs = { + data: { + event: tEvent?._id, + group: tVolunteerGroup?._id, + status: "invited", + userId: new Types.ObjectId().toString(), + }, + }; + + const context = { + userId: tUser?._id, + }; + + await createVolunteerMembership?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws NotFoundError if no event exists with _id === args.data.event`, async () => { + try { + const args: MutationCreateVolunteerMembershipArgs = { + data: { + event: new Types.ObjectId().toString(), + group: tVolunteerGroup?._id, + status: "invited", + userId: tUser?._id, + }, + }; + + const context = { + userId: tUser?._id, + }; + + await createVolunteerMembership?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual(EVENT_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`Create Voluneer Membership when volunteer already exists`, async () => { + const args: MutationCreateVolunteerMembershipArgs = { + data: { + event: tEvent?._id, + group: tVolunteerGroup?._id, + status: "invited", + userId: tUser?._id, + }, + }; + + const context = { + userId: tUser?._id, + }; + + const mem = await createVolunteerMembership?.({}, args, context); + expect(mem?.volunteer).toEqual(tVolunteer?._id); + }); + + it(`Create Voluneer Membership when volunteer doesn't exists`, async () => { + const args: MutationCreateVolunteerMembershipArgs = { + data: { + event: testEvent?._id, + status: "invited", + userId: testUser1?._id, + }, + }; + + const context = { + userId: tUser?._id, + }; + + const mem = await createVolunteerMembership?.({}, args, context); + expect(mem?.event).toEqual(testEvent?._id); + }); +}); diff --git a/tests/resolvers/Mutation/removeEventVolunteer.spec.ts b/tests/resolvers/Mutation/removeEventVolunteer.spec.ts index bc6aa6d891..30c8bb4142 100644 --- a/tests/resolvers/Mutation/removeEventVolunteer.spec.ts +++ b/tests/resolvers/Mutation/removeEventVolunteer.spec.ts @@ -25,6 +25,7 @@ import { createTestEvent } from "../../helpers/events"; import { EventVolunteer, EventVolunteerGroup } from "../../../src/models"; import { createTestUser } from "../../helpers/user"; import type { TestEventVolunteerGroupType } from "./createEventVolunteer.spec"; +import { removeEventVolunteer } from "../../../src/resolvers/Mutation/removeEventVolunteer"; let MONGOOSE_INSTANCE: typeof mongoose; let testUser: TestUserType; @@ -35,23 +36,27 @@ let testGroup: TestEventVolunteerGroupType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); + const { requestContext } = await import("../../../src/libraries"); + vi.spyOn(requestContext, "translate").mockImplementation( + (message) => message, + ); testUser = await createTestUser(); [eventAdminUser, , testEvent] = await createTestEvent(); testGroup = await EventVolunteerGroup.create({ - creatorId: eventAdminUser?._id, - eventId: testEvent?._id, - leaderId: eventAdminUser?._id, + creator: eventAdminUser?._id, + event: testEvent?._id, + leader: eventAdminUser?._id, name: "Test group", }); testEventVolunteer = await EventVolunteer.create({ - creatorId: eventAdminUser?._id, - userId: testUser?._id, - eventId: testEvent?._id, - groupId: testGroup._id, - isInvited: true, - isAssigned: false, + creator: eventAdminUser?._id, + user: testUser?._id, + event: testEvent?._id, + groups: [testGroup?._id], + hasAccepted: false, + isPublic: false, }); }); @@ -64,13 +69,33 @@ describe("resolvers -> Mutation -> removeEventVolunteer", () => { vi.doUnmock("../../../src/constants"); vi.resetModules(); }); - it(`throws NotFoundError if no user exists with _id === context.userId `, async () => { - const { requestContext } = await import("../../../src/libraries"); - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); + it(`removes event volunteer with _id === args.id and returns it`, async () => { + const args: MutationUpdateEventVolunteerArgs = { + id: testEventVolunteer?._id, + }; + + const context = { userId: eventAdminUser?._id }; + + const deletedVolunteer = await removeEventVolunteer?.({}, args, context); + + const updatedGroup = await EventVolunteerGroup.findOne({ + _id: testGroup?._id, + }); + + expect(updatedGroup?.volunteers.toString()).toEqual(""); + expect(deletedVolunteer).toEqual( + expect.objectContaining({ + _id: testEventVolunteer?._id, + user: testEventVolunteer?.user, + hasAccepted: testEventVolunteer?.hasAccepted, + isPublic: testEventVolunteer?.isPublic, + }), + ); + }); + + it(`throws NotFoundError if no user exists with _id === context.userId `, async () => { try { const args: MutationUpdateEventVolunteerArgs = { id: testEventVolunteer?._id, @@ -78,25 +103,15 @@ describe("resolvers -> Mutation -> removeEventVolunteer", () => { const context = { userId: new Types.ObjectId().toString() }; - const { removeEventVolunteer: removeEventVolunteerResolver } = - await import("../../../src/resolvers/Mutation/removeEventVolunteer"); - - await removeEventVolunteerResolver?.({}, args, context); + await removeEventVolunteer?.({}, args, context); } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); expect((error as Error).message).toEqual( - `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}`, + `${USER_NOT_FOUND_ERROR.MESSAGE}`, ); } }); it(`throws NotFoundError if no event volunteer exists with _id === args.id`, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { const args: MutationUpdateEventVolunteerArgs = { id: new Types.ObjectId().toString(), @@ -104,76 +119,35 @@ describe("resolvers -> Mutation -> removeEventVolunteer", () => { const context = { userId: testUser?._id }; - const { removeEventVolunteer: removeEventVolunteerResolver } = - await import("../../../src/resolvers/Mutation/removeEventVolunteer"); - - await removeEventVolunteerResolver?.({}, args, context); + await removeEventVolunteer?.({}, args, context); } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith( - EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE, - ); expect((error as Error).message).toEqual( - `Translated ${EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE}`, + `${EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE}`, ); } }); it(`throws UnauthorizedError if current user is not leader of group`, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { + const newVolunteer = await EventVolunteer.create({ + creator: eventAdminUser?._id, + user: testUser?._id, + event: testEvent?._id, + groups: [testGroup?._id], + hasAccepted: false, + isPublic: false, + }); const args: MutationUpdateEventVolunteerArgs = { - id: testEventVolunteer?._id, + id: newVolunteer?._id.toString(), }; const context = { userId: testUser?._id }; - const { removeEventVolunteer: removeEventVolunteerResolver } = - await import("../../../src/resolvers/Mutation/removeEventVolunteer"); - - await removeEventVolunteerResolver?.({}, args, context); + await removeEventVolunteer?.({}, args, context); } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith(USER_NOT_AUTHORIZED_ERROR.MESSAGE); expect((error as Error).message).toEqual( - `Translated ${USER_NOT_AUTHORIZED_ERROR.MESSAGE}`, + `${USER_NOT_AUTHORIZED_ERROR.MESSAGE}`, ); } }); - - it(`removes event volunteer with _id === args.id and returns it`, async () => { - const args: MutationUpdateEventVolunteerArgs = { - id: testEventVolunteer?._id, - }; - - const context = { userId: eventAdminUser?._id }; - const { removeEventVolunteer: removeEventVolunteerResolver } = await import( - "../../../src/resolvers/Mutation/removeEventVolunteer" - ); - - const deletedVolunteer = await removeEventVolunteerResolver?.( - {}, - args, - context, - ); - - const updatedGroup = await EventVolunteerGroup.findOne({ - _id: testGroup?._id, - }); - - expect(updatedGroup?.volunteers.toString()).toEqual(""); - - expect(deletedVolunteer).toEqual( - expect.objectContaining({ - _id: testEventVolunteer?._id, - userId: testEventVolunteer?.userId, - isInvited: testEventVolunteer?.isInvited, - isAssigned: testEventVolunteer?.isAssigned, - response: testEventVolunteer?.response, - }), - ); - }); }); diff --git a/tests/resolvers/Mutation/removeEventVolunteerGroup.spec.ts b/tests/resolvers/Mutation/removeEventVolunteerGroup.spec.ts index f29ca33ecd..784afb8394 100644 --- a/tests/resolvers/Mutation/removeEventVolunteerGroup.spec.ts +++ b/tests/resolvers/Mutation/removeEventVolunteerGroup.spec.ts @@ -6,6 +6,7 @@ import { USER_NOT_FOUND_ERROR, EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, + EVENT_NOT_FOUND_ERROR, } from "../../../src/constants"; import { beforeAll, @@ -22,6 +23,7 @@ import { createTestEvent } from "../../helpers/events"; import { EventVolunteer, EventVolunteerGroup } from "../../../src/models"; import { createTestUser } from "../../helpers/user"; import type { TestEventVolunteerGroupType } from "./createEventVolunteer.spec"; +import { removeEventVolunteerGroup } from "../../../src/resolvers/Mutation/removeEventVolunteerGroup"; let MONGOOSE_INSTANCE: typeof mongoose; let testUser: TestUserType; @@ -31,23 +33,27 @@ let testGroup: TestEventVolunteerGroupType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); + const { requestContext } = await import("../../../src/libraries"); + vi.spyOn(requestContext, "translate").mockImplementation( + (message) => message, + ); testUser = await createTestUser(); [eventAdminUser, , testEvent] = await createTestEvent(); testGroup = await EventVolunteerGroup.create({ - creatorId: eventAdminUser?._id, - eventId: testEvent?._id, - leaderId: eventAdminUser?._id, + creator: eventAdminUser?._id, + event: testEvent?._id, + leader: eventAdminUser?._id, name: "Test group", }); await EventVolunteer.create({ - creatorId: eventAdminUser?._id, - userId: testUser?._id, - eventId: testEvent?._id, - groupId: testGroup._id, - isInvited: true, - isAssigned: false, + creator: eventAdminUser?._id, + user: testUser?._id, + event: testEvent?._id, + groups: [testGroup._id], + hasAccepted: false, + isPublic: false, }); }); @@ -61,12 +67,6 @@ describe("resolvers -> Mutation -> removeEventVolunteerGroup", () => { vi.resetModules(); }); it(`throws NotFoundError if no user exists with _id === context.userId `, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { const args: MutationUpdateEventVolunteerArgs = { id: testGroup?._id, @@ -74,27 +74,20 @@ describe("resolvers -> Mutation -> removeEventVolunteerGroup", () => { const context = { userId: new Types.ObjectId().toString() }; - const { removeEventVolunteerGroup: removeEventVolunteerGroupResolver } = + const { removeEventVolunteerGroup: removeEventVolunteerGroup } = await import( "../../../src/resolvers/Mutation/removeEventVolunteerGroup" ); - await removeEventVolunteerGroupResolver?.({}, args, context); + await removeEventVolunteerGroup?.({}, args, context); } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); expect((error as Error).message).toEqual( - `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}`, + `${USER_NOT_FOUND_ERROR.MESSAGE}`, ); } }); it(`throws NotFoundError if no event volunteer group exists with _id === args.id`, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { const args: MutationUpdateEventVolunteerArgs = { id: new Types.ObjectId().toString(), @@ -102,29 +95,15 @@ describe("resolvers -> Mutation -> removeEventVolunteerGroup", () => { const context = { userId: testUser?._id }; - const { removeEventVolunteerGroup: removeEventVolunteerGroupResolver } = - await import( - "../../../src/resolvers/Mutation/removeEventVolunteerGroup" - ); - - await removeEventVolunteerGroupResolver?.({}, args, context); + await removeEventVolunteerGroup?.({}, args, context); } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith( - EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE, - ); expect((error as Error).message).toEqual( - `Translated ${EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE}`, + `${EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE}`, ); } }); it(`throws UnauthorizedError if current user is not an event admin`, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { const args: MutationUpdateEventVolunteerArgs = { id: testGroup?._id, @@ -132,16 +111,10 @@ describe("resolvers -> Mutation -> removeEventVolunteerGroup", () => { const context = { userId: testUser?._id }; - const { removeEventVolunteerGroup: removeEventVolunteerGroupResolver } = - await import( - "../../../src/resolvers/Mutation/removeEventVolunteerGroup" - ); - - await removeEventVolunteerGroupResolver?.({}, args, context); + await removeEventVolunteerGroup?.({}, args, context); } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith(USER_NOT_AUTHORIZED_ERROR.MESSAGE); expect((error as Error).message).toEqual( - `Translated ${USER_NOT_AUTHORIZED_ERROR.MESSAGE}`, + `${USER_NOT_AUTHORIZED_ERROR.MESSAGE}`, ); } }); @@ -152,10 +125,8 @@ describe("resolvers -> Mutation -> removeEventVolunteerGroup", () => { }; const context = { userId: eventAdminUser?._id }; - const { removeEventVolunteerGroup: removeEventVolunteerGroupResolver } = - await import("../../../src/resolvers/Mutation/removeEventVolunteerGroup"); - const deletedVolunteerGroup = await removeEventVolunteerGroupResolver?.( + const deletedVolunteerGroup = await removeEventVolunteerGroup?.( {}, args, context, @@ -164,11 +135,34 @@ describe("resolvers -> Mutation -> removeEventVolunteerGroup", () => { expect(deletedVolunteerGroup).toEqual( expect.objectContaining({ _id: testGroup?._id, - leaderId: testGroup?.leaderId, + leader: testGroup?.leader, name: testGroup?.name, - creatorId: testGroup?.creatorId, - eventId: testGroup?.eventId, + creator: testGroup?.creator, + event: testGroup?.event, }), ); }); + + it(`throws NotFoundError if volunteerGroup.event doesn't exist`, async () => { + try { + const newGrp = await EventVolunteerGroup.create({ + creator: eventAdminUser?._id, + event: new Types.ObjectId(), + leader: eventAdminUser?._id, + name: "Test group", + }); + + const args: MutationUpdateEventVolunteerArgs = { + id: newGrp?._id.toString(), + }; + + const context = { userId: eventAdminUser?._id }; + + await removeEventVolunteerGroup?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual( + `${EVENT_NOT_FOUND_ERROR.MESSAGE}`, + ); + } + }); }); diff --git a/tests/resolvers/Mutation/removeOrganization.spec.ts b/tests/resolvers/Mutation/removeOrganization.spec.ts index f2dfad353c..1555db8e61 100644 --- a/tests/resolvers/Mutation/removeOrganization.spec.ts +++ b/tests/resolvers/Mutation/removeOrganization.spec.ts @@ -146,6 +146,7 @@ beforeAll(async () => { creator: testUsers[0]?._id, assignee: testUsers[1]?._id, assigner: testUsers[0]?._id, + assigneeType: "EventVolunteer", actionItemCategory: testCategory?._id, organization: testOrganization?._id, }); diff --git a/tests/resolvers/Mutation/updateActionItem.spec.ts b/tests/resolvers/Mutation/updateActionItem.spec.ts index b7406cd6fa..fc6c8eb5cd 100644 --- a/tests/resolvers/Mutation/updateActionItem.spec.ts +++ b/tests/resolvers/Mutation/updateActionItem.spec.ts @@ -5,9 +5,10 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { ACTION_ITEM_NOT_FOUND_ERROR, EVENT_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, USER_NOT_FOUND_ERROR, - USER_NOT_MEMBER_FOR_ORGANIZATION, } from "../../../src/constants"; import { updateActionItem as updateActionItemResolver } from "../../../src/resolvers/Mutation/updateActionItem"; import type { MutationUpdateActionItemArgs } from "../../../src/types/generatedGraphQLTypes"; @@ -16,26 +17,28 @@ import type { TestOrganizationType, TestUserType, } from "../../helpers/userAndOrg"; -import { - createTestUser, - createTestUserAndOrganization, -} from "../../helpers/userAndOrg"; +import { createTestUserAndOrganization } from "../../helpers/userAndOrg"; import { nanoid } from "nanoid"; -import { ActionItem, AppUserProfile, Event, User } from "../../../src/models"; +import { ActionItem, AppUserProfile, Event } from "../../../src/models"; import type { TestActionItemType } from "../../helpers/actionItem"; import { createTestActionItem } from "../../helpers/actionItem"; import type { TestActionItemCategoryType } from "../../helpers/actionItemCategory"; import type { TestEventType } from "../../helpers/events"; +import type { + TestVolunteerGroupType, + TestVolunteerType, +} from "../../helpers/volunteers"; +import { createTestVolunteerAndGroup } from "../../helpers/volunteers"; -let randomUser: TestUserType; -let assignedTestUser: TestUserType; let testUser: TestUserType; let testUser2: TestUserType; let testOrganization: TestOrganizationType; let testCategory: TestActionItemCategoryType; let testActionItem: TestActionItemType; let testEvent: TestEventType; +let tVolunteer: TestVolunteerType; +let tVolunteerGroup: TestVolunteerGroupType; let MONGOOSE_INSTANCE: typeof mongoose; beforeAll(async () => { @@ -45,10 +48,8 @@ beforeAll(async () => { (message) => message, ); - randomUser = await createTestUser(); - [testUser2] = await createTestUserAndOrganization(); - [testUser, testOrganization, testCategory, testActionItem, assignedTestUser] = + [testUser, testOrganization, testCategory, testActionItem] = await createTestActionItem(); testEvent = await Event.create({ @@ -63,6 +64,8 @@ beforeAll(async () => { admins: [testUser2?._id], organization: testOrganization?._id, }); + + [, , , tVolunteer, tVolunteerGroup] = await createTestVolunteerAndGroup(); }); afterAll(async () => { @@ -75,7 +78,7 @@ describe("resolvers -> Mutation -> updateActionItem", () => { const args: MutationUpdateActionItemArgs = { id: new Types.ObjectId().toString(), data: { - assigneeId: randomUser?._id, + assigneeId: tVolunteer?._id, }, }; @@ -94,7 +97,7 @@ describe("resolvers -> Mutation -> updateActionItem", () => { const args: MutationUpdateActionItemArgs = { id: new Types.ObjectId().toString(), data: { - assigneeId: randomUser?._id, + assigneeId: tVolunteer?._id, }, }; @@ -116,6 +119,7 @@ describe("resolvers -> Mutation -> updateActionItem", () => { id: testActionItem?._id, data: { assigneeId: new Types.ObjectId().toString(), + assigneeType: "EventVolunteer", }, }; @@ -125,16 +129,31 @@ describe("resolvers -> Mutation -> updateActionItem", () => { await updateActionItemResolver?.({}, args, context); } catch (error: unknown) { - expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE, + ); } }); - it(`throws NotFoundError if the new asignee is not a member of the organization`, async () => { + it(`throws NotFoundError if no user exists with _id === args.data.assigneeId`, async () => { try { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteerGroup", + assigneeGroup: new Types.ObjectId().toString(), + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + }); + const args: MutationUpdateActionItemArgs = { - id: testActionItem?._id, + id: testActionItem2?._id.toString() ?? "", data: { - assigneeId: randomUser?._id, + assigneeId: new Types.ObjectId().toString(), + assigneeType: "EventVolunteerGroup", }, }; @@ -145,17 +164,83 @@ describe("resolvers -> Mutation -> updateActionItem", () => { await updateActionItemResolver?.({}, args, context); } catch (error: unknown) { expect((error as Error).message).toEqual( - USER_NOT_MEMBER_FOR_ORGANIZATION.MESSAGE, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE, ); } }); + it(`throws NotFoundError if no user exists with _id === args.data.assigneeId`, async () => { + try { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteerGroup", + assigneeGroup: new Types.ObjectId().toString(), + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + }); + + const args: MutationUpdateActionItemArgs = { + id: testActionItem2?._id.toString() ?? "", + data: { + assigneeId: new Types.ObjectId().toString(), + assigneeType: "EventVolunteerGroup", + }, + }; + + const context = { + userId: testUser?._id, + }; + + await updateActionItemResolver?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual( + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE, + ); + } + }); + it(`throws NotFoundError if no user exists when assigneeUser (doesn't exist)`, async () => { + try { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "User", + assigneeUser: new Types.ObjectId().toString(), + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: null, + }); + + const args: MutationUpdateActionItemArgs = { + id: testActionItem2?._id.toString() ?? "", + data: { + assigneeId: new Types.ObjectId().toString(), + assigneeType: "User", + }, + }; + + const context = { + userId: testUser?._id, + }; + + await updateActionItemResolver?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + } + }); + it(`throws NotAuthorizedError if the user is not a superadmin/orgAdmin/eventAdmin`, async () => { try { const args: MutationUpdateActionItemArgs = { id: testActionItem?._id, data: { - assigneeId: testUser?._id, + assigneeId: tVolunteer?._id, + assigneeType: "EventVolunteer", }, }; @@ -171,18 +256,280 @@ describe("resolvers -> Mutation -> updateActionItem", () => { } }); - it(`updates the action item and returns it as an admin`, async () => { + it(`throws NotAuthorizedError if the actionItem.event doesn't exist`, async () => { + try { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + event: new Types.ObjectId().toString(), + creator: testUser?._id, + assigneeType: "EventVolunteer", + assignee: new Types.ObjectId().toString(), + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + }); + + const args: MutationUpdateActionItemArgs = { + id: testActionItem2?._id.toString() ?? "", + data: { + assigneeId: tVolunteer?._id, + assigneeType: "EventVolunteer", + }, + }; + + const context = { + userId: testUser2?._id, + }; + + await updateActionItemResolver?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual(EVENT_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`updates the action item and sets action item as completed`, async () => { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteer", + assignee: tVolunteer?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + allottedHours: 2, + isCompleted: false, + }); + + const testActionItem3 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteer", + assignee: tVolunteer?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + allottedHours: 0, + isCompleted: false, + }); + const args: MutationUpdateActionItemArgs = { - id: testActionItem?._id, + id: testActionItem2?._id.toString() ?? "", + data: { + assigneeId: tVolunteer?._id, + assigneeType: "EventVolunteer", + isCompleted: true, + }, + }; + + const args2: MutationUpdateActionItemArgs = { + id: testActionItem3?._id.toString() ?? "", + data: { + assigneeId: tVolunteer?._id, + assigneeType: "EventVolunteer", + isCompleted: true, + }, + }; + + const context = { + userId: testUser?._id, + }; + + await updateActionItemResolver?.({}, args, context); + await updateActionItemResolver?.({}, args2, context); + }); + + it(`updates the action item and sets action item as not completed`, async () => { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteer", + assignee: tVolunteer?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + allottedHours: 2, + isCompleted: true, + }); + + const testActionItem3 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteer", + assignee: tVolunteer?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + isCompleted: true, + }); + + const args: MutationUpdateActionItemArgs = { + id: testActionItem2?._id.toString() ?? "", + data: { + assigneeId: tVolunteer?._id, + assigneeType: "EventVolunteer", + isCompleted: false, + }, + }; + + const args2: MutationUpdateActionItemArgs = { + id: testActionItem3?._id.toString() ?? "", + data: { + assigneeId: tVolunteer?._id, + assigneeType: "EventVolunteer", + isCompleted: false, + }, + }; + + const context = { + userId: testUser?._id, + }; + + await updateActionItemResolver?.({}, args, context); + await updateActionItemResolver?.({}, args2, context); + }); + + it(`updates the action item and sets action item as completed (Volunteer Group)`, async () => { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteerGroup", + assigneeGroup: tVolunteerGroup?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + allottedHours: 2, + isCompleted: false, + }); + + const testActionItem3 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteerGroup", + assigneeGroup: tVolunteerGroup?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + allottedHours: 0, + isCompleted: false, + }); + + const args: MutationUpdateActionItemArgs = { + id: testActionItem2?._id.toString() ?? "", + data: { + assigneeId: tVolunteerGroup?._id, + assigneeType: "EventVolunteerGroup", + isCompleted: true, + }, + }; + + const args2: MutationUpdateActionItemArgs = { + id: testActionItem3?._id.toString() ?? "", data: { - assigneeId: assignedTestUser?._id, + assigneeId: tVolunteerGroup?._id, + assigneeType: "EventVolunteerGroup", + isCompleted: true, }, }; - // console.log(testUser?._id); + const context = { userId: testUser?._id, }; + await updateActionItemResolver?.({}, args, context); + await updateActionItemResolver?.({}, args2, context); + }); + + it(`updates the action item and sets action item as not completed (Volunteer Group)`, async () => { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteerGroup", + assigneeGroup: tVolunteerGroup?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + allottedHours: 2, + isCompleted: true, + }); + + const testActionItem3 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteerGroup", + assigneeGroup: tVolunteerGroup?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + isCompleted: true, + }); + + const args: MutationUpdateActionItemArgs = { + id: testActionItem2?._id.toString() ?? "", + data: { + assigneeId: tVolunteerGroup?._id, + assigneeType: "EventVolunteerGroup", + isCompleted: false, + }, + }; + + const args2: MutationUpdateActionItemArgs = { + id: testActionItem3?._id.toString() ?? "", + data: { + assigneeId: tVolunteerGroup?._id, + assigneeType: "EventVolunteerGroup", + isCompleted: false, + }, + }; + + const context = { + userId: testUser?._id, + }; + + await updateActionItemResolver?.({}, args, context); + await updateActionItemResolver?.({}, args2, context); + }); + + it(`updates the actionItem when the user is authorized as an eventAdmin`, async () => { + const updatedTestActionItem = await ActionItem.findOneAndUpdate( + { + _id: testActionItem?._id, + }, + { + event: testEvent?._id, + }, + { + new: true, + }, + ); + + const args: MutationUpdateActionItemArgs = { + data: { + isCompleted: true, + }, + id: updatedTestActionItem?._id.toString() ?? "", + }; + + const context = { + userId: testUser2?._id, + }; + const updatedActionItemPayload = await updateActionItemResolver?.( {}, args, @@ -191,19 +538,19 @@ describe("resolvers -> Mutation -> updateActionItem", () => { expect(updatedActionItemPayload).toEqual( expect.objectContaining({ - assignee: assignedTestUser?._id, actionItemCategory: testCategory?._id, + isCompleted: true, }), ); }); - it(`updates the action item and returns it as superadmin`, async () => { - const superAdminTestUser = await AppUserProfile.findOneAndUpdate( + it(`updates the actionItem isCompleted is undefined (EventVolunteer)`, async () => { + const updatedTestActionItem = await ActionItem.findOneAndUpdate( { - userId: randomUser?._id, + _id: testActionItem?._id, }, { - isSuperAdmin: true, + event: testEvent?._id, }, { new: true, @@ -211,14 +558,16 @@ describe("resolvers -> Mutation -> updateActionItem", () => { ); const args: MutationUpdateActionItemArgs = { - id: testActionItem?._id, data: { - assigneeId: testUser?._id, + isCompleted: undefined, + assigneeId: undefined, + assigneeType: "EventVolunteer", }, + id: updatedTestActionItem?._id.toString() ?? "", }; const context = { - userId: superAdminTestUser?.userId, + userId: testUser2?._id, }; const updatedActionItemPayload = await updateActionItemResolver?.( @@ -229,53 +578,53 @@ describe("resolvers -> Mutation -> updateActionItem", () => { expect(updatedActionItemPayload).toEqual( expect.objectContaining({ - assignee: testUser?._id, actionItemCategory: testCategory?._id, + isCompleted: true, }), ); }); - it(`throws NotFoundError if no event exists to which the action item is associated`, async () => { + it(`updates the actionItem isCompleted is undefined (EventVolunteerGroup)`, async () => { const updatedTestActionItem = await ActionItem.findOneAndUpdate( { _id: testActionItem?._id, }, { - event: new Types.ObjectId().toString(), + event: testEvent?._id, }, { new: true, }, ); - await User.updateOne( - { - _id: randomUser?._id, - }, - { - $push: { joinedOrganizations: testOrganization?._id }, + const args: MutationUpdateActionItemArgs = { + data: { + isCompleted: undefined, + assigneeId: undefined, + assigneeType: "EventVolunteerGroup", }, - ); + id: updatedTestActionItem?._id.toString() ?? "", + }; - try { - const args: MutationUpdateActionItemArgs = { - id: updatedTestActionItem?._id.toString() ?? "", - data: { - assigneeId: randomUser?._id, - }, - }; + const context = { + userId: testUser2?._id, + }; - const context = { - userId: testUser?._id, - }; + const updatedActionItemPayload = await updateActionItemResolver?.( + {}, + args, + context, + ); - await updateActionItemResolver?.({}, args, context); - } catch (error: unknown) { - expect((error as Error).message).toEqual(EVENT_NOT_FOUND_ERROR.MESSAGE); - } + expect(updatedActionItemPayload).toEqual( + expect.objectContaining({ + actionItemCategory: testCategory?._id, + isCompleted: true, + }), + ); }); - it(`updates the actionItem when the user is authorized as an eventAdmin`, async () => { + it(`updates the actionItem isCompleted is undefined (User)`, async () => { const updatedTestActionItem = await ActionItem.findOneAndUpdate( { _id: testActionItem?._id, @@ -290,7 +639,9 @@ describe("resolvers -> Mutation -> updateActionItem", () => { const args: MutationUpdateActionItemArgs = { data: { - isCompleted: true, + isCompleted: undefined, + assigneeId: undefined, + assigneeType: "User", }, id: updatedTestActionItem?._id.toString() ?? "", }; @@ -312,6 +663,7 @@ describe("resolvers -> Mutation -> updateActionItem", () => { }), ); }); + it("throws error if user does not have appUserProfile", async () => { await AppUserProfile.deleteOne({ userId: testUser2?._id, diff --git a/tests/resolvers/Mutation/updateEventVolunteer.spec.ts b/tests/resolvers/Mutation/updateEventVolunteer.spec.ts index 4a749cca91..86ab7b1521 100644 --- a/tests/resolvers/Mutation/updateEventVolunteer.spec.ts +++ b/tests/resolvers/Mutation/updateEventVolunteer.spec.ts @@ -1,38 +1,29 @@ import type mongoose from "mongoose"; -import { Types } from "mongoose"; -import type { MutationUpdateEventVolunteerArgs } from "../../../src/types/generatedGraphQLTypes"; import { connect, disconnect } from "../../helpers/db"; -import { - USER_NOT_FOUND_ERROR, - EventVolunteerResponse, - EVENT_VOLUNTEER_NOT_FOUND_ERROR, - EVENT_VOLUNTEER_INVITE_USER_MISTMATCH, -} from "../../../src/constants"; -import { - beforeAll, - afterAll, - describe, - it, - expect, - vi, - afterEach, -} from "vitest"; -import type { - TestEventType, - TestEventVolunteerType, -} from "../../helpers/events"; -import { createTestEventAndVolunteer } from "../../helpers/events"; -import { createTestUser } from "../../helpers/user"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import type { TestEventVolunteerType } from "../../helpers/events"; +import type { TestUserType } from "../../helpers/user"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import type { InterfaceEventVolunteer } from "../../../src/models"; +import { updateEventVolunteer } from "../../../src/resolvers/Mutation/updateEventVolunteer"; +import { EVENT_VOLUNTEER_INVITE_USER_MISTMATCH } from "../../../src/constants"; +import { requestContext } from "../../../src/libraries"; let MONGOOSE_INSTANCE: typeof mongoose; -let testEvent: TestEventType; -let testEventVolunteer: TestEventVolunteerType; +let testUser1: TestUserType; +let testUser2: TestUserType; +let testEventVolunteer1: TestEventVolunteerType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); - const temp = await createTestEventAndVolunteer(); - testEvent = temp[2]; - testEventVolunteer = temp[3]; + vi.spyOn(requestContext, "translate").mockImplementation( + (message) => message, + ); + const [, , user1, user2, volunteer1] = await createVolunteerAndActions(); + + testUser1 = user1; + testUser2 = user2; + testEventVolunteer1 = volunteer1; }); afterAll(async () => { @@ -40,161 +31,55 @@ afterAll(async () => { }); describe("resolvers -> Mutation -> updateEventVolunteer", () => { - afterEach(() => { - vi.doUnmock("../../../src/constants"); - vi.resetModules(); - }); - it(`throws NotFoundError if no user exists with _id === context.userId `, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - + it(`throws error if context.userId !== volunteer._id`, async () => { try { - const args: MutationUpdateEventVolunteerArgs = { - id: testEventVolunteer?._id, - data: { - response: EventVolunteerResponse.YES, + (await updateEventVolunteer?.( + {}, + { + id: testEventVolunteer1?._id, + data: { + isPublic: false, + }, }, - }; - - const context = { userId: new Types.ObjectId().toString() }; - - const { updateEventVolunteer: updateEventVolunteerResolver } = - await import("../../../src/resolvers/Mutation/updateEventVolunteer"); - - await updateEventVolunteerResolver?.({}, args, context); + { userId: testUser2?._id.toString() }, + )) as unknown as InterfaceEventVolunteer[]; } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); expect((error as Error).message).toEqual( - `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}`, - ); - } - }); - - it(`throws NotFoundError if no event volunteer exists with _id === args.id`, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - - try { - const args: MutationUpdateEventVolunteerArgs = { - id: new Types.ObjectId().toString(), - data: { - response: EventVolunteerResponse.YES, - }, - }; - - const context = { userId: testEventVolunteer?.userId }; - - const { updateEventVolunteer: updateEventVolunteerResolver } = - await import("../../../src/resolvers/Mutation/updateEventVolunteer"); - - await updateEventVolunteerResolver?.({}, args, context); - } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith( - EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE, - ); - expect((error as Error).message).toEqual( - `Translated ${EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE}`, - ); - } - }); - - it(`throws ConflictError if userId of volunteer is not equal to context.userId `, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - - try { - const args: MutationUpdateEventVolunteerArgs = { - id: testEventVolunteer?._id, - data: { - response: EventVolunteerResponse.YES, - }, - }; - - const testUser2 = await createTestUser(); - const context = { userId: testUser2?._id }; - const { updateEventVolunteer: updateEventVolunteerResolver } = - await import("../../../src/resolvers/Mutation/updateEventVolunteer"); - - await updateEventVolunteerResolver?.({}, args, context); - } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith( EVENT_VOLUNTEER_INVITE_USER_MISTMATCH.MESSAGE, ); - expect((error as Error).message).toEqual( - `Translated ${EVENT_VOLUNTEER_INVITE_USER_MISTMATCH.MESSAGE}`, - ); } }); - it(`updates the Event Volunteer with _id === args.id and returns it`, async () => { - const args: MutationUpdateEventVolunteerArgs = { - id: testEventVolunteer?._id, - data: { - isAssigned: true, - response: EventVolunteerResponse.YES, - isInvited: true, - eventId: testEvent?._id, - }, - }; - - const context = { userId: testEventVolunteer?.userId }; - - const { updateEventVolunteer: updateEventVolunteerResolver } = await import( - "../../../src/resolvers/Mutation/updateEventVolunteer" - ); - - const updatedEventVolunteer = await updateEventVolunteerResolver?.( + it(`data remains same if no values are updated`, async () => { + const updatedVolunteer = (await updateEventVolunteer?.( {}, - args, - context, - ); - - expect(updatedEventVolunteer).toEqual( - expect.objectContaining({ - isAssigned: true, - response: EventVolunteerResponse.YES, - eventId: testEvent?._id, - isInvited: true, - }), + { + id: testEventVolunteer1?._id, + data: {}, + }, + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceEventVolunteer; + expect(updatedVolunteer.isPublic).toEqual(testEventVolunteer1?.isPublic); + expect(updatedVolunteer.hasAccepted).toEqual( + testEventVolunteer1?.hasAccepted, ); }); - it(`updates the Event Volunteer with _id === args.id, even if args.data is empty object`, async () => { - const t = await createTestEventAndVolunteer(); - testEventVolunteer = t[3]; - const args: MutationUpdateEventVolunteerArgs = { - id: testEventVolunteer?._id, - data: {}, - }; - - const context = { userId: testEventVolunteer?.userId }; - - const { updateEventVolunteer: updateEventVolunteerResolver } = await import( - "../../../src/resolvers/Mutation/updateEventVolunteer" - ); - - const updatedEventVolunteer = await updateEventVolunteerResolver?.( + it(`updates EventVolunteer`, async () => { + const updatedVolunteer = (await updateEventVolunteer?.( {}, - args, - context, - ); - - expect(updatedEventVolunteer).toEqual( - expect.objectContaining({ - isAssigned: testEventVolunteer?.isAssigned, - response: testEventVolunteer?.response, - eventId: testEventVolunteer?.eventId, - isInvited: testEventVolunteer?.isInvited, - }), - ); + { + id: testEventVolunteer1?._id, + data: { + isPublic: false, + hasAccepted: false, + assignments: [], + }, + }, + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceEventVolunteer; + expect(updatedVolunteer.isPublic).toEqual(false); + expect(updatedVolunteer.hasAccepted).toEqual(false); + expect(updatedVolunteer.assignments).toEqual([]); }); }); diff --git a/tests/resolvers/Mutation/updateEventVolunteerGroup.spec.ts b/tests/resolvers/Mutation/updateEventVolunteerGroup.spec.ts index 645161bec6..c735688e63 100644 --- a/tests/resolvers/Mutation/updateEventVolunteerGroup.spec.ts +++ b/tests/resolvers/Mutation/updateEventVolunteerGroup.spec.ts @@ -1,14 +1,12 @@ import type mongoose from "mongoose"; import { Types } from "mongoose"; -import type { - MutationUpdateEventVolunteerArgs, - MutationUpdateEventVolunteerGroupArgs, -} from "../../../src/types/generatedGraphQLTypes"; +import type { MutationUpdateEventVolunteerGroupArgs } from "../../../src/types/generatedGraphQLTypes"; import { connect, disconnect } from "../../helpers/db"; import { USER_NOT_FOUND_ERROR, EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, + EVENT_NOT_FOUND_ERROR, } from "../../../src/constants"; import { beforeAll, @@ -25,6 +23,8 @@ import { createTestUser } from "../../helpers/user"; import type { TestUserType } from "../../helpers/userAndOrg"; import type { TestEventVolunteerGroupType } from "./createEventVolunteer.spec"; import { EventVolunteerGroup } from "../../../src/models"; +import { requestContext } from "../../../src/libraries"; +import { updateEventVolunteerGroup } from "../../../src/resolvers/Mutation/updateEventVolunteerGroup"; let MONGOOSE_INSTANCE: typeof mongoose; let testEvent: TestEventType; @@ -35,9 +35,9 @@ beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); [eventAdminUser, , testEvent] = await createTestEvent(); testGroup = await EventVolunteerGroup.create({ - creatorId: eventAdminUser?._id, - eventId: testEvent?._id, - leaderId: eventAdminUser?._id, + creator: eventAdminUser?._id, + event: testEvent?._id, + leader: eventAdminUser?._id, name: "Test group", volunteersRequired: 2, }); @@ -54,8 +54,6 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { }); it(`throws NotFoundError if no user exists with _id === context.userId `, async () => { - const { requestContext } = await import("../../../src/libraries"); - const spy = vi .spyOn(requestContext, "translate") .mockImplementationOnce((message) => `Translated ${message}`); @@ -65,17 +63,12 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { id: testGroup?._id, data: { name: "updated name", + eventId: testEvent?._id, }, }; const context = { userId: new Types.ObjectId().toString() }; - - const { updateEventVolunteerGroup: updateEventVolunteerGroupResolver } = - await import( - "../../../src/resolvers/Mutation/updateEventVolunteerGroup" - ); - - await updateEventVolunteerGroupResolver?.({}, args, context); + await updateEventVolunteerGroup?.({}, args, context); } catch (error: unknown) { expect(spy).toHaveBeenLastCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); expect((error as Error).message).toEqual( @@ -85,8 +78,6 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { }); it(`throws NotFoundError if no event volunteer group exists with _id === args.id`, async () => { - const { requestContext } = await import("../../../src/libraries"); - const spy = vi .spyOn(requestContext, "translate") .mockImplementationOnce((message) => `Translated ${message}`); @@ -96,17 +87,12 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { id: new Types.ObjectId().toString(), data: { name: "updated name", + eventId: testEvent?._id, }, }; const context = { userId: eventAdminUser?._id }; - - const { updateEventVolunteerGroup: updateEventVolunteerGroupResolver } = - await import( - "../../../src/resolvers/Mutation/updateEventVolunteerGroup" - ); - - await updateEventVolunteerGroupResolver?.({}, args, context); + await updateEventVolunteerGroup?.({}, args, context); } catch (error: unknown) { expect(spy).toHaveBeenLastCalledWith( EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE, @@ -117,9 +103,31 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { } }); - it(`throws UnauthorizedError if current user is not leader of group `, async () => { - const { requestContext } = await import("../../../src/libraries"); + it(`throws NotFoundError if no event exists with _id === args.data.eventId`, async () => { + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementationOnce((message) => `Translated ${message}`); + try { + const args: MutationUpdateEventVolunteerGroupArgs = { + id: testGroup?._id, + data: { + name: "updated name", + eventId: new Types.ObjectId().toString(), + }, + }; + + const context = { userId: eventAdminUser?._id }; + await updateEventVolunteerGroup?.({}, args, context); + } catch (error: unknown) { + expect(spy).toHaveBeenLastCalledWith(EVENT_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `Translated ${EVENT_NOT_FOUND_ERROR.MESSAGE}`, + ); + } + }); + + it(`throws UnauthorizedError if current user is not leader of group `, async () => { const spy = vi .spyOn(requestContext, "translate") .mockImplementationOnce((message) => `Translated ${message}`); @@ -129,18 +137,14 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { id: testGroup?._id, data: { name: "updated name", + eventId: testEvent?._id, }, }; const testUser2 = await createTestUser(); const context = { userId: testUser2?._id }; - const { updateEventVolunteerGroup: updateEventVolunteerGroupResolver } = - await import( - "../../../src/resolvers/Mutation/updateEventVolunteerGroup" - ); - - await updateEventVolunteerGroupResolver?.({}, args, context); + await updateEventVolunteerGroup?.({}, args, context); } catch (error: unknown) { expect(spy).toHaveBeenLastCalledWith(USER_NOT_AUTHORIZED_ERROR.MESSAGE); expect((error as Error).message).toEqual( @@ -160,20 +164,12 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { }; const context = { userId: eventAdminUser?._id }; - - const { updateEventVolunteerGroup: updateEventVolunteerGroupResolver } = - await import("../../../src/resolvers/Mutation/updateEventVolunteerGroup"); - - const updatedGroup = await updateEventVolunteerGroupResolver?.( - {}, - args, - context, - ); + const updatedGroup = await updateEventVolunteerGroup?.({}, args, context); expect(updatedGroup).toEqual( expect.objectContaining({ name: "updated", - eventId: testEvent?._id, + event: testEvent?._id, volunteersRequired: 10, }), ); @@ -182,35 +178,26 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { it(`updates the Event Volunteer group with _id === args.id, even if args.data is empty object`, async () => { const testGroup2 = await EventVolunteerGroup.create({ name: "test", - eventId: testEvent?._id, - creatorId: eventAdminUser?._id, + event: testEvent?._id, + creator: eventAdminUser?._id, volunteersRequired: 2, - leaderId: eventAdminUser?._id, + leader: eventAdminUser?._id, }); - const args: MutationUpdateEventVolunteerArgs = { + const args: MutationUpdateEventVolunteerGroupArgs = { id: testGroup2?._id.toString(), - data: {}, + data: { + eventId: testEvent?._id, + }, }; const context = { userId: eventAdminUser?._id }; + const updatedGroup = await updateEventVolunteerGroup?.({}, args, context); - const { updateEventVolunteerGroup: updateEventVolunteerGroupResolver } = - await import("../../../src/resolvers/Mutation/updateEventVolunteerGroup"); - - const updatedGroup = await updateEventVolunteerGroupResolver?.( - {}, - args, - context, - ); - - console.log(updatedGroup); - - console.log(); expect(updatedGroup).toEqual( expect.objectContaining({ name: testGroup2?.name, volunteersRequired: testGroup2?.volunteersRequired, - eventId: testGroup2?.eventId, + event: testGroup2?.event, }), ); }); diff --git a/tests/resolvers/Mutation/updateVolunteerMembership.spec.ts b/tests/resolvers/Mutation/updateVolunteerMembership.spec.ts new file mode 100644 index 0000000000..348998e37e --- /dev/null +++ b/tests/resolvers/Mutation/updateVolunteerMembership.spec.ts @@ -0,0 +1,175 @@ +import type mongoose from "mongoose"; +import { connect, disconnect } from "../../helpers/db"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import type { + TestEventType, + TestEventVolunteerGroupType, + TestEventVolunteerType, +} from "../../helpers/events"; +import { createTestUser, type TestUserType } from "../../helpers/user"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import { VolunteerMembership } from "../../../src/models"; +import { updateVolunteerMembership } from "../../../src/resolvers/Mutation/updateVolunteerMembership"; +import { Types } from "mongoose"; +import { + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, + USER_NOT_FOUND_ERROR, +} from "../../../src/constants"; +import { requestContext } from "../../../src/libraries"; +import { MembershipStatus } from "../Query/getVolunteerMembership.spec"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testEvent: TestEventType; +let testUser1: TestUserType; +let testEventVolunteer1: TestEventVolunteerType; +let testEventVolunteerGroup: TestEventVolunteerGroupType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + vi.spyOn(requestContext, "translate").mockImplementation( + (message) => message, + ); + const [, event, user1, , volunteer1, , volunteerGroup, ,] = + await createVolunteerAndActions(); + + testEvent = event; + testUser1 = user1; + testEventVolunteer1 = volunteer1; + testEventVolunteerGroup = volunteerGroup; + + await VolunteerMembership.insertMany([ + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + status: MembershipStatus.INVITED, + }, + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + group: testEventVolunteerGroup._id, + status: MembershipStatus.REQUESTED, + }, + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + status: MembershipStatus.ACCEPTED, + }, + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + status: MembershipStatus.REJECTED, + }, + ]); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Mutation -> updateVolunteerMembership", () => { + it("throws NotFoundError if current User does not exist", async () => { + try { + const membership = await VolunteerMembership.findOne({ + status: MembershipStatus.REQUESTED, + group: testEventVolunteerGroup._id, + volunteer: testEventVolunteer1?._id, + }); + + await updateVolunteerMembership?.( + {}, + { + id: membership?._id.toString() ?? "", + status: MembershipStatus.ACCEPTED, + }, + { userId: new Types.ObjectId().toString() }, + ); + } catch (error: unknown) { + expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it("throws NotFoundError if VolunteerMembership does not exist", async () => { + try { + await VolunteerMembership.findOne({ + status: MembershipStatus.REQUESTED, + group: testEventVolunteerGroup._id, + volunteer: testEventVolunteer1?._id, + }); + + await updateVolunteerMembership?.( + {}, + { + id: new Types.ObjectId().toString() ?? "", + status: MembershipStatus.ACCEPTED, + }, + { userId: testUser1?._id }, + ); + } catch (error: unknown) { + expect((error as Error).message).toEqual( + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR.MESSAGE, + ); + } + }); + + it("throws UnauthorizedUser Error", async () => { + try { + const membership = await VolunteerMembership.findOne({ + status: MembershipStatus.REJECTED, + volunteer: testEventVolunteer1?._id, + }); + + const randomUser = await createTestUser(); + + await updateVolunteerMembership?.( + {}, + { + id: membership?._id.toString() as string, + status: MembershipStatus.ACCEPTED, + }, + { userId: randomUser?._id.toString() as string }, + ); + } catch (error: unknown) { + expect((error as Error).message).toEqual( + USER_NOT_AUTHORIZED_ERROR.MESSAGE, + ); + } + }); + + it(`updateVolunteerMembership (with group) - set to accepted `, async () => { + const membership = await VolunteerMembership.findOne({ + status: MembershipStatus.INVITED, + volunteer: testEventVolunteer1?._id, + }); + + const updatedMembership = await updateVolunteerMembership?.( + {}, + { + id: membership?._id.toString() ?? "", + status: MembershipStatus.REJECTED, + }, + { userId: testUser1?._id }, + ); + + expect(updatedMembership?.status).toEqual(MembershipStatus.REJECTED); + }); + + it(`updateVolunteerMembership (with group) - set to accepted `, async () => { + const membership = await VolunteerMembership.findOne({ + status: MembershipStatus.REQUESTED, + group: testEventVolunteerGroup._id, + volunteer: testEventVolunteer1?._id, + }); + + const updatedMembership = await updateVolunteerMembership?.( + {}, + { + id: membership?._id.toString() ?? "", + status: MembershipStatus.ACCEPTED, + }, + { userId: testUser1?._id }, + ); + + expect(updatedMembership?.status).toEqual(MembershipStatus.ACCEPTED); + }); +}); diff --git a/tests/resolvers/Query/actionItemsByOrganization.spec.ts b/tests/resolvers/Query/actionItemsByOrganization.spec.ts index 8437ad5cdd..36cca12583 100644 --- a/tests/resolvers/Query/actionItemsByOrganization.spec.ts +++ b/tests/resolvers/Query/actionItemsByOrganization.spec.ts @@ -1,31 +1,46 @@ -import "dotenv/config"; -import type { InterfaceActionItem } from "../../../src/models"; -import { ActionItem, ActionItemCategory } from "../../../src/models"; +import type mongoose from "mongoose"; import { connect, disconnect } from "../../helpers/db"; -import type { - ActionItemWhereInput, - ActionItemsOrderByInput, - QueryActionItemsByOrganizationArgs, -} from "../../../src/types/generatedGraphQLTypes"; -import { actionItemsByOrganization as actionItemsByOrganizationResolver } from "../../../src/resolvers/Query/actionItemsByOrganization"; import { beforeAll, afterAll, describe, it, expect } from "vitest"; -import type mongoose from "mongoose"; -import { createTestActionItems } from "../../helpers/actionItem"; -import type { - TestOrganizationType, - TestUserType, -} from "../../helpers/userAndOrg"; import type { TestEventType } from "../../helpers/events"; +import type { TestUserType } from "../../helpers/user"; +import type { TestOrganizationType } from "../../helpers/userAndOrg"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import type { InterfaceActionItem } from "../../../src/models"; +import { ActionItem } from "../../../src/models"; +import type { TestActionItemType } from "../../helpers/actionItem"; +import { actionItemsByOrganization } from "../../../src/resolvers/Query/actionItemsByOrganization"; let MONGOOSE_INSTANCE: typeof mongoose; let testOrganization: TestOrganizationType; let testEvent: TestEventType; -let testAssigneeUser: TestUserType; +let testUser1: TestUserType; +let testActionItem1: TestActionItemType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); - [, testAssigneeUser, testEvent, testOrganization] = - await createTestActionItems(); + const [organization, event, user1, , , , , actionItem1] = + await createVolunteerAndActions(); + + testOrganization = organization; + testEvent = event; + testUser1 = user1; + testActionItem1 = actionItem1; + + await ActionItem.create({ + creator: testUser1?._id, + assigner: testUser1?._id, + assigneeUser: testUser1?._id, + assigneeType: "User", + assignee: null, + assigneeGroup: null, + actionItemCategory: testActionItem1.actionItemCategory, + event: null, + organization: testOrganization?._id, + allottedHours: 2, + assignmentDate: new Date(), + dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 3000), + isCompleted: false, + }); }); afterAll(async () => { @@ -33,238 +48,37 @@ afterAll(async () => { }); describe("resolvers -> Query -> actionItemsByOrganization", () => { - it(`returns list of all action items associated with an organization in ascending order where eventId is not null`, async () => { - const orderBy: ActionItemsOrderByInput = "createdAt_ASC"; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - eventId: testEvent?._id, - orderBy, - }; - - const actionItemsByOrganizationPayload = - (await actionItemsByOrganizationResolver?.( - {}, - args, - {}, - )) as InterfaceActionItem[]; - - const actionItemsByOrganizationInfo = await ActionItem.find({ - organization: args.organizationId, - event: args.eventId, - }).lean(); - - expect(actionItemsByOrganizationPayload[0]).toEqual( - expect.objectContaining({ - _id: actionItemsByOrganizationInfo[0]._id, - }), - ); - }); - - it(`returns list of all action items associated with an organization in ascending order where eventId is null`, async () => { - const orderBy: ActionItemsOrderByInput = "createdAt_ASC"; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - eventId: null, - orderBy, - }; - - const actionItemsByOrganizationPayload = - (await actionItemsByOrganizationResolver?.( - {}, - args, - {}, - )) as InterfaceActionItem[]; - - const actionItemsByOrganizationInfo = await ActionItem.find({ - organization: args.organizationId, - event: args.eventId, - }).lean(); - - expect(actionItemsByOrganizationPayload[0]).toEqual( - expect.objectContaining({ - _id: actionItemsByOrganizationInfo[0]._id, - }), - ); - }); - - it(`returns list of all action items associated with an organization in descending order`, async () => { - const orderBy: ActionItemsOrderByInput = "createdAt_DESC"; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - eventId: null, - orderBy, - }; - - const actionItemsByOrganizationPayload = - (await actionItemsByOrganizationResolver?.( - {}, - args, - {}, - )) as InterfaceActionItem[]; - - const actionItemsByOrganizationInfo = await ActionItem.find({ - organization: args.organizationId, - event: args.eventId, - }).lean(); - - expect(actionItemsByOrganizationPayload[0]).toEqual( - expect.objectContaining({ - _id: actionItemsByOrganizationInfo[0]._id, - }), + it(`actionItemsByOrganization - organizationId, eventId, assigneeName`, async () => { + const actionItems = (await actionItemsByOrganization?.( + {}, + { + organizationId: testOrganization?._id, + eventId: testEvent?._id, + where: { + categoryName: "Test Action Item Category 1", + assigneeName: testUser1?.firstName, + }, + }, + {}, + )) as unknown as InterfaceActionItem[]; + expect(actionItems[0].assigneeType).toEqual("EventVolunteer"); + expect(actionItems[0].assignee.user.firstName).toEqual( + testUser1?.firstName, ); }); - it(`returns list of all action items associated with an organization and belonging to an action item category`, async () => { - const actionItemCategories = await ActionItemCategory.find({ - organizationId: testOrganization?._id, - }); - - const actionItemCategoriesIds = actionItemCategories.map( - (category) => category._id, - ); - - const actionItemCategoryId = actionItemCategoriesIds[0]; - - const where: ActionItemWhereInput = { - actionItemCategory_id: actionItemCategoryId.toString(), - }; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - where, - }; - - const actionItemsByOrganizationPayload = - await actionItemsByOrganizationResolver?.({}, args, {}); - - const actionItemsByOrganizationInfo = await ActionItem.find({ - actionItemCategoryId, - }).lean(); - - expect(actionItemsByOrganizationPayload).toEqual( - actionItemsByOrganizationInfo, - ); - }); - it(`returns list of all action items associated with an organization that are active`, async () => { - const where: ActionItemWhereInput = { - is_completed: false, - }; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - eventId: testEvent?._id, - where, - }; - - const actionItemsByOrganizationPayload = - (await actionItemsByOrganizationResolver?.( - {}, - args, - {}, - )) as InterfaceActionItem[]; - - const actionItemsByOrganizationInfo = await ActionItem.find({ - organization: args.organizationId, - event: args.eventId, - }).lean(); - - expect(actionItemsByOrganizationPayload[0]).toEqual( - expect.objectContaining({ - _id: actionItemsByOrganizationInfo[1]._id, - }), - ); - }); - - it(`returns list of all action items associated with an organization that are completed`, async () => { - const where: ActionItemWhereInput = { - is_completed: true, - }; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - eventId: testEvent?._id, - where, - }; - - const actionItemsByOrganizationPayload = - (await actionItemsByOrganizationResolver?.( - {}, - args, - {}, - )) as InterfaceActionItem[]; - - const actionItemsByOrganizationInfo = await ActionItem.find({ - organization: args.organizationId, - event: args.eventId, - }).lean(); - - expect(actionItemsByOrganizationPayload[0]).toEqual( - expect.objectContaining({ - _id: actionItemsByOrganizationInfo[0]._id, - }), - ); - }); - - it(`returns list of all action items matching categoryName Filter`, async () => { - const where: ActionItemWhereInput = { - categoryName: "Default", - }; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - eventId: testEvent?._id, - where, - }; - - const actionItemsByOrganizationPayload = - (await actionItemsByOrganizationResolver?.( - {}, - args, - {}, - )) as InterfaceActionItem[]; - - const actionItemsByOrganizationInfo = await ActionItem.find({ - organization: args.organizationId, - event: args.eventId, - }).lean(); - - expect(actionItemsByOrganizationPayload[0].actionItemCategory).toEqual( - expect.objectContaining({ - _id: actionItemsByOrganizationInfo[0].actionItemCategory, - }), - ); - }); - - it(`returns list of all action items matching assigneeName Filter`, async () => { - const where: ActionItemWhereInput = { - assigneeName: testAssigneeUser?.firstName, - }; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - eventId: testEvent?._id, - where, - }; - - const actionItemsByOrganizationPayload = - (await actionItemsByOrganizationResolver?.( - {}, - args, - {}, - )) as InterfaceActionItem[]; - - const actionItemsByOrganizationInfo = await ActionItem.find({ - organization: args.organizationId, - event: args.eventId, - }).lean(); - - expect(actionItemsByOrganizationPayload[0].assignee).toEqual( - expect.objectContaining({ - _id: actionItemsByOrganizationInfo[0].assignee, - }), - ); + it(`actionItemsByOrganization - organizationId, assigneeName`, async () => { + const actionItems = (await actionItemsByOrganization?.( + {}, + { + organizationId: testOrganization?._id, + where: { + assigneeName: testUser1?.firstName, + }, + }, + {}, + )) as unknown as InterfaceActionItem[]; + expect(actionItems[0].assigneeType).toEqual("User"); + expect(actionItems[0].assigneeUser.firstName).toEqual(testUser1?.firstName); }); }); diff --git a/tests/resolvers/Query/actionItemsByUser.spec.ts b/tests/resolvers/Query/actionItemsByUser.spec.ts new file mode 100644 index 0000000000..e5929381b6 --- /dev/null +++ b/tests/resolvers/Query/actionItemsByUser.spec.ts @@ -0,0 +1,98 @@ +import type mongoose from "mongoose"; +import { connect, disconnect } from "../../helpers/db"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { TestUserType } from "../../helpers/user"; +import type { TestOrganizationType } from "../../helpers/userAndOrg"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import type { InterfaceActionItem } from "../../../src/models"; +import { ActionItem } from "../../../src/models"; +import type { TestActionItemType } from "../../helpers/actionItem"; +import { actionItemsByUser } from "../../../src/resolvers/Query/actionItemsByUser"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testOrganization: TestOrganizationType; +let testUser1: TestUserType; +let testActionItem1: TestActionItemType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const [organization, , user1, , , , , actionItem1] = + await createVolunteerAndActions(); + + testOrganization = organization; + testUser1 = user1; + testActionItem1 = actionItem1; + + await ActionItem.create({ + creator: testUser1?._id, + assigner: testUser1?._id, + assigneeUser: testUser1?._id, + assigneeType: "User", + assignee: null, + assigneeGroup: null, + actionItemCategory: testActionItem1.actionItemCategory, + event: null, + organization: testOrganization?._id, + allottedHours: 2, + assignmentDate: new Date(), + dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 3000), + isCompleted: false, + }); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Query -> actionItemsByUser", () => { + it(`actionItemsByUser for userId, categoryName, dueDate_ASC`, async () => { + const actionItems = (await actionItemsByUser?.( + {}, + { + userId: testUser1?._id.toString() ?? "testUserId", + orderBy: "dueDate_ASC", + where: { + categoryName: "Test Action Item Category 1", + orgId: testOrganization?._id.toString(), + }, + }, + {}, + )) as unknown as InterfaceActionItem[]; + expect(actionItems[0].assigneeType).toEqual("EventVolunteer"); + expect(actionItems[1].assigneeType).toEqual("EventVolunteerGroup"); + }); + + it(`actionItemsByUser for userId, assigneeName, dueDate_DESC`, async () => { + const actionItems = (await actionItemsByUser?.( + {}, + { + userId: testUser1?._id.toString() ?? "testUserId", + orderBy: "dueDate_DESC", + where: { + categoryName: "Test Action Item Category 1", + assigneeName: testUser1?.firstName, + orgId: testOrganization?._id.toString(), + }, + }, + {}, + )) as unknown as InterfaceActionItem[]; + expect(actionItems[1].assignee.user.firstName).toEqual( + testUser1?.firstName, + ); + }); + + it(`actionItemsByUser for userId, assigneeName doesn't match`, async () => { + const actionItems = (await actionItemsByUser?.( + {}, + { + userId: testUser1?._id.toString() ?? "testUserId", + where: { + assigneeName: "xyz", + orgId: testOrganization?._id.toString(), + }, + }, + {}, + )) as unknown as InterfaceActionItem[]; + expect(actionItems.length).toEqual(0); + }); +}); diff --git a/tests/resolvers/Query/eventVolunteersByEvent.spec.ts b/tests/resolvers/Query/eventVolunteersByEvent.spec.ts deleted file mode 100644 index 7c02d96f0e..0000000000 --- a/tests/resolvers/Query/eventVolunteersByEvent.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type mongoose from "mongoose"; -import { connect, disconnect } from "../../helpers/db"; -import { eventVolunteersByEvent } from "../../../src/resolvers/Query/eventVolunteersByEvent"; -import { beforeAll, afterAll, describe, it, expect } from "vitest"; -import type { TestEventType } from "../../helpers/events"; -import { createTestEventAndVolunteer } from "../../helpers/events"; -import { EventVolunteer } from "../../../src/models"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let testEvent: TestEventType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - const temp = await createTestEventAndVolunteer(); - testEvent = temp[2]; -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> Mutation -> eventVolunteersByEvent", () => { - it(`returns list of all existing event volunteers with eventId === args.id`, async () => { - const volunteersPayload = await eventVolunteersByEvent?.( - {}, - { - id: testEvent?._id, - }, - {}, - ); - - const volunteers = await EventVolunteer.find({ - eventId: testEvent?._id, - }) - .populate("userId", "-password") - .lean(); - - expect(volunteersPayload).toEqual(volunteers); - }); -}); diff --git a/tests/resolvers/Query/eventsByOrganizationConnection.spec.ts b/tests/resolvers/Query/eventsByOrganizationConnection.spec.ts index 674ad19e97..15d1ce6492 100644 --- a/tests/resolvers/Query/eventsByOrganizationConnection.spec.ts +++ b/tests/resolvers/Query/eventsByOrganizationConnection.spec.ts @@ -40,24 +40,50 @@ beforeAll(async () => { await dropAllCollectionsFromDatabase(MONGOOSE_INSTANCE); [testUser, testOrganization] = await createTestUserAndOrganization(); const testEvent1 = await createEventWithRegistrant( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - testUser!._id.toString(), + testUser?._id.toString() ?? "defaultUserId", testOrganization?._id, true, ); const testEvent2 = await createEventWithRegistrant( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - testUser!._id.toString(), + testUser?._id.toString() ?? "defaultUserId", testOrganization?._id, false, ); const testEvent3 = await createEventWithRegistrant( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - testUser!._id.toString(), + testUser?._id.toString() ?? "defaultUserId", testOrganization?._id, false, ); - testEvents = [testEvent1, testEvent2, testEvent3]; + const testEvent4 = await createEventWithRegistrant( + testUser?._id.toString() ?? "defaultUserId", + testOrganization?._id, + false, + ); + const testEvent5 = await createEventWithRegistrant( + testUser?._id.toString() ?? "defaultUserId", + testOrganization?._id, + false, + ); + + if (testEvent4) { + const today = new Date(); + const nextWeek = addDays(today, 7); + testEvent4.startDate = nextWeek.toISOString().split("T")[0]; + testEvent4.endDate = nextWeek.toISOString().split("T")[0]; + await testEvent4.save(); + } + + if (testEvent5) { + // set endDate to today and set endTime to 1 min from now + const today = new Date(); + testEvent5.endDate = today.toISOString().split("T")[0]; + testEvent5.endTime = new Date( + today.setMinutes(today.getMinutes() + 1), + ).toISOString(); + await testEvent5.save(); + } + + testEvents = [testEvent1, testEvent2, testEvent3, testEvent4, testEvent5]; }); afterAll(async () => { @@ -639,4 +665,19 @@ describe("resolvers -> Query -> organizationsMemberConnection", () => { vi.useRealTimers(); }); + + it("fetch upcoming events for the current date", async () => { + const upcomingEvents = (await eventsByOrganizationConnectionResolver?.( + {}, + { + upcomingOnly: true, + where: { + organization_id: testOrganization?._id, + }, + }, + {}, + )) as unknown as InterfaceEvent[]; + expect(upcomingEvents[0]?._id).toEqual(testEvents[3]?._id); + expect(upcomingEvents[1]?._id).toEqual(testEvents[4]?._id); + }); }); diff --git a/tests/resolvers/Query/getEventVolunteerGroups.spec.ts b/tests/resolvers/Query/getEventVolunteerGroups.spec.ts index a52a9a49d4..ab95e31c01 100644 --- a/tests/resolvers/Query/getEventVolunteerGroups.spec.ts +++ b/tests/resolvers/Query/getEventVolunteerGroups.spec.ts @@ -1,23 +1,52 @@ import type mongoose from "mongoose"; import { connect, disconnect } from "../../helpers/db"; -import { getEventVolunteerGroups } from "../../../src/resolvers/Query/getEventVolunteerGroups"; import { beforeAll, afterAll, describe, it, expect } from "vitest"; import type { TestEventType, TestEventVolunteerGroupType, + TestEventVolunteerType, } from "../../helpers/events"; -import { createTestEventVolunteerGroup } from "../../helpers/events"; -import type { EventVolunteerGroup } from "../../../src/types/generatedGraphQLTypes"; +import type { TestUserType } from "../../helpers/user"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import type { InterfaceEventVolunteerGroup } from "../../../src/models"; +import { EventVolunteer, EventVolunteerGroup } from "../../../src/models"; +import { getEventVolunteerGroups } from "../../../src/resolvers/Query/getEventVolunteerGroups"; +import type { TestOrganizationType } from "../../helpers/userAndOrg"; let MONGOOSE_INSTANCE: typeof mongoose; +let testOrganization: TestOrganizationType; let testEvent: TestEventType; -let testEventVolunteerGroup: TestEventVolunteerGroupType; +let testUser1: TestUserType; +let testEventVolunteer1: TestEventVolunteerType; +let testVolunteerGroup1: TestEventVolunteerGroupType; +let testVolunteerGroup2: TestEventVolunteerGroupType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); - const temp = await createTestEventVolunteerGroup(); - testEvent = temp[2]; - testEventVolunteerGroup = temp[4]; + const [organization, event, user1, , volunteer1, , volunteerGroup] = + await createVolunteerAndActions(); + + testOrganization = organization; + testEvent = event; + testUser1 = user1; + testEventVolunteer1 = volunteer1; + testVolunteerGroup1 = volunteerGroup; + testVolunteerGroup2 = await EventVolunteerGroup.create({ + creator: testUser1?._id, + event: testEvent?._id, + volunteers: [testEventVolunteer1?._id], + leader: testUser1?._id, + assignments: [], + name: "Test Volunteer Group 2", + }); + + await EventVolunteer.updateOne( + { _id: testEventVolunteer1?._id }, + { groups: [testVolunteerGroup1?._id, testVolunteerGroup2?._id] }, + { + new: true, + }, + ); }); afterAll(async () => { @@ -25,31 +54,77 @@ afterAll(async () => { }); describe("resolvers -> Query -> getEventVolunteerGroups", () => { - it(`returns list of all existing event volunteer groups with eventId === args.where.eventId`, async () => { - const volunteerGroupsPayload = (await getEventVolunteerGroups?.( + it(`getEventVolunteerGroups - eventId, name_contains, orderBy is volunteers_ASC`, async () => { + const groups = (await getEventVolunteerGroups?.( {}, { where: { eventId: testEvent?._id, + name_contains: testVolunteerGroup1.name, + leaderName: testUser1?.firstName, }, + orderBy: "volunteers_ASC", }, {}, - )) as unknown as EventVolunteerGroup[]; + )) as unknown as InterfaceEventVolunteerGroup[]; + + expect(groups[0].name).toEqual(testVolunteerGroup1.name); + }); + + it(`getEventVolunteerGroups - userId, orgId, orderBy is volunteers_DESC`, async () => { + const groups = (await getEventVolunteerGroups?.( + {}, + { + where: { + userId: testUser1?._id.toString(), + orgId: testOrganization?._id, + }, + orderBy: "volunteers_DESC", + }, + {}, + )) as unknown as InterfaceEventVolunteerGroup[]; + expect(groups.length).toEqual(2); + }); - expect(volunteerGroupsPayload[0]._id).toEqual(testEventVolunteerGroup._id); + it(`getEventVolunteerGroups - eventId, orderBy is assignments_ASC`, async () => { + const groups = (await getEventVolunteerGroups?.( + {}, + { + where: { + eventId: testEvent?._id, + }, + orderBy: "assignments_ASC", + }, + {}, + )) as unknown as InterfaceEventVolunteerGroup[]; + expect(groups[0].name).toEqual(testVolunteerGroup2.name); }); - it(`returns empty list of all existing event volunteer groups with eventId !== args.where.eventId`, async () => { - const volunteerGroupsPayload = (await getEventVolunteerGroups?.( + it(`getEventVolunteerGroups - eventId, orderBy is assignements_DESC`, async () => { + const groups = (await getEventVolunteerGroups?.( {}, { where: { - eventId: "123456789012345678901234", + eventId: testEvent?._id, }, + orderBy: "assignments_DESC", }, {}, - )) as unknown as EventVolunteerGroup[]; + )) as unknown as InterfaceEventVolunteerGroup[]; + expect(groups[0].name).toEqual(testVolunteerGroup1.name); + }); - expect(volunteerGroupsPayload).toEqual([]); + it(`getEventVolunteerGroups - userId, wrong orgId`, async () => { + const groups = (await getEventVolunteerGroups?.( + {}, + { + where: { + userId: testUser1?._id.toString(), + orgId: testEvent?._id, + }, + }, + {}, + )) as unknown as InterfaceEventVolunteerGroup[]; + expect(groups).toEqual([]); }); }); diff --git a/tests/resolvers/Query/getEventVolunteers.spec.ts b/tests/resolvers/Query/getEventVolunteers.spec.ts new file mode 100644 index 0000000000..93c532c0d3 --- /dev/null +++ b/tests/resolvers/Query/getEventVolunteers.spec.ts @@ -0,0 +1,62 @@ +import type mongoose from "mongoose"; +import { connect, disconnect } from "../../helpers/db"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { + TestEventType, + TestEventVolunteerGroupType, + TestEventVolunteerType, +} from "../../helpers/events"; +import type { TestUserType } from "../../helpers/user"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import type { InterfaceEventVolunteer } from "../../../src/models"; +import { getEventVolunteers } from "../../../src/resolvers/Query/getEventVolunteers"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testEvent: TestEventType; +let testUser1: TestUserType; +let testEventVolunteer1: TestEventVolunteerType; +let testVolunteerGroup: TestEventVolunteerGroupType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const [, event, user1, , volunteer1, , volunteerGroup, ,] = + await createVolunteerAndActions(); + + testEvent = event; + testUser1 = user1; + testEventVolunteer1 = volunteer1; + testVolunteerGroup = volunteerGroup; +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Query -> getEventVolunteers", () => { + it(`getEventVolunteers - eventId, name_contains`, async () => { + const eventVolunteers = (await getEventVolunteers?.( + {}, + { + where: { + eventId: testEvent?._id, + name_contains: testUser1?.firstName, + }, + }, + {}, + )) as unknown as InterfaceEventVolunteer[]; + expect(eventVolunteers[0].user.firstName).toEqual(testUser1?.firstName); + }); + it(`getEventVolunteers - eventId, groupId`, async () => { + const eventVolunteers = (await getEventVolunteers?.( + {}, + { + where: { + eventId: testEvent?._id, + groupId: testVolunteerGroup?._id, + }, + }, + {}, + )) as unknown as InterfaceEventVolunteer[]; + expect(eventVolunteers[0]._id).toEqual(testEventVolunteer1?._id); + }); +}); diff --git a/tests/resolvers/Query/getVolunteerMembership.spec.ts b/tests/resolvers/Query/getVolunteerMembership.spec.ts new file mode 100644 index 0000000000..ecc68c5d3a --- /dev/null +++ b/tests/resolvers/Query/getVolunteerMembership.spec.ts @@ -0,0 +1,170 @@ +import type mongoose from "mongoose"; +import { connect, disconnect } from "../../helpers/db"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { + TestEventType, + TestEventVolunteerGroupType, + TestEventVolunteerType, +} from "../../helpers/events"; +import type { TestUserType } from "../../helpers/user"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import type { InterfaceVolunteerMembership } from "../../../src/models"; +import { VolunteerMembership } from "../../../src/models"; +import { getVolunteerMembership } from "../../../src/resolvers/Query/getVolunteerMembership"; + +export enum MembershipStatus { + INVITED = "invited", + REQUESTED = "requested", + ACCEPTED = "accepted", + REJECTED = "rejected", +} + +let MONGOOSE_INSTANCE: typeof mongoose; +let testEvent: TestEventType; +let testUser1: TestUserType; +let testEventVolunteer1: TestEventVolunteerType; +let testEventVolunteerGroup: TestEventVolunteerGroupType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const [, event, user1, , volunteer1, , volunteerGroup, ,] = + await createVolunteerAndActions(); + + testEvent = event; + testUser1 = user1; + testEventVolunteer1 = volunteer1; + testEventVolunteerGroup = volunteerGroup; + + await VolunteerMembership.insertMany([ + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + status: MembershipStatus.INVITED, + }, + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + group: testEventVolunteerGroup._id, + status: MembershipStatus.REQUESTED, + }, + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + status: MembershipStatus.ACCEPTED, + }, + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + status: MembershipStatus.REJECTED, + }, + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + status: MembershipStatus.INVITED, + group: testEventVolunteerGroup._id, + }, + ]); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Query -> getVolunteerMembership", () => { + it(`getVolunteerMembership for userId, status invited`, async () => { + const volunteerMemberships = (await getVolunteerMembership?.( + {}, + { + where: { + userId: testUser1?._id.toString(), + status: MembershipStatus.INVITED, + }, + }, + {}, + )) as unknown as InterfaceVolunteerMembership[]; + expect(volunteerMemberships[0].volunteer?._id).toEqual( + testEventVolunteer1?._id, + ); + expect(volunteerMemberships[0].status).toEqual(MembershipStatus.INVITED); + }); + + it(`getVolunteerMembership for eventId, status accepted`, async () => { + const volunteerMemberships = (await getVolunteerMembership?.( + {}, + { + where: { + eventId: testEvent?._id, + eventTitle: testEvent?.title, + status: MembershipStatus.ACCEPTED, + }, + orderBy: "createdAt_ASC", + }, + {}, + )) as unknown as InterfaceVolunteerMembership[]; + expect(volunteerMemberships[0].volunteer?._id).toEqual( + testEventVolunteer1?._id, + ); + expect(volunteerMemberships[0].status).toEqual(MembershipStatus.ACCEPTED); + expect(volunteerMemberships[0].event._id).toEqual(testEvent?._id); + }); + + it(`getVolunteerMembership for eventId, filter group, userName`, async () => { + const volunteerMemberships = (await getVolunteerMembership?.( + {}, + { + where: { + eventId: testEvent?._id, + filter: "group", + userName: testUser1?.firstName, + }, + }, + {}, + )) as unknown as InterfaceVolunteerMembership[]; + expect(volunteerMemberships[0].volunteer?._id).toEqual( + testEventVolunteer1?._id, + ); + expect(volunteerMemberships[0].status).toEqual(MembershipStatus.REQUESTED); + expect(volunteerMemberships[0].event._id).toEqual(testEvent?._id); + expect(volunteerMemberships[0].group?._id).toEqual( + testEventVolunteerGroup?._id, + ); + }); + + it(`getVolunteerMembership for userId`, async () => { + const volunteerMemberships = (await getVolunteerMembership?.( + {}, + { + where: { + userId: testUser1?._id.toString(), + filter: "individual", + }, + }, + {}, + )) as unknown as InterfaceVolunteerMembership[]; + expect(volunteerMemberships.length).toEqual(3); + expect(volunteerMemberships[0].group).toBeUndefined(); + expect(volunteerMemberships[1].group).toBeUndefined(); + expect(volunteerMemberships[2].group).toBeUndefined(); + }); + + it(`getVolunteerMembership for eventId, groupId`, async () => { + const volunteerMemberships = (await getVolunteerMembership?.( + {}, + { + where: { + eventId: testEvent?._id, + groupId: testEventVolunteerGroup?._id, + filter: "group", + userName: testUser1?.firstName, + }, + }, + {}, + )) as unknown as InterfaceVolunteerMembership[]; + expect(volunteerMemberships[0].volunteer?._id).toEqual( + testEventVolunteer1?._id, + ); + expect(volunteerMemberships[0].group?._id).toEqual( + testEventVolunteerGroup?._id, + ); + }); +}); diff --git a/tests/resolvers/Query/getVolunteerRanks.spec.ts b/tests/resolvers/Query/getVolunteerRanks.spec.ts new file mode 100644 index 0000000000..0f5d6973d5 --- /dev/null +++ b/tests/resolvers/Query/getVolunteerRanks.spec.ts @@ -0,0 +1,100 @@ +import type mongoose from "mongoose"; +import { connect, disconnect } from "../../helpers/db"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { VolunteerRank } from "../../../src/types/generatedGraphQLTypes"; +import type { TestUserType } from "../../helpers/user"; +import type { TestOrganizationType } from "../../helpers/userAndOrg"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import { getVolunteerRanks } from "../../../src/resolvers/Query/getVolunteerRanks"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testOrganization: TestOrganizationType; +let testUser1: TestUserType; +let testUser2: TestUserType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const [organization, , user1, user2] = await createVolunteerAndActions(); + testOrganization = organization; + testUser1 = user1; + testUser2 = user2; +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Query -> getVolunteerRanks", () => { + it(`getVolunteerRanks for allTime, descending, no limit/name`, async () => { + const volunteerRanks = (await getVolunteerRanks?.( + {}, + { + orgId: testOrganization?._id, + where: { + timeFrame: "allTime", + orderBy: "hours_DESC", + }, + }, + {}, + )) as unknown as VolunteerRank[]; + expect(volunteerRanks[0].hoursVolunteered).toEqual(10); + expect(volunteerRanks[0].user._id).toEqual(testUser1?._id); + expect(volunteerRanks[0].rank).toEqual(1); + expect(volunteerRanks[1].hoursVolunteered).toEqual(8); + expect(volunteerRanks[1].user._id).toEqual(testUser2?._id); + expect(volunteerRanks[1].rank).toEqual(2); + }); + + it(`getVolunteerRanks for weekly, descending, limit, no name`, async () => { + const volunteerRanks = (await getVolunteerRanks?.( + {}, + { + orgId: testOrganization?._id, + where: { + timeFrame: "weekly", + orderBy: "hours_DESC", + limit: 1, + }, + }, + {}, + )) as unknown as VolunteerRank[]; + expect(volunteerRanks[0].hoursVolunteered).toEqual(2); + expect(volunteerRanks[0].user._id).toEqual(testUser1?._id); + expect(volunteerRanks[0].rank).toEqual(1); + }); + + it(`getVolunteerRanks for monthly, descending, name, no limit`, async () => { + const volunteerRanks = (await getVolunteerRanks?.( + {}, + { + orgId: testOrganization?._id, + where: { + timeFrame: "monthly", + orderBy: "hours_ASC", + nameContains: testUser1?.firstName, + }, + }, + {}, + )) as unknown as VolunteerRank[]; + expect(volunteerRanks[0].hoursVolunteered).toEqual(2); + expect(volunteerRanks[0].user._id).toEqual(testUser1?._id); + expect(volunteerRanks[0].rank).toEqual(1); + }); + + it(`getVolunteerRanks for yearly, descending, no name/limit`, async () => { + const volunteerRanks = (await getVolunteerRanks?.( + {}, + { + orgId: testOrganization?._id, + where: { + timeFrame: "yearly", + orderBy: "hours_DESC", + }, + }, + {}, + )) as unknown as VolunteerRank[]; + expect(volunteerRanks[0].hoursVolunteered).toEqual(8); + expect(volunteerRanks[0].user._id).toEqual(testUser1?._id); + expect(volunteerRanks[0].rank).toEqual(1); + }); +}); diff --git a/tests/resolvers/Query/helperFunctions/getSort.spec.ts b/tests/resolvers/Query/helperFunctions/getSort.spec.ts index 10b0b1668c..98c5ca9434 100644 --- a/tests/resolvers/Query/helperFunctions/getSort.spec.ts +++ b/tests/resolvers/Query/helperFunctions/getSort.spec.ts @@ -9,6 +9,9 @@ import type { VenueOrderByInput, FundOrderByInput, CampaignOrderByInput, + ActionItemsOrderByInput, + EventVolunteersOrderByInput, + VolunteerMembershipOrderByInput, } from "../../../../src/types/generatedGraphQLTypes"; describe("getSort function", () => { @@ -61,6 +64,8 @@ describe("getSort function", () => { ["fundingGoal_DESC", { fundingGoal: -1 }], ["dueDate_ASC", { dueDate: 1 }], ["dueDate_DESC", { dueDate: -1 }], + ["hoursVolunteered_ASC", { hoursVolunteered: 1 }], + ["hoursVolunteered_DESC", { hoursVolunteered: -1 }], ]; it.each(testCases)( @@ -75,7 +80,10 @@ describe("getSort function", () => { | VenueOrderByInput | PledgeOrderByInput | FundOrderByInput - | CampaignOrderByInput, + | CampaignOrderByInput + | ActionItemsOrderByInput + | EventVolunteersOrderByInput + | VolunteerMembershipOrderByInput, ); expect(result).toEqual(expected); }, diff --git a/tests/resolvers/Query/helperFunctions/getWhere.spec.ts b/tests/resolvers/Query/helperFunctions/getWhere.spec.ts index 3c3494c9b6..b6e8c12faa 100644 --- a/tests/resolvers/Query/helperFunctions/getWhere.spec.ts +++ b/tests/resolvers/Query/helperFunctions/getWhere.spec.ts @@ -13,6 +13,7 @@ import type { EventVolunteerGroupWhereInput, PledgeWhereInput, ActionItemCategoryWhereInput, + EventVolunteerWhereInput, } from "../../../../src/types/generatedGraphQLTypes"; describe("getWhere function", () => { @@ -30,7 +31,8 @@ describe("getWhere function", () => { FundWhereInput & CampaignWhereInput & VenueWhereInput & - PledgeWhereInput + PledgeWhereInput & + EventVolunteerWhereInput >, Record, ][] = [ @@ -335,17 +337,10 @@ describe("getWhere function", () => { { organizationId: "6f6cd" }, ], ["campaignId", { campaignId: "6f6c" }, { _id: "6f6c" }], - [ - "volunteerId", - { volunteerId: "6f43d" }, - { - volunteers: { - $in: ["6f43d"], - }, - }, - ], ["is_disabled", { is_disabled: true }, { isDisabled: true }], ["is_disabled", { is_disabled: false }, { isDisabled: false }], + ["hasAccepted", { hasAccepted: true }, { hasAccepted: true }], + ["hasAccepted", { hasAccepted: false }, { hasAccepted: false }], ]; it.each(testCases)( diff --git a/tests/utilities/adminCheck.spec.ts b/tests/utilities/adminCheck.spec.ts index 84e2c41798..d5ba8b0662 100644 --- a/tests/utilities/adminCheck.spec.ts +++ b/tests/utilities/adminCheck.spec.ts @@ -16,6 +16,8 @@ import { AppUserProfile, Organization } from "../../src/models"; import { connect, disconnect } from "../helpers/db"; import type { TestOrganizationType, TestUserType } from "../helpers/userAndOrg"; import { createTestUserAndOrganization } from "../helpers/userAndOrg"; +import { adminCheck } from "../../src/utilities"; +import { requestContext } from "../../src/libraries"; let testUser: TestUserType; let testOrganization: TestOrganizationType; @@ -38,14 +40,11 @@ describe("utilities -> adminCheck", () => { }); it("throws error if userIsOrganizationAdmin === false and isUserSuperAdmin === false", async () => { - const { requestContext } = await import("../../src/libraries"); - const spy = vi .spyOn(requestContext, "translate") .mockImplementationOnce((message) => `Translated ${message}`); try { - const { adminCheck } = await import("../../src/utilities"); await adminCheck( testUser?._id, testOrganization ?? ({} as InterfaceOrganization), @@ -58,6 +57,16 @@ describe("utilities -> adminCheck", () => { expect(spy).toBeCalledWith(USER_NOT_AUTHORIZED_ADMIN.MESSAGE); }); + it("Returns boolean if userIsOrganizationAdmin === false and isUserSuperAdmin === false and throwError is false", async () => { + expect( + await adminCheck( + testUser?._id, + testOrganization ?? ({} as InterfaceOrganization), + false, + ), + ).toEqual(false); + }); + it("throws no error if userIsOrganizationAdmin === false and isUserSuperAdmin === true", async () => { const updatedUser = await AppUserProfile.findOneAndUpdate( { @@ -72,12 +81,11 @@ describe("utilities -> adminCheck", () => { }, ); - const { adminCheck } = await import("../../src/utilities"); - await expect( adminCheck( updatedUser?.userId?.toString() ?? "", testOrganization ?? ({} as InterfaceOrganization), + false, ), ).resolves.not.toThrowError(); }); @@ -98,8 +106,6 @@ describe("utilities -> adminCheck", () => { }, ); - const { adminCheck } = await import("../../src/utilities"); - await expect( adminCheck( testUser?._id, @@ -108,14 +114,11 @@ describe("utilities -> adminCheck", () => { ).resolves.not.toThrowError(); }); it("throws error if user is not found with the specific Id", async () => { - const { requestContext } = await import("../../src/libraries"); - const spy = vi .spyOn(requestContext, "translate") .mockImplementationOnce((message) => `Translated ${message}`); try { - const { adminCheck } = await import("../../src/utilities"); await adminCheck( new mongoose.Types.ObjectId(), testOrganization ?? ({} as InterfaceOrganization), diff --git a/tests/utilities/checks.spec.ts b/tests/utilities/checks.spec.ts new file mode 100644 index 0000000000..6cf1f3744e --- /dev/null +++ b/tests/utilities/checks.spec.ts @@ -0,0 +1,156 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + it, + vi, +} from "vitest"; +import { + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, + USER_NOT_FOUND_ERROR, +} from "../../src/constants"; + +import type { InterfaceUser } from "../../src/models"; +import { AppUserProfile, VolunteerMembership } from "../../src/models"; +import { connect, disconnect } from "../helpers/db"; +import type { TestUserType } from "../helpers/userAndOrg"; +import { requestContext } from "../../src/libraries"; +import { + checkAppUserProfileExists, + checkEventVolunteerExists, + checkUserExists, + checkVolunteerGroupExists, + checkVolunteerMembershipExists, +} from "../../src/utilities/checks"; +import { createTestUser } from "../helpers/user"; +import { createVolunteerAndActions } from "../helpers/volunteers"; +import type { TestEventVolunteerType } from "../helpers/events"; +import type { TestEventVolunteerGroupType } from "../resolvers/Mutation/createEventVolunteer.spec"; + +let randomUser: InterfaceUser; +let testUser: TestUserType; +let testVolunteer: TestEventVolunteerType; +let testGroup: TestEventVolunteerGroupType; +let MONGOOSE_INSTANCE: typeof mongoose; + +const expectError = async ( + fn: () => Promise, + expectedMessage: string, +): Promise => { + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementationOnce((message) => `Translated ${message}`); + + try { + await fn(); + } catch (error: unknown) { + expect((error as Error).message).toEqual(`Translated ${expectedMessage}`); + } + + expect(spy).toBeCalledWith(expectedMessage); +}; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + + const [, , user1, , volunteer1, , volunteerGroup] = + await createVolunteerAndActions(); + + testUser = user1; + testVolunteer = volunteer1; + testGroup = volunteerGroup; + + randomUser = (await createTestUser()) as InterfaceUser; + await AppUserProfile.deleteOne({ + userId: randomUser._id, + }); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("utilities -> checks", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("checkUserExists -> invalid userId", async () => { + await expectError( + () => checkUserExists(testUser?.appUserProfileId), + USER_NOT_FOUND_ERROR.MESSAGE, + ); + }); + + it("checkUserExists -> valid userId", async () => { + expect((await checkUserExists(testUser?._id))._id).toEqual(testUser?._id); + }); + + it("checkAppUserProfileExists -> unauthorized user", async () => { + await expectError( + () => checkAppUserProfileExists(randomUser), + USER_NOT_AUTHORIZED_ERROR.MESSAGE, + ); + }); + + it("checkAppUserProfileExists -> authorized user", async () => { + expect( + (await checkAppUserProfileExists(testUser as InterfaceUser)).userId, + ).toEqual(testUser?._id); + }); + + it("checkEventVolunteerExists -> invalid volunteerId", async () => { + await expectError( + () => checkEventVolunteerExists(testUser?._id), + EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE, + ); + }); + + it("checkEventVolunteerExists -> valid volunteerId", async () => { + expect((await checkEventVolunteerExists(testVolunteer?._id))._id).toEqual( + testVolunteer?._id, + ); + }); + + it("checkVolunteerGroupExists -> invalid groupId", async () => { + await expectError( + () => checkVolunteerGroupExists(testUser?._id), + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE, + ); + }); + + it("checkVolunteerGroupExists -> valid groupId", async () => { + expect((await checkVolunteerGroupExists(testGroup?._id))._id).toEqual( + testGroup?._id, + ); + }); + + it("checkVolunteerMembershipExists -> invalid membershipId", async () => { + await expectError( + () => checkVolunteerMembershipExists(testUser?._id), + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR.MESSAGE, + ); + }); + + it("checkVolunteerMembershipExists -> valid membershipId", async () => { + const volunteerMembership = await VolunteerMembership.create({ + event: testVolunteer?._id, + volunteer: testUser?._id, + status: "invited", + }); + expect( + ( + await checkVolunteerMembershipExists( + volunteerMembership?._id.toString(), + ) + )._id, + ).toEqual(volunteerMembership?._id); + }); +}); From e22122f2aa65c7149c8585a76cb5fbd20c5b39bc Mon Sep 17 00:00:00 2001 From: Shekhar Patel <90516956+duplixx@users.noreply.github.com> Date: Sun, 3 Nov 2024 14:48:20 +0530 Subject: [PATCH 5/6] Events attended support added with fetching recurring event query (#2551) * eventsAttended added * events attended support added * eventsAttended added in user * changes implemented for eventsattended * changes added in add Event Attendee * added event supported * added eventsattended on expected reponse * added suggested changes * added expected payload * formatting done * added test cases * wip * added test cases for both queries * minor format changes * minor changes * minor * minor * reverted the updated user error handling * .. * fixed model error --------- Co-authored-by: Dominic Mills --- config/vitestSetup.ts | 1 - schema.graphql | 4 + src/models/User.ts | 8 + src/resolvers/Mutation/addEventAttendee.ts | 20 ++- .../Mutation/updateAgendaCategory.ts | 2 +- src/resolvers/Query/eventsAttendedByUser.ts | 28 ++++ src/resolvers/Query/getRecurringEvents.ts | 25 +++ src/resolvers/Query/index.ts | 5 + .../Query/organizationsMemberConnection.ts | 2 + src/setup/superAdmin.ts | 1 - src/typeDefs/queries.ts | 4 + src/typeDefs/types.ts | 1 + src/types/generatedGraphQLTypes.ts | 22 +++ tests/helpers/userAndOrg.ts | 4 +- .../Mutation/addEventAttendee.spec.ts | 157 ++++++++++++++---- .../Query/eventsAttendedByUser.spec.ts | 62 +++++++ .../Query/getRecurringEvents.spec.ts | 118 +++++++++++++ .../organizationsMemberConnection.spec.ts | 10 ++ tests/resolvers/UserTag/childTags.spec.ts | 1 - .../middleware/currentUserExists.spec.ts | 1 - 20 files changed, 437 insertions(+), 39 deletions(-) create mode 100644 src/resolvers/Query/eventsAttendedByUser.ts create mode 100644 src/resolvers/Query/getRecurringEvents.ts create mode 100644 tests/resolvers/Query/eventsAttendedByUser.spec.ts create mode 100644 tests/resolvers/Query/getRecurringEvents.spec.ts diff --git a/config/vitestSetup.ts b/config/vitestSetup.ts index 44152f1a19..3be116e90c 100644 --- a/config/vitestSetup.ts +++ b/config/vitestSetup.ts @@ -1,6 +1,5 @@ // FAIL LOUDLY on unhandled promise rejections / errors process.on("unhandledRejection", (reason) => { - // eslint-disable-next-line no-console console.log("FAILED TO HANDLE PROMISE REJECTION"); throw reason; }); diff --git a/schema.graphql b/schema.graphql index 9ce8443540..4d94d1d4c7 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1518,6 +1518,8 @@ type Query { customDataByOrganization(organizationId: ID!): [UserCustomData!]! customFieldsByOrganization(id: ID!): [OrganizationCustomField] event(id: ID!): Event + eventVolunteersByEvent(id: ID!): [EventVolunteer] + eventsAttendedByUser(id: ID, orderBy: EventOrderByInput): [Event] eventsByOrganization(id: ID, orderBy: EventOrderByInput): [Event] eventsByOrganizationConnection(first: Int, orderBy: EventOrderByInput, skip: Int, upcomingOnly: Boolean, where: EventWhereInput): [Event!]! fundsByOrganization(orderBy: FundOrderByInput, organizationId: ID!, where: FundWhereInput): [Fund] @@ -1540,6 +1542,7 @@ type Query { getNoteById(id: ID!): Note! getPledgesByUserId(orderBy: PledgeOrderByInput, userId: ID!, where: PledgeWhereInput): [FundraisingCampaignPledge] getPlugins: [Plugin] + getRecurringEvents(baseRecurringEventId: ID!): [Event] getUserTag(id: ID!): UserTag getVenueByOrgId(first: Int, orderBy: VenueOrderByInput, orgId: ID!, skip: Int, where: VenueWhereInput): [Venue] getVolunteerMembership(orderBy: VolunteerMembershipOrderByInput, where: VolunteerMembershipWhereInput!): [VolunteerMembership]! @@ -1848,6 +1851,7 @@ type User { email: EmailAddress! employmentStatus: EmploymentStatus eventAdmin: [Event] + eventsAttended: [Event] firstName: String! gender: Gender identifier: Int! diff --git a/src/models/User.ts b/src/models/User.ts index 3908629a92..990e131a63 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -48,6 +48,7 @@ export interface InterfaceUser { mobile: string; work: string; }; + eventsAttended: PopulatedDoc[]; registeredEvents: PopulatedDoc[]; status: string; @@ -77,6 +78,7 @@ export interface InterfaceUser { * @param phone - User's contact numbers (home, mobile, work). * @param registeredEvents - Events the user has registered for. * @param status - User's status (ACTIVE, BLOCKED, DELETED). + * @param eventsAttended - Events the user has attended. * @param updatedAt - Timestamp of when the user was last updated. */ const userSchema = new Schema( @@ -220,6 +222,12 @@ const userSchema = new Schema( ref: "Event", }, ], + eventsAttended: [ + { + type: Schema.Types.ObjectId, + ref: "Event", + }, + ], status: { type: String, required: true, diff --git a/src/resolvers/Mutation/addEventAttendee.ts b/src/resolvers/Mutation/addEventAttendee.ts index 8c4242c484..d683c2047e 100644 --- a/src/resolvers/Mutation/addEventAttendee.ts +++ b/src/resolvers/Mutation/addEventAttendee.ts @@ -172,5 +172,23 @@ export const addEventAttendee: MutationResolvers["addEventAttendee"] = async ( await EventAttendee.create({ ...args.data }); - return requestUser; + const updatedUser = await User.findByIdAndUpdate( + args.data.userId, + { + $push: { + eventsAttended: args.data.eventId, + }, + }, + { new: true }, + ); + + if (updatedUser === null) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM, + ); + } + + return updatedUser; }; diff --git a/src/resolvers/Mutation/updateAgendaCategory.ts b/src/resolvers/Mutation/updateAgendaCategory.ts index cf2c523930..82105f8c64 100644 --- a/src/resolvers/Mutation/updateAgendaCategory.ts +++ b/src/resolvers/Mutation/updateAgendaCategory.ts @@ -117,7 +117,7 @@ export const updateAgendaCategory: MutationResolvers["updateAgendaCategory"] = { $set: { updatedBy: context.userId, - // eslint-disable-next-line + ...(args.input as UpdateAgendaCategoryInput), }, }, diff --git a/src/resolvers/Query/eventsAttendedByUser.ts b/src/resolvers/Query/eventsAttendedByUser.ts new file mode 100644 index 0000000000..202a962707 --- /dev/null +++ b/src/resolvers/Query/eventsAttendedByUser.ts @@ -0,0 +1,28 @@ +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import { Event } from "../../models"; +import { getSort } from "./helperFunctions/getSort"; + +/** + * This query will fetch all the events for which user attended from the database. + * @param _parent- + * @param args - An object that contains `id` of the user and `orderBy`. + * @returns An object that contains the Event data. + * @remarks The query function uses `getSort()` function to sort the data in specified. + */ +export const eventsAttendedByUser: QueryResolvers["eventsAttendedByUser"] = + async (_parent, args) => { + const sort = getSort(args.orderBy); + + return await Event.find({ + registrants: { + $elemMatch: { + userId: args.id, + status: "ACTIVE", + }, + }, + }) + .sort(sort) + .populate("creatorId", "-password") + .populate("admins", "-password") + .lean(); + }; diff --git a/src/resolvers/Query/getRecurringEvents.ts b/src/resolvers/Query/getRecurringEvents.ts new file mode 100644 index 0000000000..e36ad7bb77 --- /dev/null +++ b/src/resolvers/Query/getRecurringEvents.ts @@ -0,0 +1,25 @@ +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import { Event } from "../../models"; +import type { InterfaceEvent } from "../../models/Event"; +/** + * This query will fetch all the events with the same BaseRecurringEventId from the database. + * @param _parent - + * @param args - An object that contains `baseRecurringEventId` of the base recurring event. + * @returns An array of `Event` objects that are instances of the base recurring event. + */ + +export const getRecurringEvents: QueryResolvers["getRecurringEvents"] = async ( + _parent, + args, +) => { + try { + const recurringEvents = await Event.find({ + baseRecurringEventId: args.baseRecurringEventId, + }).lean(); + + return recurringEvents as InterfaceEvent[]; + } catch (error) { + console.error("Error fetching recurring events:", error); + throw error; + } +}; diff --git a/src/resolvers/Query/index.ts b/src/resolvers/Query/index.ts index a96026ce71..534dec83ce 100644 --- a/src/resolvers/Query/index.ts +++ b/src/resolvers/Query/index.ts @@ -51,8 +51,11 @@ import { getEventAttendeesByEventId } from "./getEventAttendeesByEventId"; import { getVenueByOrgId } from "./getVenueByOrgId"; import { getAllNotesForAgendaItem } from "./getAllNotesForAgendaItem"; import { getNoteById } from "./getNoteById"; +import { eventsAttendedByUser } from "./eventsAttendedByUser"; +import { getRecurringEvents } from "./getRecurringEvents"; import { getVolunteerMembership } from "./getVolunteerMembership"; import { getVolunteerRanks } from "./getVolunteerRanks"; + export const Query: QueryResolvers = { actionItemsByEvent, actionItemsByUser, @@ -85,6 +88,7 @@ export const Query: QueryResolvers = { getNoteById, getlanguage, getPlugins, + getRecurringEvents, getUserTag, isSampleOrganization, me, @@ -106,6 +110,7 @@ export const Query: QueryResolvers = { getEventAttendee, getEventAttendeesByEventId, getVenueByOrgId, + eventsAttendedByUser, getVolunteerMembership, getVolunteerRanks, }; diff --git a/src/resolvers/Query/organizationsMemberConnection.ts b/src/resolvers/Query/organizationsMemberConnection.ts index 0a0c29c2dc..a49434df5e 100644 --- a/src/resolvers/Query/organizationsMemberConnection.ts +++ b/src/resolvers/Query/organizationsMemberConnection.ts @@ -141,6 +141,7 @@ export const organizationsMemberConnection: QueryResolvers["organizationsMemberC registeredEvents: user.registeredEvents, status: user.status, updatedAt: user.updatedAt, + eventsAttended: user.eventsAttended, })); } else { users = usersModel.docs.map((user) => ({ @@ -175,6 +176,7 @@ export const organizationsMemberConnection: QueryResolvers["organizationsMemberC registeredEvents: user.registeredEvents, status: user.status, updatedAt: user.updatedAt, + eventsAttended: user.eventsAttended, })); } diff --git a/src/setup/superAdmin.ts b/src/setup/superAdmin.ts index 2d62240aba..66fb954c45 100644 --- a/src/setup/superAdmin.ts +++ b/src/setup/superAdmin.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ import inquirer from "inquirer"; import { isValidEmail } from "./isValidEmail"; diff --git a/src/typeDefs/queries.ts b/src/typeDefs/queries.ts index 43d6f880af..3d7d5f0223 100644 --- a/src/typeDefs/queries.ts +++ b/src/typeDefs/queries.ts @@ -147,6 +147,8 @@ export const queries = gql` getAllNotesForAgendaItem(agendaItemId: ID!): [Note] + getRecurringEvents(baseRecurringEventId: ID!): [Event] + advertisementsConnection( after: String before: String @@ -213,5 +215,7 @@ export const queries = gql` ): [UserData]! @auth venue(id: ID!): Venue + + eventsAttendedByUser(id: ID, orderBy: EventOrderByInput): [Event] } `; diff --git a/src/typeDefs/types.ts b/src/typeDefs/types.ts index 6018d229e3..5e4ab332dd 100644 --- a/src/typeDefs/types.ts +++ b/src/typeDefs/types.ts @@ -630,6 +630,7 @@ export const types = gql` phone: UserPhone membershipRequests: [MembershipRequest] registeredEvents: [Event] + eventsAttended: [Event] pluginCreationAllowed: Boolean! tagsAssignedWith( after: String diff --git a/src/types/generatedGraphQLTypes.ts b/src/types/generatedGraphQLTypes.ts index f7995820cb..31761aa2fd 100644 --- a/src/types/generatedGraphQLTypes.ts +++ b/src/types/generatedGraphQLTypes.ts @@ -2285,6 +2285,8 @@ export type Query = { customDataByOrganization: Array; customFieldsByOrganization?: Maybe>>; event?: Maybe; + eventVolunteersByEvent?: Maybe>>; + eventsAttendedByUser?: Maybe>>; eventsByOrganization?: Maybe>>; eventsByOrganizationConnection: Array; fundsByOrganization?: Maybe>>; @@ -2307,6 +2309,7 @@ export type Query = { getNoteById: Note; getPledgesByUserId?: Maybe>>; getPlugins?: Maybe>>; + getRecurringEvents?: Maybe>>; getUserTag?: Maybe; getVenueByOrgId?: Maybe>>; getVolunteerMembership: Array>; @@ -2416,6 +2419,15 @@ export type QueryEventArgs = { id: Scalars['ID']['input']; }; +export type QueryEventVolunteersByEventArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryEventsAttendedByUserArgs = { + id?: InputMaybe; + orderBy?: InputMaybe; +}; export type QueryEventsByOrganizationArgs = { id?: InputMaybe; @@ -2531,6 +2543,11 @@ export type QueryGetPledgesByUserIdArgs = { }; +export type QueryGetRecurringEventsArgs = { + baseRecurringEventId: Scalars['ID']['input']; +}; + + export type QueryGetUserTagArgs = { id: Scalars['ID']['input']; }; @@ -2941,6 +2958,7 @@ export type User = { email: Scalars['EmailAddress']['output']; employmentStatus?: Maybe; eventAdmin?: Maybe>>; + eventsAttended?: Maybe>>; firstName: Scalars['String']['output']; gender?: Maybe; identifier: Scalars['Int']['output']; @@ -4690,6 +4708,8 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; customFieldsByOrganization?: Resolver>>, ParentType, ContextType, RequireFields>; event?: Resolver, ParentType, ContextType, RequireFields>; + eventVolunteersByEvent?: Resolver>>, ParentType, ContextType, RequireFields>; + eventsAttendedByUser?: Resolver>>, ParentType, ContextType, Partial>; eventsByOrganization?: Resolver>>, ParentType, ContextType, Partial>; eventsByOrganizationConnection?: Resolver, ParentType, ContextType, Partial>; fundsByOrganization?: Resolver>>, ParentType, ContextType, RequireFields>; @@ -4712,6 +4732,7 @@ export type QueryResolvers>; getPledgesByUserId?: Resolver>>, ParentType, ContextType, RequireFields>; getPlugins?: Resolver>>, ParentType, ContextType>; + getRecurringEvents?: Resolver>>, ParentType, ContextType, RequireFields>; getUserTag?: Resolver, ParentType, ContextType, RequireFields>; getVenueByOrgId?: Resolver>>, ParentType, ContextType, RequireFields>; getVolunteerMembership?: Resolver>, ParentType, ContextType, RequireFields>; @@ -4813,6 +4834,7 @@ export type UserResolvers; employmentStatus?: Resolver, ParentType, ContextType>; eventAdmin?: Resolver>>, ParentType, ContextType>; + eventsAttended?: Resolver>>, ParentType, ContextType>; firstName?: Resolver; gender?: Resolver, ParentType, ContextType>; identifier?: Resolver; diff --git a/tests/helpers/userAndOrg.ts b/tests/helpers/userAndOrg.ts index d7dde673dd..015b86a657 100644 --- a/tests/helpers/userAndOrg.ts +++ b/tests/helpers/userAndOrg.ts @@ -9,8 +9,8 @@ import type { import { AppUserProfile, Organization, User } from "../../src/models"; export type TestOrganizationType = - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (InterfaceOrganization & Document) | null; + | (InterfaceOrganization & Document) + | null; export type TestUserType = | (InterfaceUser & Document) diff --git a/tests/resolvers/Mutation/addEventAttendee.spec.ts b/tests/resolvers/Mutation/addEventAttendee.spec.ts index a83a1d0d81..6e371ae0f1 100644 --- a/tests/resolvers/Mutation/addEventAttendee.spec.ts +++ b/tests/resolvers/Mutation/addEventAttendee.spec.ts @@ -10,7 +10,12 @@ import { USER_NOT_FOUND_ERROR, USER_NOT_MEMBER_FOR_ORGANIZATION, } from "../../../src/constants"; -import { AppUserProfile, EventAttendee, User } from "../../../src/models"; +import { + AppUserProfile, + Event, + EventAttendee, + User, +} from "../../../src/models"; import type { MutationAddEventAttendeeArgs } from "../../../src/types/generatedGraphQLTypes"; import { connect, disconnect } from "../../helpers/db"; import { createTestEvent, type TestEventType } from "../../helpers/events"; @@ -153,39 +158,41 @@ describe("resolvers -> Mutation -> addEventAttendee", () => { }); it(`registers the request user for the event successfully and returns the request user`, async () => { - const eventOrganizationId = testEvent?.organization._id; - const userId = randomTestUser?._id; - - await User.updateOne( - { _id: userId }, - { $addToSet: { joinedOrganizations: eventOrganizationId } }, - ); - - const args: MutationAddEventAttendeeArgs = { - data: { - userId: userId, - eventId: testEvent?._id ?? "", - }, - }; + if (testEvent?.organization) { + const eventOrganizationId = testEvent?.organization._id; + const userId = randomTestUser?._id; - const context = { userId: testUser?._id }; + await User.updateOne( + { _id: userId }, + { $addToSet: { joinedOrganizations: eventOrganizationId } }, + ); - const { addEventAttendee: addEventAttendeeResolver } = await import( - "../../../src/resolvers/Mutation/addEventAttendee" - ); - const payload = await addEventAttendeeResolver?.({}, args, context); + const args: MutationAddEventAttendeeArgs = { + data: { + userId: userId, + eventId: testEvent?._id.toString() ?? "", + }, + }; - const requestUser = await User.findOne({ - _id: userId?._id, - }).lean(); + const context = { userId: testUser?._id }; - const isUserRegistered = await EventAttendee.exists({ - ...args.data, - }); - expect(payload).toEqual(requestUser); - expect(isUserRegistered).toBeTruthy(); + const { addEventAttendee: addEventAttendeeResolver } = await import( + "../../../src/resolvers/Mutation/addEventAttendee" + ); + const payload = await addEventAttendeeResolver?.({}, args, context); + + expect(payload).toBeDefined(); + + const requestUser = await User.findOne({ + _id: userId?._id, + }).lean(); + expect(payload).toEqual(expect.objectContaining(requestUser)); + const isUserRegistered = await EventAttendee.exists({ + ...args.data, + }); + expect(isUserRegistered).toBeTruthy(); + } }); - it(`throws UnauthorizedError if the requestUser is not a member of the organization`, async () => { const { requestContext } = await import("../../../src/libraries"); @@ -199,11 +206,10 @@ describe("resolvers -> Mutation -> addEventAttendee", () => { ); try { - // Create a test user who is not a member of the organization const args: MutationAddEventAttendeeArgs = { data: { userId: testUser?._id, - eventId: testEvent!._id, + eventId: testEvent!._id.toString(), }, }; @@ -291,4 +297,93 @@ describe("resolvers -> Mutation -> addEventAttendee", () => { ); } }); + it("throws NotFoundError if the user is not found after update", async () => { + const { requestContext } = await import("../../../src/libraries"); + + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementationOnce((message) => `Translated ${message}`); + + const mockFindByIdAndUpdate = vi + .spyOn(User, "findByIdAndUpdate") + .mockResolvedValueOnce(null); + + const args: MutationAddEventAttendeeArgs = { + data: { + userId: testUser?._id, + eventId: testEvent?._id.toString() ?? "", + }, + }; + + const context = { userId: testUser?._id }; + + try { + const { addEventAttendee: addEventAttendeeResolver } = await import( + "../../../src/resolvers/Mutation/addEventAttendee" + ); + + await addEventAttendeeResolver?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual( + `Translated ${USER_NOT_AUTHORIZED_ERROR.MESSAGE}`, + ); + expect(spy).toHaveBeenLastCalledWith(USER_NOT_AUTHORIZED_ERROR.MESSAGE); + } + mockFindByIdAndUpdate.mockRestore(); + }); + it("throws UnauthorizedError when user update fails", async () => { + const { requestContext } = await import("../../../src/libraries"); + + await AppUserProfile.create({ + userId: testUser?._id, + adminFor: [testEvent?.organization._id], + }); + + await Event.findByIdAndUpdate( + testEvent?._id, + { $pull: { admins: testUser?._id } }, + { new: true }, + ); + + await User.findByIdAndUpdate(testUser?._id, { + $push: { joinedOrganizations: testEvent?.organization._id }, + }); + + await EventAttendee.deleteOne({ + userId: testUser?._id, + eventId: testEvent?._id, + }); + + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementationOnce((message) => `Translated ${message}`); + + const mockFindByIdAndUpdate = vi + .spyOn(User, "findByIdAndUpdate") + .mockResolvedValueOnce(null); + + const args: MutationAddEventAttendeeArgs = { + data: { + userId: testUser?._id, + eventId: testEvent?._id.toString(), + }, + }; + + const context = { userId: testUser?._id }; + + try { + const { addEventAttendee: addEventAttendeeResolver } = await import( + "../../../src/resolvers/Mutation/addEventAttendee" + ); + + await addEventAttendeeResolver?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual( + `Translated ${USER_NOT_AUTHORIZED_ERROR.MESSAGE}`, + ); + expect(spy).toHaveBeenCalledWith(USER_NOT_AUTHORIZED_ERROR.MESSAGE); + } + + mockFindByIdAndUpdate.mockRestore(); + }); }); diff --git a/tests/resolvers/Query/eventsAttendedByUser.spec.ts b/tests/resolvers/Query/eventsAttendedByUser.spec.ts new file mode 100644 index 0000000000..7ee2807417 --- /dev/null +++ b/tests/resolvers/Query/eventsAttendedByUser.spec.ts @@ -0,0 +1,62 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import { Event } from "../../../src/models"; +import { connect, disconnect } from "../../helpers/db"; +import type { QueryEventsAttendedByUserArgs } from "../../../src/types/generatedGraphQLTypes"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { + TestUserType, + TestOrganizationType, +} from "../../helpers/userAndOrg"; +import { createTestUserAndOrganization } from "../../helpers/userAndOrg"; +import { createEventWithRegistrant } from "../../helpers/events"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testUser: TestUserType; +let testOrganization: TestOrganizationType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [testUser, testOrganization] = await createTestUserAndOrganization(); + + await createEventWithRegistrant(testUser?._id, testOrganization?._id, true); + await createEventWithRegistrant(testUser?._id, testOrganization?._id, true); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Query -> eventsAttendedByUser", () => { + it(`returns list of all events attended by user sorted by ascending order of event._id if args.orderBy === 'id_ASC'`, async () => { + const args: QueryEventsAttendedByUserArgs = { + id: testUser?._id, + orderBy: "id_ASC", + }; + + const { eventsAttendedByUser } = await import( + "../../../src/resolvers/Query/eventsAttendedByUser" + ); + + const eventsAttendedByUserPayload = await eventsAttendedByUser?.( + {}, + args, + {}, + ); + + const eventsAttendedByUserInfo = await Event.find({ + registrants: { + $elemMatch: { + userId: testUser?._id, + status: "ACTIVE", + }, + }, + }) + .sort({ _id: 1 }) + .populate("creatorId", "-password") + .populate("admins", "-password") + .lean(); + + expect(eventsAttendedByUserPayload).toEqual(eventsAttendedByUserInfo); + }); +}); diff --git a/tests/resolvers/Query/getRecurringEvents.spec.ts b/tests/resolvers/Query/getRecurringEvents.spec.ts new file mode 100644 index 0000000000..8b995ca4b3 --- /dev/null +++ b/tests/resolvers/Query/getRecurringEvents.spec.ts @@ -0,0 +1,118 @@ +import "dotenv/config"; +import { getRecurringEvents } from "../../../src/resolvers/Query/getRecurringEvents"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { Event } from "../../../src/models"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import { Types } from "mongoose"; +import { + createTestUser, + createTestUserAndOrganization, + type TestOrganizationType, + type TestUserType, +} from "../../helpers/userAndOrg"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testUser: TestUserType; +let testAdminUser: TestUserType; +let testOrganization: TestOrganizationType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const result = await createTestUserAndOrganization(); + testUser = result[0]; + testOrganization = result[1]; + testAdminUser = await createTestUser(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Query -> getRecurringEvents", () => { + it("returns list of recurring events for a given baseRecurringEventId", async () => { + if (!testUser || !testAdminUser || !testOrganization) { + throw new Error("Test setup failed"); + } + const baseRecurringEventId = new Types.ObjectId(); + + const testEvents = [ + { + title: "Event 1", + description: "description", + allDay: true, + startDate: new Date().toUTCString(), + recurring: true, + isPublic: true, + isRegisterable: true, + creatorId: testUser._id, + admins: [testAdminUser._id], + registrants: [], + organization: testOrganization._id, + baseRecurringEventId, + }, + { + title: "Event 2", + description: "description", + allDay: true, + startDate: new Date(), + recurring: true, + isPublic: true, + isRegisterable: true, + creatorId: testUser._id, + admins: [testAdminUser._id], + registrants: [], + organization: testOrganization._id, + baseRecurringEventId, + }, + ]; + + await Event.insertMany(testEvents); + + const args = { baseRecurringEventId: baseRecurringEventId.toString() }; + const getRecurringEventsFunction = getRecurringEvents as unknown as ( + parent: any, + args: any, + context: any, + ) => Promise; + const result = await getRecurringEventsFunction({}, args, {}); + + expect(result).toHaveLength(2); + expect(result[0].title).toBe("Event 1"); + expect(result[1].title).toBe("Event 2"); + + await Event.deleteMany({ baseRecurringEventId }); + }); + + it("returns an empty array when no recurring events are found", async () => { + const nonExistentId = new Types.ObjectId(); + const args = { baseRecurringEventId: nonExistentId.toString() }; + const getRecurringEventsFunction = getRecurringEvents as unknown as ( + parent: any, + args: any, + context: any, + ) => Promise; + const result = await getRecurringEventsFunction({}, args, {}); + + expect(result).toEqual([]); + }); + + it("throws an error when there's a problem fetching events", async () => { + const mockFind = vi.spyOn(Event, "find").mockImplementation(() => { + throw new Error("Database error"); + }); + + const args = { baseRecurringEventId: new Types.ObjectId().toString() }; + const getRecurringEventsFunction = getRecurringEvents as unknown as ( + parent: any, + args: any, + context: any, + ) => Promise; + + await expect(getRecurringEventsFunction({}, args, {})).rejects.toThrow( + "Database error", + ); + + mockFind.mockRestore(); + }); +}); diff --git a/tests/resolvers/Query/organizationsMemberConnection.spec.ts b/tests/resolvers/Query/organizationsMemberConnection.spec.ts index 0a773fcda5..27147bb126 100644 --- a/tests/resolvers/Query/organizationsMemberConnection.spec.ts +++ b/tests/resolvers/Query/organizationsMemberConnection.spec.ts @@ -248,6 +248,7 @@ describe("resolvers -> Query -> organizationsMemberConnection", () => { registeredEvents: user.registeredEvents, status: user.status, updatedAt: user.updatedAt, + eventsAttended: user.eventsAttended, }; }); @@ -341,6 +342,7 @@ describe("resolvers -> Query -> organizationsMemberConnection", () => { registeredEvents: user.registeredEvents, status: user.status, updatedAt: user.updatedAt, + eventsAttended: user.eventsAttended, }; }); // console.log(organizationsMemberConnectionPayload, usersWithPassword); @@ -433,6 +435,7 @@ describe("resolvers -> Query -> organizationsMemberConnection", () => { registeredEvents: user.registeredEvents, status: user.status, updatedAt: user.updatedAt, + eventsAttended: user.eventsAttended, }; }); @@ -528,6 +531,7 @@ describe("resolvers -> Query -> organizationsMemberConnection", () => { registeredEvents: user.registeredEvents, status: user.status, updatedAt: user.updatedAt, + eventsAttended: user.eventsAttended, }; }); @@ -623,6 +627,7 @@ describe("resolvers -> Query -> organizationsMemberConnection", () => { registeredEvents: user.registeredEvents, status: user.status, updatedAt: user.updatedAt, + eventsAttended: user.eventsAttended, }; }); @@ -706,6 +711,7 @@ describe("resolvers -> Query -> organizationsMemberConnection", () => { registeredEvents: user.registeredEvents, status: user.status, updatedAt: user.updatedAt, + eventsAttended: user.eventsAttended, }; }); @@ -888,6 +894,7 @@ describe("resolvers -> Query -> organizationsMemberConnection", () => { registeredEvents: user.registeredEvents, status: user.status, updatedAt: user.updatedAt, + eventsAttended: user.eventsAttended, }; }); @@ -960,6 +967,7 @@ describe("resolvers -> Query -> organizationsMemberConnection", () => { registeredEvents: user.registeredEvents, status: user.status, updatedAt: user.updatedAt, + eventsAttended: user.eventsAttended, }; }); @@ -1061,6 +1069,7 @@ describe("resolvers -> Query -> organizationsMemberConnection", () => { registeredEvents: user.registeredEvents, status: user.status, updatedAt: user.updatedAt, + eventsAttended: user.eventsAttended, }; }); @@ -1143,6 +1152,7 @@ describe("resolvers -> Query -> organizationsMemberConnection", () => { registeredEvents: user.registeredEvents, status: user.status, updatedAt: user.updatedAt, + eventsAttended: user.eventsAttended, }; }); diff --git a/tests/resolvers/UserTag/childTags.spec.ts b/tests/resolvers/UserTag/childTags.spec.ts index 463dbb3e51..bb3aab877d 100644 --- a/tests/resolvers/UserTag/childTags.spec.ts +++ b/tests/resolvers/UserTag/childTags.spec.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ import "dotenv/config"; import { GraphQLError } from "graphql"; import type mongoose from "mongoose"; diff --git a/tests/resolvers/middleware/currentUserExists.spec.ts b/tests/resolvers/middleware/currentUserExists.spec.ts index 3cc0a4fced..0dd3707e89 100644 --- a/tests/resolvers/middleware/currentUserExists.spec.ts +++ b/tests/resolvers/middleware/currentUserExists.spec.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import "dotenv/config"; import type mongoose from "mongoose"; import { Types } from "mongoose"; From 7631f1723632438205c2f23956c210640428abb8 Mon Sep 17 00:00:00 2001 From: Aman Singh Date: Mon, 4 Nov 2024 08:41:35 +0530 Subject: [PATCH 6/6] Enhance File Storage and API Capabilities with Minio Integration and Post Management APIs (GSoC'24). (#2637) * feat: enhanced file storage and API functionalities - Integrated Minio service for file upload and deletion in Minio storage - Added REST APIs for Post creation, updating, and deletion - Updated mutations and queries to correctly handle file (image/video) retrieval - Extended models for File and Post with new attributes - Implemented file hashing to prevent file duplication - Added authentication middleware for secure REST API access * test: added unit tests for createPost controller * test: added unit tests for updatePost controller * test: Improved test for updatePost. * test: added unit tests for getFile * test: added unit tests for createFile * test: added unit tests for deleteFile service * test: added unit tests for uploadFile service * test: added unit tests for fileUpload middleware * test: updated unit tests for isAuth middleware * test: updated unit tests for removePost mutation * test: updated unit tests for updateUserProfile mutation * test: updated unit tests for checkAuth query * test: updated unit tests for organizationsMemberConnection query * test: updated unit tests for user query * test: updated unit tests for deletePreviousFile utility * test: added minio utility to check if the server is running * test: added test for isValidMimeType and removed multerErrorHandler * test: Improved the tests for organization posts and image * tsDoc: Added documentation for createPost and updatePost * fix: fixed test for remove post --- locales/en.json | 7 +- locales/fr.json | 7 +- locales/hi.json | 7 +- locales/sp.json | 7 +- locales/zh.json | 7 +- package-lock.json | 8999 ++++++++++------- package.json | 5 + schema.graphql | 39 +- src/REST/controllers/mutation/createPost.ts | 262 + src/REST/controllers/mutation/index.ts | 2 + src/REST/controllers/mutation/updatePost.ts | 246 + src/REST/controllers/query/getFile.ts | 40 + src/REST/controllers/query/index.ts | 1 + src/REST/routes/index.ts | 31 + src/REST/services/file/createFile.ts | 55 + src/REST/services/file/deleteFile.ts | 35 + src/REST/services/file/index.ts | 3 + src/REST/services/file/uploadFile.ts | 79 + src/REST/services/minio/index.ts | 130 + src/REST/types/index.ts | 9 + src/app.ts | 41 +- src/config/minio/index.ts | 47 + src/config/multer/index.ts | 69 + src/constants.ts | 41 + src/middleware/fileUpload.ts | 74 + src/middleware/index.ts | 1 + src/middleware/isAuth.ts | 59 +- src/models/File.ts | 146 +- src/models/Post.ts | 14 +- src/resolvers/Mutation/index.ts | 2 - src/resolvers/Mutation/removePost.ts | 21 +- src/resolvers/Mutation/updatePost.ts | 196 - src/resolvers/Mutation/updateUserProfile.ts | 7 +- src/resolvers/Organization/image.ts | 8 +- src/resolvers/Organization/posts.ts | 28 +- src/resolvers/Query/checkAuth.ts | 8 +- .../Query/organizationsMemberConnection.ts | 6 +- src/resolvers/Query/post.ts | 10 +- src/resolvers/Query/user.ts | 1 - src/typeDefs/enums.ts | 5 + src/typeDefs/types.ts | 33 +- src/types/generatedGraphQLTypes.ts | 93 +- .../encodedImageStorage/deletePreviousFile.ts | 38 + src/utilities/isValidMimeType.ts | 17 + tests/helpers/minio.ts | 49 + tests/helpers/posts.ts | 22 +- tests/middleware/fileUpload.spec.ts | 215 + tests/middleware/isAuth.spec.ts | 80 +- tests/resolvers/Mutation/createPost.spec.ts | 789 +- tests/resolvers/Mutation/removePost.spec.ts | 113 +- tests/resolvers/Mutation/updatePost.spec.ts | 666 +- .../Mutation/updateUserProfile.spec.ts | 94 +- tests/resolvers/Organization/image.spec.ts | 2 +- tests/resolvers/Organization/posts.spec.ts | 85 +- tests/resolvers/Query/checkAuth.spec.ts | 30 +- tests/resolvers/Query/getFile.spec.ts | 138 + .../organizationsMemberConnection.spec.ts | 4 +- tests/resolvers/Query/post.spec.ts | 14 +- tests/resolvers/Query/user.spec.ts | 35 +- tests/services/createFile.spec.ts | 110 + tests/services/deleteFile.spec.ts | 123 + tests/services/uploadFile.spec.ts | 162 + .../createSampleOrganizationUtil.spec.ts | 1 - tests/utilities/deletePreviousFile.spec.ts | 94 + tests/utilities/isValidMimeType.spec.ts | 23 + 65 files changed, 8799 insertions(+), 4986 deletions(-) create mode 100644 src/REST/controllers/mutation/createPost.ts create mode 100644 src/REST/controllers/mutation/index.ts create mode 100644 src/REST/controllers/mutation/updatePost.ts create mode 100644 src/REST/controllers/query/getFile.ts create mode 100644 src/REST/controllers/query/index.ts create mode 100644 src/REST/routes/index.ts create mode 100644 src/REST/services/file/createFile.ts create mode 100644 src/REST/services/file/deleteFile.ts create mode 100644 src/REST/services/file/index.ts create mode 100644 src/REST/services/file/uploadFile.ts create mode 100644 src/REST/services/minio/index.ts create mode 100644 src/REST/types/index.ts create mode 100644 src/config/minio/index.ts create mode 100644 src/config/multer/index.ts create mode 100644 src/middleware/fileUpload.ts delete mode 100644 src/resolvers/Mutation/updatePost.ts create mode 100644 src/utilities/encodedImageStorage/deletePreviousFile.ts create mode 100644 src/utilities/isValidMimeType.ts create mode 100644 tests/helpers/minio.ts create mode 100644 tests/middleware/fileUpload.spec.ts create mode 100644 tests/resolvers/Query/getFile.spec.ts create mode 100644 tests/services/createFile.spec.ts create mode 100644 tests/services/deleteFile.spec.ts create mode 100644 tests/services/uploadFile.spec.ts create mode 100644 tests/utilities/deletePreviousFile.spec.ts create mode 100644 tests/utilities/isValidMimeType.spec.ts diff --git a/locales/en.json b/locales/en.json index 58ca2bd8ff..15dcd74f54 100644 --- a/locales/en.json +++ b/locales/en.json @@ -36,5 +36,10 @@ "translation.alreadyPresent": "Translation Already Present", "translation.notFound": "Translation not found", "parameter.missing": "Missing Skip parameter. Set it to either 0 or some other value not found", - "tag.alreadyExists": "A tag with the same name already exists at this level" + "tag.alreadyExists": "A tag with the same name already exists at this level", + "invalid.contentType": "Invalid content type. Expected multipart/form-data", + "invalid.fieldFileName": "Invalid file input field name received", + "file.sizeExceeded": "File size exceeds the allowable limit", + "file.notFound": "File not found.", + "invalid.argument": "Invalid argument received" } diff --git a/locales/fr.json b/locales/fr.json index f54df13c4c..07eb2be24f 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -35,5 +35,10 @@ "translation.alreadyPresent": "Traduction déjà présente", "translation.notFound": "Traduction introuvable", "parameter.missing": "Paramètre de saut manquant. Réglez-le sur 0 ou sur une autre valeur", - "tag.alreadyExists": "Un tag avec le même nom existe déjà à ce niveau" + "tag.alreadyExists": "Un tag avec le même nom existe déjà à ce niveau", + "invalid.contentType": "Type de contenu invalide. multipart/form-data attendu.", + "invalid.fieldFileName": "Nom de champ de fichier non valide reçu.", + "file.sizeExceeded": "La taille du fichier dépasse la limite autorisée.", + "file.notFound": "Fichier non trouvé.", + "invalid.argument": "Argument non valide reçu." } diff --git a/locales/hi.json b/locales/hi.json index 52b87cbae8..366c04895f 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -36,5 +36,10 @@ "translation.alreadyPresent": "अनुवाद पहले से मौजूद है", "translation.notFound": "अनुवाद नहीं मिला", "parameter.missing": "छोड़ें पैरामीटर मौजूद नहीं है. इसे 0 या किसी अन्य मान पर सेट करें", - "tag.alreadyExists": "इस स्तर पर समान नाम वाला टैग पहले से मौजूद है" + "tag.alreadyExists": "इस स्तर पर समान नाम वाला टैग पहले से मौजूद है", + "invalid.contentType": "अमान्य सामग्री प्रकार। अपेक्षित है multipart/form-data।", + "invalid.fieldFileName": "अमान्य फ़ाइल इनपुट फ़ील्ड नाम प्राप्त हुआ।", + "file.sizeExceeded": "फ़ाइल का आकार अनुमत सीमा से अधिक है।", + "file.notFound": "फ़ाइल नहीं मिली।", + "invalid.argument": "अमान्य तर्क प्राप्त हुआ।" } diff --git a/locales/sp.json b/locales/sp.json index f291588472..59a5fdc801 100644 --- a/locales/sp.json +++ b/locales/sp.json @@ -35,5 +35,10 @@ "translation.alreadyPresent": "Traducción ya presente", "translation.notFound": "Traducción no encontrada", "parameter.missing": "Falta el parámetro Omitir. Establézcalo en 0 o en algún otro valor", - "tag.alreadyExists": "Ya existe una etiqueta con el mismo nombre en este nivel" + "tag.alreadyExists": "Ya existe una etiqueta con el mismo nombre en este nivel", + "invalid.contentType": "Tipo de contenido no válido. Se esperaba multipart/form-data.", + "invalid.fieldFileName": "Se recibió un nombre de campo de archivo no válido.", + "file.sizeExceeded": "El tamaño del archivo supera el límite permitido.", + "file.notFound": "Archivo no encontrado.", + "invalid.argument": "Se recibió un argumento no válido." } diff --git a/locales/zh.json b/locales/zh.json index 3f065d31bf..7593b1eb2a 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -35,5 +35,10 @@ "translation.alreadyPresent": "翻译已经存在", "translation.notFound": "找不到翻译", "parameter.missing": "缺少跳过参数。将其设置为 0 或其他值", - "tag.alreadyExists": "此级别已存在相同名称的标签" + "tag.alreadyExists": "此级别已存在相同名称的标签", + "invalid.contentType": "内容类型无效。预期为 multipart/form-data。", + "invalid.fieldFileName": "收到无效的文件输入字段名称。", + "file.sizeExceeded": "文件大小超过允许的限制。", + "file.notFound": "文件未找到。", + "invalid.argument": "收到无效的参数。" } diff --git a/package-lock.json b/package-lock.json index 15ed045896..bd9f53aa5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "GNU General Public License v3.0", "dependencies": { "@apollo/server": "^4.11.0", + "@aws-sdk/client-s3": "^3.675.0", "@faker-js/faker": "^9.0.1", "@graphql-inspector/cli": "^5.0.6", "@graphql-tools/resolvers-composition": "^7.0.2", @@ -52,6 +53,7 @@ "mongoose": "^8.3.2", "mongoose-paginate-v2": "^1.8.3", "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", "nanoid": "^5.0.7", "nodemailer": "^6.9.15", "pm2": "^5.4.0", @@ -88,8 +90,10 @@ "@types/lodash": "^4.17.12", "@types/mongoose-paginate-v2": "^1.6.5", "@types/morgan": "^1.9.9", + "@types/multer": "^1.4.12", "@types/node": "^22.7.8", "@types/nodemailer": "^6.4.15", + "@types/supertest": "^6.0.2", "@types/uuid": "^10.0.0", "@types/validator": "^13.12.2", "@vitest/coverage-v8": "^2.1.3", @@ -106,6 +110,7 @@ "lint-staged": "^15.2.10", "prettier": "^3.3.3", "rimraf": "^6.0.1", + "supertest": "^7.0.0", "tsx": "^4.19.1", "typescript": "^5.5.4", "vitest": "^2.1.3" @@ -551,3160 +556,3291 @@ "node": ">=14" } }, - "node_modules/@babel/code-frame": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", - "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", "dependencies": { - "@babel/highlight": "^7.24.6", - "picocolors": "^1.0.0" + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.6.tgz", - "integrity": "sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ==", - "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/core": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.6.tgz", - "integrity": "sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ==", + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.6", - "@babel/generator": "^7.24.6", - "@babel/helper-compilation-targets": "^7.24.6", - "@babel/helper-module-transforms": "^7.24.6", - "@babel/helpers": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/template": "^7.24.6", - "@babel/traverse": "^7.24.6", - "@babel/types": "^7.24.6", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" } }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } }, - "node_modules/@babel/generator": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz", - "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==", + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "dependencies": { - "@babel/types": "^7.24.6", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dev": true, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "dependencies": { - "@babel/types": "^7.22.5" + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz", - "integrity": "sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg==", + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "dependencies": { - "@babel/compat-data": "^7.24.6", - "@babel/helper-validator-option": "^7.24.6", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.1.tgz", - "integrity": "sha512-1yJa9dX9g//V6fDebXoEfEsxkZHk3Hcbm+zLhyu6qVgYFLvmTALTeV+jNU9e5RnYtioBrGEOdoI2joMSNQ/+aA==", - "dev": true, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "semver": "^6.3.1" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", - "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==", + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz", - "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==", + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "dependencies": { - "@babel/template": "^7.24.6", - "@babel/types": "^7.24.6" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz", - "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==", + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", "dependencies": { - "@babel/types": "^7.24.6" + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", - "dev": true, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", "dependencies": { - "@babel/types": "^7.23.0" + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz", - "integrity": "sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g==", + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "dependencies": { - "@babel/types": "^7.24.6" + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz", - "integrity": "sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA==", + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "dependencies": { - "@babel/helper-environment-visitor": "^7.24.6", - "@babel/helper-module-imports": "^7.24.6", - "@babel/helper-simple-access": "^7.24.6", - "@babel/helper-split-export-declaration": "^7.24.6", - "@babel/helper-validator-identifier": "^7.24.6" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" + "node_modules/@aws-sdk/client-s3": { + "version": "3.675.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.675.0.tgz", + "integrity": "sha512-WKPc9fwFsD0SrWmrj0MdMHE+hQ0YAIGLqACmTnL1yW76qAwjIlFa9TAhR8f29aVCQodt/I6HDf9dHX/F+GyDFg==", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.675.0", + "@aws-sdk/client-sts": "3.675.0", + "@aws-sdk/core": "3.667.0", + "@aws-sdk/credential-provider-node": "3.675.0", + "@aws-sdk/middleware-bucket-endpoint": "3.667.0", + "@aws-sdk/middleware-expect-continue": "3.667.0", + "@aws-sdk/middleware-flexible-checksums": "3.669.0", + "@aws-sdk/middleware-host-header": "3.667.0", + "@aws-sdk/middleware-location-constraint": "3.667.0", + "@aws-sdk/middleware-logger": "3.667.0", + "@aws-sdk/middleware-recursion-detection": "3.667.0", + "@aws-sdk/middleware-sdk-s3": "3.674.0", + "@aws-sdk/middleware-ssec": "3.667.0", + "@aws-sdk/middleware-user-agent": "3.669.0", + "@aws-sdk/region-config-resolver": "3.667.0", + "@aws-sdk/signature-v4-multi-region": "3.674.0", + "@aws-sdk/types": "3.667.0", + "@aws-sdk/util-endpoints": "3.667.0", + "@aws-sdk/util-user-agent-browser": "3.675.0", + "@aws-sdk/util-user-agent-node": "3.669.0", + "@aws-sdk/xml-builder": "3.662.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/eventstream-serde-browser": "^3.0.10", + "@smithy/eventstream-serde-config-resolver": "^3.0.7", + "@smithy/eventstream-serde-node": "^3.0.9", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-blob-browser": "^3.1.6", + "@smithy/hash-node": "^3.0.7", + "@smithy/hash-stream-node": "^3.1.6", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/md5-js": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-stream": "^3.1.9", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.6", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", - "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "node_modules/@aws-sdk/client-sso": { + "version": "3.675.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.675.0.tgz", + "integrity": "sha512-2goBCEr4acZJ1YJ69eWPTsIfZUbO7enog+lBA5kZShDiwovqzwYSHSlf6OGz4ETs2xT1n7n+QfKY0p+TluTfEw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.667.0", + "@aws-sdk/middleware-host-header": "3.667.0", + "@aws-sdk/middleware-logger": "3.667.0", + "@aws-sdk/middleware-recursion-detection": "3.667.0", + "@aws-sdk/middleware-user-agent": "3.669.0", + "@aws-sdk/region-config-resolver": "3.667.0", + "@aws-sdk/types": "3.667.0", + "@aws-sdk/util-endpoints": "3.667.0", + "@aws-sdk/util-user-agent-browser": "3.675.0", + "@aws-sdk/util-user-agent-node": "3.669.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", - "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5" + "node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.675.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.675.0.tgz", + "integrity": "sha512-4kEcaa2P/BFz+xy5tagbtzM08gbjHXyYqW+n6SJuUFK7N6bZNnA4cu1hVgHcqOqk8Dbwv7fiseGT0x3Hhqjwqg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.667.0", + "@aws-sdk/credential-provider-node": "3.675.0", + "@aws-sdk/middleware-host-header": "3.667.0", + "@aws-sdk/middleware-logger": "3.667.0", + "@aws-sdk/middleware-recursion-detection": "3.667.0", + "@aws-sdk/middleware-user-agent": "3.669.0", + "@aws-sdk/region-config-resolver": "3.667.0", + "@aws-sdk/types": "3.667.0", + "@aws-sdk/util-endpoints": "3.667.0", + "@aws-sdk/util-user-agent-browser": "3.675.0", + "@aws-sdk/util-user-agent-node": "3.669.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz", - "integrity": "sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g==", - "dependencies": { - "@babel/types": "^7.24.6" + "@aws-sdk/client-sts": "^3.675.0" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.675.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.675.0.tgz", + "integrity": "sha512-zgjyR4GyuONeDGJBKNt9lFJ8HfDX7rpxZZVR7LSXr9lUkjf6vUGgD2k/K4UAoOTWCKKCor6TA562ezGlA8su6Q==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.675.0", + "@aws-sdk/core": "3.667.0", + "@aws-sdk/credential-provider-node": "3.675.0", + "@aws-sdk/middleware-host-header": "3.667.0", + "@aws-sdk/middleware-logger": "3.667.0", + "@aws-sdk/middleware-recursion-detection": "3.667.0", + "@aws-sdk/middleware-user-agent": "3.669.0", + "@aws-sdk/region-config-resolver": "3.667.0", + "@aws-sdk/types": "3.667.0", + "@aws-sdk/util-endpoints": "3.667.0", + "@aws-sdk/util-user-agent-browser": "3.675.0", + "@aws-sdk/util-user-agent-node": "3.669.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/core": "^2.4.8", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/hash-node": "^3.0.7", + "@smithy/invalid-dependency": "^3.0.7", + "@smithy/middleware-content-length": "^3.0.9", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.23", + "@smithy/util-defaults-mode-node": "^3.0.23", + "@smithy/util-endpoints": "^2.1.3", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" + "node_modules/@aws-sdk/core": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.667.0.tgz", + "integrity": "sha512-pMcDVI7Tmdsc8R3sDv0Omj/4iRParGY+uJtAfF669WnZfDfaBQaix2Mq7+Mu08vdjqO9K3gicFvjk9S1VLmOKA==", + "dependencies": { + "@aws-sdk/types": "3.667.0", + "@smithy/core": "^2.4.8", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/signature-v4": "^4.2.0", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-middleware": "^3.0.7", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", - "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.667.0.tgz", + "integrity": "sha512-zZbrkkaPc54WXm+QAnpuv0LPNfsts0HPPd+oCECGs7IQRaFsGj187cwvPg9RMWDFZqpm64MdBDoA8OQHsqzYCw==", "dependencies": { - "@babel/types": "^7.24.6" + "@aws-sdk/core": "3.667.0", + "@aws-sdk/types": "3.667.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz", - "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==", + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.667.0.tgz", + "integrity": "sha512-sjtybFfERZWiqTY7fswBxKQLvUkiCucOWyqh3IaPo/4nE1PXRnaZCVG0+kRBPrYIxWqiVwytvZzMJy8sVZcG0A==", + "dependencies": { + "@aws-sdk/core": "3.667.0", + "@aws-sdk/types": "3.667.0", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-stream": "^3.1.9", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", - "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.675.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.675.0.tgz", + "integrity": "sha512-kCBlC6grpbpCvgowk9T4JHZxJ88VfN0r77bDZClcadFRAKQ8UHyO02zhgFCfUdnU1lNv1mr3ngEcGN7XzJlYWA==", + "dependencies": { + "@aws-sdk/core": "3.667.0", + "@aws-sdk/credential-provider-env": "3.667.0", + "@aws-sdk/credential-provider-http": "3.667.0", + "@aws-sdk/credential-provider-process": "3.667.0", + "@aws-sdk/credential-provider-sso": "3.675.0", + "@aws-sdk/credential-provider-web-identity": "3.667.0", + "@aws-sdk/types": "3.667.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz", - "integrity": "sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ==", + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.675.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.675.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.675.0.tgz", + "integrity": "sha512-VO1WVZCDmAYu4sY/6qIBzdm5vJTxLhWKJWvL5kVFfSe8WiNNoHlTqYYUK9vAm/JYpIgFLTefPbIc5W4MK7o6Pg==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.667.0", + "@aws-sdk/credential-provider-http": "3.667.0", + "@aws-sdk/credential-provider-ini": "3.675.0", + "@aws-sdk/credential-provider-process": "3.667.0", + "@aws-sdk/credential-provider-sso": "3.675.0", + "@aws-sdk/credential-provider-web-identity": "3.667.0", + "@aws-sdk/types": "3.667.0", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helpers": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.6.tgz", - "integrity": "sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==", + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.667.0.tgz", + "integrity": "sha512-HZHnvop32fKgsNHkdhVaul7UzQ25sEc0j9yqA4bjhtbk0ECl42kj3f1pJ+ZU/YD9ut8lMJs/vVqiOdNThVdeBw==", "dependencies": { - "@babel/template": "^7.24.6", - "@babel/types": "^7.24.6" + "@aws-sdk/core": "3.667.0", + "@aws-sdk/types": "3.667.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/highlight": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", - "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.675.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.675.0.tgz", + "integrity": "sha512-p/EE2c0ebSgRhg1Fe1OH2+xNl7j1P4DTc7kZy1mX1NJ72fkqnGgBuf1vk5J9RmiRpbauPNMlm+xohjkGS7iodA==", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.6", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@aws-sdk/client-sso": "3.675.0", + "@aws-sdk/core": "3.667.0", + "@aws-sdk/token-providers": "3.667.0", + "@aws-sdk/types": "3.667.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.667.0.tgz", + "integrity": "sha512-t8CFlZMD/1p/8Cli3rvRiTJpjr/8BO64gw166AHgFZYSN2h95L2l1tcW0jpsc3PprA32nLg1iQVKYt4WGM4ugw==", "dependencies": { - "color-convert": "^1.9.0" + "@aws-sdk/core": "3.667.0", + "@aws-sdk/types": "3.667.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=4" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.667.0" } }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.667.0.tgz", + "integrity": "sha512-XGz4jMAkDoTyFdtLz7ZF+C05IAhCTC1PllpvTBaj821z/L0ilhbqVhrT/f2Buw8Id/K5A390csGXgusXyrFFjA==", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "@aws-sdk/types": "3.667.0", + "@aws-sdk/util-arn-parser": "3.568.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "@smithy/util-config-provider": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=4" + "node": ">=16.0.0" } }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.667.0.tgz", + "integrity": "sha512-0TiSL9S5DSG95NHGIz6qTMuV7GDKVn8tvvGSrSSZu/wXO3JaYSH0AElVpYfc4PtPRqVpEyNA7nnc7W56mMCLWQ==", "dependencies": { - "color-name": "1.1.3" + "@aws-sdk/types": "3.667.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.669.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.669.0.tgz", + "integrity": "sha512-01UQLoUzVwWMf+b+AEuwJ2lluBD+Cp8AcbyEHqvEaPdjGKHIS4BCvnY70mZYnAfRtL8R2h9tt7iI61oWU3Gjkg==", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-sdk/core": "3.667.0", + "@aws-sdk/types": "3.667.0", + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=0.8.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.667.0.tgz", + "integrity": "sha512-Z7fIAMQnPegs7JjAQvlOeWXwpMRfegh5eCoIP6VLJIeR6DLfYKbP35JBtt98R6DXslrN2RsbTogjbxPEDQfw1w==", + "dependencies": { + "@aws-sdk/types": "3.667.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=4" + "node": ">=16.0.0" } }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.667.0.tgz", + "integrity": "sha512-ob85H3HhT3/u5O+x0o557xGZ78vSNeSSwMaSitxdsfs2hOuoUl1uk+OeLpi1hkuJnL41FPpokV7TVII2XrFfmg==", "dependencies": { - "has-flag": "^3.0.0" + "@aws-sdk/types": "3.667.0", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=4" + "node": ">=16.0.0" } }, - "node_modules/@babel/parser": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz", - "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==", - "bin": { - "parser": "bin/babel-parser.js" + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.667.0.tgz", + "integrity": "sha512-PtTRNpNm/5c746jRgZCNg4X9xEJIwggkGJrF0GP9AB1ANg4pc/sF2Fvn1NtqPe9wtQ2stunJprnm5WkCHN7QiA==", + "dependencies": { + "@aws-sdk/types": "3.667.0", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.0.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", - "dev": true, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.667.0.tgz", + "integrity": "sha512-U5glWD3ehFohzpUpopLtmqAlDurGWo2wRGPNgi4SwhWU7UDt6LS7E/UvJjqC0CUrjlzOw+my2A+Ncf+fisMhxQ==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@aws-sdk/types": "3.667.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.674.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.674.0.tgz", + "integrity": "sha512-IvXnWrKy4mO+I44kLYHd6Wlw+FdB4sg1jvHCmnZo1KNaAFIA3x1iXgOaZynKoBdEmol3xfr2uDbeXUQvIwoIgg==", + "dependencies": { + "@aws-sdk/core": "3.667.0", + "@aws-sdk/types": "3.667.0", + "@aws-sdk/util-arn-parser": "3.568.0", + "@smithy/core": "^2.4.8", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/protocol-http": "^4.1.4", + "@smithy/signature-v4": "^4.2.0", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-stream": "^3.1.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", - "dev": true, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.667.0.tgz", + "integrity": "sha512-1wuAUZIkmZIvOmGg5qNQU821CGFHhkuKioxXgNh0DpUxZ9+AeiV7yorJr+bqkb2KBFv1i1TnzGRecvKf/KvZIQ==", "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" + "@aws-sdk/types": "3.667.0", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=16.0.0" } }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.669.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.669.0.tgz", + "integrity": "sha512-K8ScPi45zjJrj5Y2gRqVsvKKQCQbvQBfYGcBw9ZOx9TTavH80bOCBjWg/GFnvs4f37tqVc1wMN2oGvcTF6HveQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" + "@aws-sdk/core": "3.667.0", + "@aws-sdk/types": "3.667.0", + "@aws-sdk/util-endpoints": "3.667.0", + "@smithy/core": "^2.4.8", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@babel/plugin-syntax-flow": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.1.tgz", - "integrity": "sha512-sxi2kLTI5DeW5vDtMUsk4mTPwvlUDbjOnoWayhynCwrw4QXRld4QEYwqzY8JmQXaJUtgUuCIurtSRH5sn4c7mA==", - "dev": true, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.667.0.tgz", + "integrity": "sha512-iNr+JhhA902JMKHG9IwT9YdaEx6KGl6vjAL5BRNeOjfj4cZYMog6Lz/IlfOAltMtT0w88DAHDEFrBd2uO0l2eg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@aws-sdk/types": "3.667.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.7", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=16.0.0" } }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", - "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.674.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.674.0.tgz", + "integrity": "sha512-VMQWbtcbg4FV/fILrODADV21pPg9AghuEzQlW2kH0hCtacvBwFl7eBxIiCBLLtkNple+CVPJvyBcqOZdBkEv/w==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@aws-sdk/middleware-sdk-s3": "3.674.0", + "@aws-sdk/types": "3.667.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/signature-v4": "^4.2.0", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=16.0.0" } }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", - "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", - "dev": true, + "node_modules/@aws-sdk/token-providers": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.667.0.tgz", + "integrity": "sha512-ZecJlG8p6D4UTYlBHwOWX6nknVtw/OBJ3yPXTSajBjhUlj9lE2xvejI8gl4rqkyLXk7z3bki+KR4tATbMaM9yg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@aws-sdk/types": "3.667.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@aws-sdk/client-sso-oidc": "^3.667.0" } }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, + "node_modules/@aws-sdk/types": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.667.0.tgz", + "integrity": "sha512-gYq0xCsqFfQaSL/yT1Gl1vIUjtsg7d7RhnUfsXaHt8xTxOKRTdH9GjbesBjXOzgOvB0W0vfssfreSNGFlOOMJg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", - "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", - "dev": true, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.568.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.568.0.tgz", + "integrity": "sha512-XUKJWWo+KOB7fbnPP0+g/o5Ulku/X53t7i/h+sPHr5xxYTJJ9CYnbToo95mzxe7xWvkLrsNtJ8L+MnNn9INs2w==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=16.0.0" } }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", - "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", - "dev": true, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.667.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.667.0.tgz", + "integrity": "sha512-X22SYDAuQJWnkF1/q17pkX3nGw5XMD9YEUbmt87vUnRq7iyJ3JOpl6UKOBeUBaL838wA5yzdbinmCITJ/VZ1QA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@aws-sdk/types": "3.667.0", + "@smithy/types": "^3.5.0", + "@smithy/util-endpoints": "^2.1.3", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.568.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.568.0.tgz", + "integrity": "sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig==", + "dependencies": { + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.1.tgz", - "integrity": "sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==", - "dev": true, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.675.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.675.0.tgz", + "integrity": "sha512-HW4vGfRiX54RLcsYjLuAhcBBJ6lRVEZd7njfGpAwBB9s7BH8t48vrpYbyA5XbbqbTvXfYBnugQCUw9HWjEa1ww==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@aws-sdk/types": "3.667.0", + "@smithy/types": "^3.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.669.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.669.0.tgz", + "integrity": "sha512-9jxCYrgggy2xd44ZASqI7AMiRVaSiFp+06Kg8BQSU0ijKpBJlwcsqIS8pDT/n6LxuOw2eV5ipvM2C0r1iKzrGA==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.669.0", + "@aws-sdk/types": "3.667.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.1.tgz", - "integrity": "sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q==", - "dev": true, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.662.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.662.0.tgz", + "integrity": "sha512-ikLkXn0igUpnJu2mCZjklvmcDGWT9OaLRv3JyC/cRkTaaSrblPjPM7KKsltxdMTLQ+v7fjCN0TsJpxphMfaOPA==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-split-export-declaration": "^7.22.6", - "globals": "^11.1.0" + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", + "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", + "dependencies": { + "@babel/highlight": "^7.24.6", + "picocolors": "^1.0.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/plugin-transform-classes/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, + "node_modules/@babel/compat-data": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.6.tgz", + "integrity": "sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ==", "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", - "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", - "dev": true, + "node_modules/@babel/core": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.6.tgz", + "integrity": "sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/template": "^7.24.0" + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.6", + "@babel/generator": "^7.24.6", + "@babel/helper-compilation-targets": "^7.24.6", + "@babel/helper-module-transforms": "^7.24.6", + "@babel/helpers": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/template": "^7.24.6", + "@babel/traverse": "^7.24.6", + "@babel/types": "^7.24.6", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.1.tgz", - "integrity": "sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw==", - "dev": true, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/@babel/generator": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz", + "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/types": "^7.24.6", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.1.tgz", - "integrity": "sha512-iIYPIWt3dUmUKKE10s3W+jsQ3icFkw0JyRVyY1B7G4yK/nngAOHLVx8xlhA6b/Jzl/Y0nis8gjqhqKtRDQqHWQ==", + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-flow": "^7.24.1" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", - "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", - "dev": true, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz", + "integrity": "sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/compat-data": "^7.24.6", + "@babel/helper-validator-option": "^7.24.6", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-function-name": { + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", - "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.1.tgz", + "integrity": "sha512-1yJa9dX9g//V6fDebXoEfEsxkZHk3Hcbm+zLhyu6qVgYFLvmTALTeV+jNU9e5RnYtioBrGEOdoI2joMSNQ/+aA==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.24.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", - "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", + "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==", "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", - "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", - "dev": true, + "node_modules/@babel/helper-function-name": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz", + "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", - "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", - "dev": true, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz", + "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==", "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-simple-access": "^7.22.5" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", - "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-replace-supers": "^7.24.1" + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.1.tgz", - "integrity": "sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg==", - "dev": true, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz", + "integrity": "sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", - "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", - "dev": true, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz", + "integrity": "sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-module-imports": "^7.24.6", + "@babel/helper-simple-access": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz", - "integrity": "sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw==", + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", - "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.23.3", - "@babel/types": "^7.23.4" - }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-shorthand-properties": { + "node_modules/@babel/helper-replace-supers": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", - "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", + "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", - "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", - "dev": true, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz", + "integrity": "sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", - "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", - "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", + "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", "dependencies": { - "regenerator-runtime": "^0.14.0" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/template": { + "node_modules/@babel/helper-string-parser": { "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", - "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==", - "dependencies": { - "@babel/code-frame": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/types": "^7.24.6" - }, + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz", + "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse": { + "node_modules/@babel/helper-validator-identifier": { "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz", - "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==", - "dependencies": { - "@babel/code-frame": "^7.24.6", - "@babel/generator": "^7.24.6", - "@babel/helper-environment-visitor": "^7.24.6", - "@babel/helper-function-name": "^7.24.6", - "@babel/helper-hoist-variables": "^7.24.6", - "@babel/helper-split-export-declaration": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/types": "^7.24.6", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", + "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "node_modules/@babel/helper-validator-option": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz", + "integrity": "sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ==", "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/@babel/types": { + "node_modules/@babel/helpers": { "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", - "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.6.tgz", + "integrity": "sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==", "dependencies": { - "@babel/helper-string-parser": "^7.24.6", - "@babel/helper-validator-identifier": "^7.24.6", - "to-fast-properties": "^2.0.0" + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "node_modules/@canvas/image-data": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@canvas/image-data/-/image-data-1.0.0.tgz", - "integrity": "sha512-BxOqI5LgsIQP1odU5KMwV9yoijleOPzHL18/YvNqF9KFSGF2K/DLlYAbDQsWqd/1nbaFuSkYD/191dpMtNh4vw==" - }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "optional": true, + "node_modules/@babel/highlight": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", + "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.6", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, "engines": { - "node": ">=0.1.90" + "node": ">=6.9.0" } }, - "node_modules/@cwasm/webp": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@cwasm/webp/-/webp-0.1.5.tgz", - "integrity": "sha512-ceIZQkyxK+s7mmItNcWqqHdOBiJAxYxTnrnPNgUNjldB1M9j+Bp/3eVIVwC8rUFyN/zoFwuT0331pyY3ackaNA==", + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dependencies": { - "@canvas/image-data": "^1.0.0" + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/@emotion/babel-plugin": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", - "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==", + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.2.0", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" + "color-name": "1.1.3" } }, - "node_modules/@emotion/cache": { - "version": "11.13.1", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz", - "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==", - "dependencies": { - "@emotion/memoize": "^0.9.0", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.0", - "@emotion/weak-memoize": "^0.4.0", - "stylis": "4.2.0" + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" } }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", - "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dependencies": { - "@emotion/memoize": "^0.9.0" + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" - }, - "node_modules/@emotion/react": { - "version": "11.13.3", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", - "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.12.0", - "@emotion/cache": "^11.13.0", - "@emotion/serialize": "^1.3.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", - "@emotion/utils": "^1.4.0", - "@emotion/weak-memoize": "^0.4.0", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" + "node_modules/@babel/parser": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz", + "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==", + "bin": { + "parser": "bin/babel-parser.js" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@emotion/serialize": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz", - "integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==", + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "dev": true, "dependencies": { - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.1", - "csstype": "^3.0.2" + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@emotion/sheet": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" - }, - "node_modules/@emotion/styled": { - "version": "11.13.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz", - "integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==", + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", + "dev": true, "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.12.0", - "@emotion/is-prop-valid": "^1.3.0", - "@emotion/serialize": "^1.3.0", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", - "@emotion/utils": "^1.4.0" + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" }, - "peerDependencies": { - "@emotion/react": "^11.0.0-rc.0", - "react": ">=16.8.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@emotion/unitless": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", - "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, "peerDependencies": { - "react": ">=16.8.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@emotion/utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz", - "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" - }, - "node_modules/@envelop/core": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.0.1.tgz", - "integrity": "sha512-wxA8EyE1fPnlbP0nC/SFI7uU8wSNf4YjxZhAPu0P63QbgIvqHtHsH4L3/u+rsTruzhk3OvNRgQyLsMfaR9uzAQ==", + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.1.tgz", + "integrity": "sha512-sxi2kLTI5DeW5vDtMUsk4mTPwvlUDbjOnoWayhynCwrw4QXRld4QEYwqzY8JmQXaJUtgUuCIurtSRH5sn4c7mA==", + "dev": true, "dependencies": { - "@envelop/types": "5.0.0", - "tslib": "^2.5.0" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@envelop/types": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@envelop/types/-/types-5.0.0.tgz", - "integrity": "sha512-IPjmgSc4KpQRlO4qbEDnBEixvtb06WDmjKfi/7fkZaryh5HuOmTtixe1EupQI5XfXO8joc3d27uUZ0QdC++euA==", + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", + "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", "dependencies": { - "tslib": "^2.5.0" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", + "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", + "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", + "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.1.tgz", + "integrity": "sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.1.tgz", + "integrity": "sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-replace-supers": "^7.24.1", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], "engines": { - "node": ">=12" + "node": ">=4" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", + "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/template": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.1.tgz", + "integrity": "sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.1.tgz", + "integrity": "sha512-iIYPIWt3dUmUKKE10s3W+jsQ3icFkw0JyRVyY1B7G4yK/nngAOHLVx8xlhA6b/Jzl/Y0nis8gjqhqKtRDQqHWQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-flow": "^7.24.1" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", + "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", + "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", + "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", + "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", + "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-simple-access": "^7.22.5" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", + "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-replace-supers": "^7.24.1" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.1.tgz", + "integrity": "sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", + "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz", + "integrity": "sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", + "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.23.3", + "@babel/types": "^7.23.4" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", + "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", + "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", + "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", - "license": "MIT", + "node_modules/@babel/runtime": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", + "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/compat": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.1.1.tgz", - "integrity": "sha512-lpHyRyplhGPL5mGEh6M9O5nnKk0Gz4bFI+Zu6tKlPpDUN7XshWvH9C/px4UVm87IAANE0W81CEsNGbS1KlzXpA==", - "dev": true, + "node_modules/@babel/template": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", + "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==", + "dependencies": { + "@babel/code-frame": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/types": "^7.24.6" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.4", + "node_modules/@babel/traverse": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz", + "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==", + "dependencies": { + "@babel/code-frame": "^7.24.6", + "@babel/generator": "^7.24.6", + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-function-name": "^7.24.6", + "@babel/helper-hoist-variables": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/types": "^7.24.6", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "globals": "^11.1.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node": ">=6.9.0" } }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "engines": { - "node": "*" + "node": ">=4" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, + "node_modules/@babel/types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", + "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@babel/helper-string-parser": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6", + "to-fast-properties": "^2.0.0" }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.9.0" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } + "node_modules/@canvas/image-data": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@canvas/image-data/-/image-data-1.0.0.tgz", + "integrity": "sha512-BxOqI5LgsIQP1odU5KMwV9yoijleOPzHL18/YvNqF9KFSGF2K/DLlYAbDQsWqd/1nbaFuSkYD/191dpMtNh4vw==" }, - "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", - "license": "Apache-2.0", + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "optional": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=0.1.90" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz", - "integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==", - "license": "Apache-2.0", + "node_modules/@cwasm/webp": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@cwasm/webp/-/webp-0.1.5.tgz", + "integrity": "sha512-ceIZQkyxK+s7mmItNcWqqHdOBiJAxYxTnrnPNgUNjldB1M9j+Bp/3eVIVwC8rUFyN/zoFwuT0331pyY3ackaNA==", "dependencies": { - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@canvas/image-data": "^1.0.0" } }, - "node_modules/@faker-js/faker": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.1.tgz", - "integrity": "sha512-4mDeYIgM3By7X6t5E6eYwLAa+2h4DeZDF7thhzIg6XB76jeEvMwadYAMCFJL/R4AnEBcAUO9+gL0vhy3s+qvZA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/fakerjs" - } - ], - "engines": { - "node": ">=18.0.0", - "npm": ">=9.0.0" + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", - "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "node_modules/@emotion/babel-plugin": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", + "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==", "dependencies": { - "@floating-ui/utils": "^0.2.8" + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.2.0", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" } }, - "node_modules/@floating-ui/dom": { - "version": "1.6.11", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz", - "integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==", + "node_modules/@emotion/cache": { + "version": "11.13.1", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz", + "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==", "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.8" + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.0", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" } }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "@emotion/memoize": "^0.9.0" } }, - "node_modules/@floating-ui/utils": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", - "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" }, - "node_modules/@graphql-codegen/add": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-5.0.2.tgz", - "integrity": "sha512-ouBkSvMFUhda5VoKumo/ZvsZM9P5ZTyDsI8LW18VxSNWOjrTeLXBWHG8Gfaai0HwhflPtCYVABbriEcOmrRShQ==", - "dev": true, + "node_modules/@emotion/react": { + "version": "11.13.3", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", + "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.3", - "tslib": "~2.6.0" + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.12.0", + "@emotion/cache": "^11.13.0", + "@emotion/serialize": "^1.3.1", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.0", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@graphql-codegen/cli": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-5.0.2.tgz", - "integrity": "sha512-MBIaFqDiLKuO4ojN6xxG9/xL9wmfD3ZjZ7RsPjwQnSHBCUXnEkdKvX+JVpx87Pq29Ycn8wTJUguXnTZ7Di0Mlw==", - "dev": true, + "node_modules/@emotion/serialize": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz", + "integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==", "dependencies": { - "@babel/generator": "^7.18.13", - "@babel/template": "^7.18.10", - "@babel/types": "^7.18.13", - "@graphql-codegen/client-preset": "^4.2.2", - "@graphql-codegen/core": "^4.0.2", - "@graphql-codegen/plugin-helpers": "^5.0.3", - "@graphql-tools/apollo-engine-loader": "^8.0.0", - "@graphql-tools/code-file-loader": "^8.0.0", - "@graphql-tools/git-loader": "^8.0.0", - "@graphql-tools/github-loader": "^8.0.0", - "@graphql-tools/graphql-file-loader": "^8.0.0", - "@graphql-tools/json-file-loader": "^8.0.0", - "@graphql-tools/load": "^8.0.0", - "@graphql-tools/prisma-loader": "^8.0.0", - "@graphql-tools/url-loader": "^8.0.0", - "@graphql-tools/utils": "^10.0.0", - "@whatwg-node/fetch": "^0.8.0", - "chalk": "^4.1.0", - "cosmiconfig": "^8.1.3", - "debounce": "^1.2.0", - "detect-indent": "^6.0.0", - "graphql-config": "^5.0.2", - "inquirer": "^8.0.0", - "is-glob": "^4.0.1", - "jiti": "^1.17.1", - "json-to-pretty-yaml": "^1.2.2", - "listr2": "^4.0.5", - "log-symbols": "^4.0.0", - "micromatch": "^4.0.5", - "shell-quote": "^1.7.3", - "string-env-interpolation": "^1.0.1", - "ts-log": "^2.2.3", - "tslib": "^2.4.0", - "yaml": "^2.3.1", - "yargs": "^17.0.0" - }, - "bin": { - "gql-gen": "cjs/bin.js", - "graphql-code-generator": "cjs/bin.js", - "graphql-codegen": "cjs/bin.js", - "graphql-codegen-esm": "esm/bin.js" + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + }, + "node_modules/@emotion/styled": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz", + "integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.12.0", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.0" }, "peerDependencies": { - "@parcel/watcher": "^2.1.0", - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" }, "peerDependenciesMeta": { - "@parcel/watcher": { + "@types/react": { "optional": true } } }, - "node_modules/@graphql-codegen/client-preset": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-4.2.4.tgz", - "integrity": "sha512-k1c8v2YxJhhITGQGxViG9asLAoop9m7X9duU7Zztqjc98ooxsUzXICfvAWsH3mLAUibXAx4Ax6BPzKsTtQmBPg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/template": "^7.20.7", - "@graphql-codegen/add": "^5.0.2", - "@graphql-codegen/gql-tag-operations": "4.0.6", - "@graphql-codegen/plugin-helpers": "^5.0.3", - "@graphql-codegen/typed-document-node": "^5.0.6", - "@graphql-codegen/typescript": "^4.0.6", - "@graphql-codegen/typescript-operations": "^4.2.0", - "@graphql-codegen/visitor-plugin-common": "^5.1.0", - "@graphql-tools/documents": "^1.0.0", - "@graphql-tools/utils": "^10.0.0", - "@graphql-typed-document-node/core": "3.2.0", - "tslib": "~2.6.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" }, - "node_modules/@graphql-codegen/core": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/core/-/core-4.0.2.tgz", - "integrity": "sha512-IZbpkhwVqgizcjNiaVzNAzm/xbWT6YnGgeOLwVjm4KbJn3V2jchVtuzHH09G5/WkkLSk2wgbXNdwjM41JxO6Eg==", - "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.3", - "@graphql-tools/schema": "^10.0.0", - "@graphql-tools/utils": "^10.0.0", - "tslib": "~2.6.0" - }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", + "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + "react": ">=16.8.0" } }, - "node_modules/@graphql-codegen/gql-tag-operations": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-4.0.6.tgz", - "integrity": "sha512-y6iXEDpDNjwNxJw3WZqX1/Znj0QHW7+y8O+t2V8qvbTT+3kb2lr9ntc8By7vCr6ctw9tXI4XKaJgpTstJDOwFA==", - "dev": true, + "node_modules/@emotion/utils": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz", + "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, + "node_modules/@envelop/core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.0.1.tgz", + "integrity": "sha512-wxA8EyE1fPnlbP0nC/SFI7uU8wSNf4YjxZhAPu0P63QbgIvqHtHsH4L3/u+rsTruzhk3OvNRgQyLsMfaR9uzAQ==", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.3", - "@graphql-codegen/visitor-plugin-common": "5.1.0", - "@graphql-tools/utils": "^10.0.0", - "auto-bind": "~4.0.0", - "tslib": "~2.6.0" + "@envelop/types": "5.0.0", + "tslib": "^2.5.0" }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@graphql-codegen/plugin-helpers": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-5.0.4.tgz", - "integrity": "sha512-MOIuHFNWUnFnqVmiXtrI+4UziMTYrcquljaI5f/T/Bc7oO7sXcfkAvgkNWEEi9xWreYwvuer3VHCuPI/lAFWbw==", - "dev": true, + "node_modules/@envelop/types": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@envelop/types/-/types-5.0.0.tgz", + "integrity": "sha512-IPjmgSc4KpQRlO4qbEDnBEixvtb06WDmjKfi/7fkZaryh5HuOmTtixe1EupQI5XfXO8joc3d27uUZ0QdC++euA==", "dependencies": { - "@graphql-tools/utils": "^10.0.0", - "change-case-all": "1.0.15", - "common-tags": "1.8.2", - "import-from": "4.0.0", - "lodash": "~4.17.0", - "tslib": "~2.6.0" + "tslib": "^2.5.0" }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@graphql-codegen/schema-ast": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/schema-ast/-/schema-ast-4.0.2.tgz", - "integrity": "sha512-5mVAOQQK3Oz7EtMl/l3vOQdc2aYClUzVDHHkMvZlunc+KlGgl81j8TLa+X7ANIllqU4fUEsQU3lJmk4hXP6K7Q==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.3", - "@graphql-tools/utils": "^10.0.0", - "tslib": "~2.6.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-codegen/typed-document-node": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-5.0.6.tgz", - "integrity": "sha512-US0J95hOE2/W/h42w4oiY+DFKG7IetEN1mQMgXXeat1w6FAR5PlIz4JrRrEkiVfVetZ1g7K78SOwBD8/IJnDiA==", + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.3", - "@graphql-codegen/visitor-plugin-common": "5.1.0", - "auto-bind": "~4.0.0", - "change-case-all": "1.0.15", - "tslib": "~2.6.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-codegen/typescript": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-4.1.0.tgz", - "integrity": "sha512-/fS53Nh6U6c58GTOxqfyKTLQfQv36P8II/vPw/fg0cdcWbALhRPls69P8vXUWjrElmLKzCrdusBWPp/r+AKUBQ==", + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.4", - "@graphql-codegen/schema-ast": "^4.0.2", - "@graphql-codegen/visitor-plugin-common": "5.4.0", - "auto-bind": "~4.0.0", - "tslib": "~2.6.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=16" - }, - "peerDependencies": { - "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + "node": ">=12" } }, - "node_modules/@graphql-codegen/typescript-operations": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-4.2.0.tgz", - "integrity": "sha512-lmuwYb03XC7LNRS8oo9M4/vlOrq/wOKmTLBHlltK2YJ1BO/4K/Q9Jdv/jDmJpNydHVR1fmeF4wAfsIp1f9JibA==", - "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.3", - "@graphql-codegen/typescript": "^4.0.6", - "@graphql-codegen/visitor-plugin-common": "5.1.0", - "auto-bind": "~4.0.0", - "tslib": "~2.6.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/typescript-resolvers": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-resolvers/-/typescript-resolvers-4.2.1.tgz", - "integrity": "sha512-q/ggqNSKNGG9bn49DdZrw2KokagDZmzl1EpxIfzmpHrPa3XaCLfxQuNNEUhqEXtJzQZtLfuYvGy1y+MrTU8WnA==", + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.4", - "@graphql-codegen/typescript": "^4.0.9", - "@graphql-codegen/visitor-plugin-common": "5.3.1", - "@graphql-tools/utils": "^10.0.0", - "auto-bind": "~4.0.0", - "tslib": "~2.6.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-codegen/typescript-resolvers/node_modules/@graphql-codegen/visitor-plugin-common": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-5.3.1.tgz", - "integrity": "sha512-MktoBdNZhSmugiDjmFl1z6rEUUaqyxtFJYWnDilE7onkPgyw//O0M+TuPBJPBWdyV6J2ond0Hdqtq+rkghgSIQ==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.4", - "@graphql-tools/optimize": "^2.0.0", - "@graphql-tools/relay-operation-optimizer": "^7.0.0", - "@graphql-tools/utils": "^10.0.0", - "auto-bind": "~4.0.0", - "change-case-all": "1.0.15", - "dependency-graph": "^0.11.0", - "graphql-tag": "^2.11.0", - "parse-filepath": "^1.0.2", - "tslib": "~2.6.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-codegen/typescript/node_modules/@graphql-codegen/visitor-plugin-common": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-5.4.0.tgz", - "integrity": "sha512-tL7hOrO+4MiNfDiHewhRQCiH9GTAh0M9Y/BZxYGGEdnrfGgqK5pCxtjq7EY/L19VGIyU7hhzYTQ0r1HzEbB4Jw==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.4", - "@graphql-tools/optimize": "^2.0.0", - "@graphql-tools/relay-operation-optimizer": "^7.0.0", - "@graphql-tools/utils": "^10.0.0", - "auto-bind": "~4.0.0", - "change-case-all": "1.0.15", - "dependency-graph": "^0.11.0", - "graphql-tag": "^2.11.0", - "parse-filepath": "^1.0.2", - "tslib": "~2.6.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=16" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + "node": ">=12" } }, - "node_modules/@graphql-codegen/visitor-plugin-common": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-5.1.0.tgz", - "integrity": "sha512-eamQxtA9bjJqI2lU5eYoA1GbdMIRT2X8m8vhWYsVQVWD3qM7sx/IqJU0kx0J3Vd4/CSd36BzL6RKwksibytDIg==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.3", - "@graphql-tools/optimize": "^2.0.0", - "@graphql-tools/relay-operation-optimizer": "^7.0.0", - "@graphql-tools/utils": "^10.0.0", - "auto-bind": "~4.0.0", - "change-case-all": "1.0.15", - "dependency-graph": "^0.11.0", - "graphql-tag": "^2.11.0", - "parse-filepath": "^1.0.2", - "tslib": "~2.6.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-eslint/eslint-plugin": { - "version": "3.20.1", - "resolved": "https://registry.npmjs.org/@graphql-eslint/eslint-plugin/-/eslint-plugin-3.20.1.tgz", - "integrity": "sha512-RbwVlz1gcYG62sECR1u0XqMh8w5e5XMCCZoMvPQ3nJzEBCTfXLGX727GBoRmSvY1x4gJmqNZ1lsOX7lZY14RIw==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@graphql-tools/code-file-loader": "^7.3.6", - "@graphql-tools/graphql-tag-pluck": "^7.3.6", - "@graphql-tools/utils": "^9.0.0", - "chalk": "^4.1.2", - "debug": "^4.3.4", - "fast-glob": "^3.2.12", - "graphql-config": "^4.4.0", - "graphql-depth-limit": "^1.1.0", - "lodash.lowercase": "^4.3.0", - "tslib": "^2.4.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { "node": ">=12" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/batch-execute": { - "version": "8.5.22", - "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-8.5.22.tgz", - "integrity": "sha512-hcV1JaY6NJQFQEwCKrYhpfLK8frSXDbtNMoTur98u10Cmecy1zrqNKSqhEyGetpgHxaJRqszGzKeI3RuroDN6A==", + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "@graphql-tools/utils": "^9.2.1", - "dataloader": "^2.2.2", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.12" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/code-file-loader": { - "version": "7.3.23", - "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-7.3.23.tgz", - "integrity": "sha512-8Wt1rTtyTEs0p47uzsPJ1vAtfAx0jmxPifiNdmo9EOCuUPyQGEbMaik/YkqZ7QUFIEYEQu+Vgfo8tElwOPtx5Q==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@graphql-tools/graphql-tag-pluck": "7.5.2", - "@graphql-tools/utils": "^9.2.1", - "globby": "^11.0.3", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/delegate": { - "version": "9.0.35", - "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-9.0.35.tgz", - "integrity": "sha512-jwPu8NJbzRRMqi4Vp/5QX1vIUeUPpWmlQpOkXQD2r1X45YsVceyUUBnktCrlJlDB4jPRVy7JQGwmYo3KFiOBMA==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], "dev": true, - "dependencies": { - "@graphql-tools/batch-execute": "^8.5.22", - "@graphql-tools/executor": "^0.0.20", - "@graphql-tools/schema": "^9.0.19", - "@graphql-tools/utils": "^9.2.1", - "dataloader": "^2.2.2", - "tslib": "^2.5.0", - "value-or-promise": "^1.0.12" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/executor": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-0.0.20.tgz", - "integrity": "sha512-GdvNc4vszmfeGvUqlcaH1FjBoguvMYzxAfT6tDd4/LgwymepHhinqLNA5otqwVLW+JETcDaK7xGENzFomuE6TA==", - "dev": true, - "dependencies": { - "@graphql-tools/utils": "^9.2.1", - "@graphql-typed-document-node/core": "3.2.0", - "@repeaterjs/repeater": "^3.0.4", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.12" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/executor-graphql-ws": { - "version": "0.0.14", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-0.0.14.tgz", - "integrity": "sha512-P2nlkAsPZKLIXImFhj0YTtny5NQVGSsKnhi7PzXiaHSXc6KkzqbWZHKvikD4PObanqg+7IO58rKFpGXP7eeO+w==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], "dev": true, - "dependencies": { - "@graphql-tools/utils": "^9.2.1", - "@repeaterjs/repeater": "3.0.4", - "@types/ws": "^8.0.0", - "graphql-ws": "5.12.1", - "isomorphic-ws": "5.0.0", - "tslib": "^2.4.0", - "ws": "8.13.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/executor-http": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-0.1.10.tgz", - "integrity": "sha512-hnAfbKv0/lb9s31LhWzawQ5hghBfHS+gYWtqxME6Rl0Aufq9GltiiLBcl7OVVOnkLF0KhwgbYP1mB5VKmgTGpg==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], "dev": true, - "dependencies": { - "@graphql-tools/utils": "^9.2.1", - "@repeaterjs/repeater": "^3.0.4", - "@whatwg-node/fetch": "^0.8.1", - "dset": "^3.1.2", - "extract-files": "^11.0.0", - "meros": "^1.2.1", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.12" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/executor-legacy-ws": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-0.0.11.tgz", - "integrity": "sha512-4ai+NnxlNfvIQ4c70hWFvOZlSUN8lt7yc+ZsrwtNFbFPH/EroIzFMapAxM9zwyv9bH38AdO3TQxZ5zNxgBdvUw==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "@graphql-tools/utils": "^9.2.1", - "@types/ws": "^8.0.0", - "isomorphic-ws": "5.0.0", - "tslib": "^2.4.0", - "ws": "8.13.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/graphql-file-loader": { - "version": "7.5.17", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-7.5.17.tgz", - "integrity": "sha512-hVwwxPf41zOYgm4gdaZILCYnKB9Zap7Ys9OhY1hbwuAuC4MMNY9GpUjoTU3CQc3zUiPoYStyRtUGkHSJZ3HxBw==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], "dev": true, - "dependencies": { - "@graphql-tools/import": "6.7.18", - "@graphql-tools/utils": "^9.2.1", - "globby": "^11.0.3", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/import": { - "version": "6.7.18", - "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-6.7.18.tgz", - "integrity": "sha512-XQDdyZTp+FYmT7as3xRWH/x8dx0QZA2WZqfMF5EWb36a0PiH7WwlRQYIdyYXj8YCLpiWkeBXgBRHmMnwEYR8iQ==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], "dev": true, - "dependencies": { - "@graphql-tools/utils": "^9.2.1", - "resolve-from": "5.0.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/json-file-loader": { - "version": "7.4.18", - "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-7.4.18.tgz", - "integrity": "sha512-AJ1b6Y1wiVgkwsxT5dELXhIVUPs/u3VZ8/0/oOtpcoyO/vAeM5rOvvWegzicOOnQw8G45fgBRMkkRfeuwVt6+w==", + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@graphql-tools/utils": "^9.2.1", - "globby": "^11.0.3", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/load": { - "version": "7.8.14", - "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-7.8.14.tgz", - "integrity": "sha512-ASQvP+snHMYm+FhIaLxxFgVdRaM0vrN9wW2BKInQpktwWTXVyk+yP5nQUCEGmn0RTdlPKrffBaigxepkEAJPrg==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@graphql-tools/schema": "^9.0.18", - "@graphql-tools/utils": "^9.2.1", - "p-limit": "3.1.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/merge": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.4.2.tgz", - "integrity": "sha512-XbrHAaj8yDuINph+sAfuq3QCZ/tKblrTLOpirK0+CAgNlZUCHs0Fa+xtMUURgwCVThLle1AF7svJCxFizygLsw==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@graphql-tools/utils": "^9.2.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/schema": { - "version": "9.0.19", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.19.tgz", - "integrity": "sha512-oBRPoNBtCkk0zbUsyP4GaIzCt8C0aCI4ycIRUL67KK5pOHljKLBBtGT+Jr6hkzA74C8Gco8bpZPe7aWFjiaK2w==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@graphql-tools/merge": "^8.4.1", - "@graphql-tools/utils": "^9.2.1", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.12" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/url-loader": { - "version": "7.17.18", - "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-7.17.18.tgz", - "integrity": "sha512-ear0CiyTj04jCVAxi7TvgbnGDIN2HgqzXzwsfcqiVg9cvjT40NcMlZ2P1lZDgqMkZ9oyLTV8Bw6j+SyG6A+xPw==", - "dev": true, - "dependencies": { - "@ardatan/sync-fetch": "^0.0.1", - "@graphql-tools/delegate": "^9.0.31", - "@graphql-tools/executor-graphql-ws": "^0.0.14", - "@graphql-tools/executor-http": "^0.1.7", - "@graphql-tools/executor-legacy-ws": "^0.0.11", - "@graphql-tools/utils": "^9.2.1", - "@graphql-tools/wrap": "^9.4.2", - "@types/ws": "^8.0.0", - "@whatwg-node/fetch": "^0.8.0", - "isomorphic-ws": "^5.0.0", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.11", - "ws": "^8.12.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/utils": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", - "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "tslib": "^2.4.0" + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/wrap": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-9.4.2.tgz", - "integrity": "sha512-DFcd9r51lmcEKn0JW43CWkkI2D6T9XI1juW/Yo86i04v43O9w2/k4/nx2XTJv4Yv+iXwUw7Ok81PGltwGJSDSA==", + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/compat": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.1.1.tgz", + "integrity": "sha512-lpHyRyplhGPL5mGEh6M9O5nnKk0Gz4bFI+Zu6tKlPpDUN7XshWvH9C/px4UVm87IAANE0W81CEsNGbS1KlzXpA==", "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "license": "Apache-2.0", "dependencies": { - "@graphql-tools/delegate": "^9.0.31", - "@graphql-tools/schema": "^9.0.18", - "@graphql-tools/utils": "^9.2.1", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.12" + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/@repeaterjs/repeater": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.4.tgz", - "integrity": "sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==", - "dev": true - }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/brace-expansion": { + "node_modules/@eslint/config-array/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/cosmiconfig": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.0.0.tgz", - "integrity": "sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ==", - "dev": true, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", "dependencies": { - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=14" + "node": "*" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/graphql-config": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-4.5.0.tgz", - "integrity": "sha512-x6D0/cftpLUJ0Ch1e5sj1TZn6Wcxx4oMfmhaG9shM0DKajA9iR+j1z86GSTQ19fShbGvrSSvbIQsHku6aQ6BBw==", + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { - "@graphql-tools/graphql-file-loader": "^7.3.7", - "@graphql-tools/json-file-loader": "^7.3.7", - "@graphql-tools/load": "^7.5.5", - "@graphql-tools/merge": "^8.2.6", - "@graphql-tools/url-loader": "^7.9.7", - "@graphql-tools/utils": "^9.0.0", - "cosmiconfig": "8.0.0", - "jiti": "1.17.1", - "minimatch": "4.2.3", - "string-env-interpolation": "1.0.1", - "tslib": "^2.4.0" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "cosmiconfig-toml-loader": "^1.0.0", - "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "cosmiconfig-toml-loader": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/graphql-ws": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.12.1.tgz", - "integrity": "sha512-umt4f5NnMK46ChM2coO36PTFhHouBrK9stWWBczERguwYrGnPNxJ9dimU6IyOBfOkC6Izhkg4H8+F51W/8CYDg==", + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "graphql": ">=0.11 <=16" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/jiti": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.17.1.tgz", - "integrity": "sha512-NZIITw8uZQFuzQimqjUxIrIcEdxYDFIe/0xYfIlVXTkiBjjyBEvgasj5bb0/cHtPRD/NziPbT312sFrkI5ALpw==", + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "bin": { - "jiti": "bin/jiti.js" + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/minimatch": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.3.tgz", - "integrity": "sha512-lIUdtK5hdofgCTu3aT0sOaHsYR37viUuIc0rwnnDXImbwFRcumyLMeZaM0t0I/fgxS6s6JMfu0rLD1Wz9pv1ng==", + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=10" + "node": "*" } }, - "node_modules/@graphql-eslint/eslint-plugin/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@graphql-inspector/audit-command": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@graphql-inspector/audit-command/-/audit-command-5.0.6.tgz", - "integrity": "sha512-XfQIKoQj9TTNjEQOKUzTXVhjdJ97zJAqQi4M17OiYKsS8HpiSTmbPEzs+3GcJ/B3OZz+BtW4Oh0OGHTgImDzuw==", - "dependencies": { - "@graphql-inspector/commands": "5.0.4", - "@graphql-inspector/core": "6.1.0", - "@graphql-inspector/logger": "5.0.1", - "@graphql-tools/utils": "10.2.1", - "cli-table3": "0.6.3", - "tslib": "2.6.2" - }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "license": "Apache-2.0", "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@graphql-inspector/audit-command/node_modules/@graphql-tools/utils": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.2.1.tgz", - "integrity": "sha512-U8OMdkkEt3Vp3uYHU2pMc6mwId7axVAcSSmcqJcUmWNPqY2pfee5O655ybTI2kNPWAe58Zu6gLu4Oi4QT4BgWA==", + "node_modules/@eslint/plugin-kit": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz", + "integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==", + "license": "Apache-2.0", "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "cross-inspect": "1.0.0", - "dset": "^3.1.2", - "tslib": "^2.4.0" + "levn": "^0.4.1" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@graphql-inspector/cli": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@graphql-inspector/cli/-/cli-5.0.6.tgz", - "integrity": "sha512-yawOs8fY9AC6eBeeH1CNw1DyAQmBe9dNvAMjS+bG1k9haSn7erzfeWWkoPbqYcFty6FWLlHwVwA00nfpOdJhzQ==", - "dependencies": { - "@babel/core": "7.24.6", - "@graphql-inspector/audit-command": "5.0.6", - "@graphql-inspector/code-loader": "5.0.1", - "@graphql-inspector/commands": "5.0.4", - "@graphql-inspector/config": "4.0.2", - "@graphql-inspector/coverage-command": "6.1.0", - "@graphql-inspector/diff-command": "5.0.6", - "@graphql-inspector/docs-command": "5.0.4", - "@graphql-inspector/git-loader": "5.0.1", - "@graphql-inspector/github-loader": "5.0.1", - "@graphql-inspector/graphql-loader": "5.0.1", - "@graphql-inspector/introspect-command": "5.0.6", - "@graphql-inspector/json-loader": "5.0.1", - "@graphql-inspector/loaders": "4.0.5", - "@graphql-inspector/serve-command": "5.0.5", - "@graphql-inspector/similar-command": "5.0.6", - "@graphql-inspector/url-loader": "5.0.1", - "@graphql-inspector/validate-command": "5.0.6", - "tslib": "2.6.2", - "yargs": "17.7.2" - }, - "bin": { - "graphql-inspector": "cjs/index.js" - }, + "node_modules/@faker-js/faker": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.1.tgz", + "integrity": "sha512-4mDeYIgM3By7X6t5E6eYwLAa+2h4DeZDF7thhzIg6XB76jeEvMwadYAMCFJL/R4AnEBcAUO9+gL0vhy3s+qvZA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "node": ">=18.0.0", + "npm": ">=9.0.0" } }, - "node_modules/@graphql-inspector/code-loader": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@graphql-inspector/code-loader/-/code-loader-5.0.1.tgz", - "integrity": "sha512-kdyP76g0QrtOFRda67+aNshSf0PXYyGJLiGxoVBogpAbkzDRhZQZAsdQVKP0tdEQAn4w0zN6VBdmpF/PAeBO5A==", + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", "dependencies": { - "@graphql-tools/code-file-loader": "8.1.2", - "tslib": "2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "@floating-ui/utils": "^0.2.8" } }, - "node_modules/@graphql-inspector/commands": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@graphql-inspector/commands/-/commands-5.0.4.tgz", - "integrity": "sha512-m6SzYxjkKhor7pV33r1FSL2Wq/epzeWDE1cfPT/eFJ4qKavTBcglr+Vpien6PK1a2vy69GhviFhMoJEakrlZMA==", + "node_modules/@floating-ui/dom": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz", + "integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==", "dependencies": { - "tslib": "2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@graphql-inspector/config": "^4.0.0", - "@graphql-inspector/loaders": "^4.0.0", - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", - "yargs": "17.7.2" + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" } }, - "node_modules/@graphql-inspector/config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@graphql-inspector/config/-/config-4.0.2.tgz", - "integrity": "sha512-fnIwVpGM5AtTr4XyV8NJkDnwpXxZSBzi3BopjuXwBPXXD1F3tcVkCKNT6/5WgUQGfNPskBVbitcOPtM4hIYAOQ==", + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", "dependencies": { - "tslib": "2.6.2" - }, - "engines": { - "node": ">=16.0.0" + "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, - "node_modules/@graphql-inspector/core": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@graphql-inspector/core/-/core-6.1.0.tgz", - "integrity": "sha512-5/kqD5330duUsfMBfhMc0iVld76JwSKTkKi7aOr1x9MvSnP8p1anQo7BCNZ5VY9+EvWn4njHbkNfdS/lrqsi+A==", + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" + }, + "node_modules/@graphql-codegen/add": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-5.0.2.tgz", + "integrity": "sha512-ouBkSvMFUhda5VoKumo/ZvsZM9P5ZTyDsI8LW18VxSNWOjrTeLXBWHG8Gfaai0HwhflPtCYVABbriEcOmrRShQ==", + "dev": true, "dependencies": { - "dependency-graph": "1.0.0", - "object-inspect": "1.13.1", - "tslib": "2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "@graphql-codegen/plugin-helpers": "^5.0.3", + "tslib": "~2.6.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-inspector/core/node_modules/dependency-graph": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", - "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", - "engines": { - "node": ">=4" + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-inspector/coverage-command": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@graphql-inspector/coverage-command/-/coverage-command-6.1.0.tgz", - "integrity": "sha512-c5/nERSACMkVkjlKF6ggUwI4nuKTyO991fqQiM9Dm36Heahu1BsH5BjtHWY6zlOg5b7e0v7X0+wqsLjNUsYGEA==", + "node_modules/@graphql-codegen/cli": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-5.0.2.tgz", + "integrity": "sha512-MBIaFqDiLKuO4ojN6xxG9/xL9wmfD3ZjZ7RsPjwQnSHBCUXnEkdKvX+JVpx87Pq29Ycn8wTJUguXnTZ7Di0Mlw==", + "dev": true, "dependencies": { - "@graphql-inspector/commands": "5.0.4", - "@graphql-inspector/core": "6.1.0", - "@graphql-inspector/logger": "5.0.1", - "@graphql-tools/utils": "10.2.1", - "tslib": "2.6.2" + "@babel/generator": "^7.18.13", + "@babel/template": "^7.18.10", + "@babel/types": "^7.18.13", + "@graphql-codegen/client-preset": "^4.2.2", + "@graphql-codegen/core": "^4.0.2", + "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-tools/apollo-engine-loader": "^8.0.0", + "@graphql-tools/code-file-loader": "^8.0.0", + "@graphql-tools/git-loader": "^8.0.0", + "@graphql-tools/github-loader": "^8.0.0", + "@graphql-tools/graphql-file-loader": "^8.0.0", + "@graphql-tools/json-file-loader": "^8.0.0", + "@graphql-tools/load": "^8.0.0", + "@graphql-tools/prisma-loader": "^8.0.0", + "@graphql-tools/url-loader": "^8.0.0", + "@graphql-tools/utils": "^10.0.0", + "@whatwg-node/fetch": "^0.8.0", + "chalk": "^4.1.0", + "cosmiconfig": "^8.1.3", + "debounce": "^1.2.0", + "detect-indent": "^6.0.0", + "graphql-config": "^5.0.2", + "inquirer": "^8.0.0", + "is-glob": "^4.0.1", + "jiti": "^1.17.1", + "json-to-pretty-yaml": "^1.2.2", + "listr2": "^4.0.5", + "log-symbols": "^4.0.0", + "micromatch": "^4.0.5", + "shell-quote": "^1.7.3", + "string-env-interpolation": "^1.0.1", + "ts-log": "^2.2.3", + "tslib": "^2.4.0", + "yaml": "^2.3.1", + "yargs": "^17.0.0" }, - "engines": { - "node": ">=18.0.0" + "bin": { + "gql-gen": "cjs/bin.js", + "graphql-code-generator": "cjs/bin.js", + "graphql-codegen": "cjs/bin.js", + "graphql-codegen-esm": "esm/bin.js" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "@parcel/watcher": "^2.1.0", + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "@parcel/watcher": { + "optional": true + } } }, - "node_modules/@graphql-inspector/coverage-command/node_modules/@graphql-tools/utils": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.2.1.tgz", - "integrity": "sha512-U8OMdkkEt3Vp3uYHU2pMc6mwId7axVAcSSmcqJcUmWNPqY2pfee5O655ybTI2kNPWAe58Zu6gLu4Oi4QT4BgWA==", + "node_modules/@graphql-codegen/client-preset": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-4.2.4.tgz", + "integrity": "sha512-k1c8v2YxJhhITGQGxViG9asLAoop9m7X9duU7Zztqjc98ooxsUzXICfvAWsH3mLAUibXAx4Ax6BPzKsTtQmBPg==", + "dev": true, "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "cross-inspect": "1.0.0", - "dset": "^3.1.2", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/template": "^7.20.7", + "@graphql-codegen/add": "^5.0.2", + "@graphql-codegen/gql-tag-operations": "4.0.6", + "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-codegen/typed-document-node": "^5.0.6", + "@graphql-codegen/typescript": "^4.0.6", + "@graphql-codegen/typescript-operations": "^4.2.0", + "@graphql-codegen/visitor-plugin-common": "^5.1.0", + "@graphql-tools/documents": "^1.0.0", + "@graphql-tools/utils": "^10.0.0", + "@graphql-typed-document-node/core": "3.2.0", + "tslib": "~2.6.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-inspector/diff-command": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@graphql-inspector/diff-command/-/diff-command-5.0.6.tgz", - "integrity": "sha512-DmFfOM5QHQphBvPyyRNmei1IY9DvoqySd/D3iV7/1iOBnFxNkljM8jqbUGe+jZfcrDdM9DRYJseb5d0atSXBRQ==", + "node_modules/@graphql-codegen/core": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/core/-/core-4.0.2.tgz", + "integrity": "sha512-IZbpkhwVqgizcjNiaVzNAzm/xbWT6YnGgeOLwVjm4KbJn3V2jchVtuzHH09G5/WkkLSk2wgbXNdwjM41JxO6Eg==", + "dev": true, "dependencies": { - "@graphql-inspector/commands": "5.0.4", - "@graphql-inspector/core": "6.1.0", - "@graphql-inspector/logger": "5.0.1", - "tslib": "2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-tools/schema": "^10.0.0", + "@graphql-tools/utils": "^10.0.0", + "tslib": "~2.6.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-inspector/docs-command": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@graphql-inspector/docs-command/-/docs-command-5.0.4.tgz", - "integrity": "sha512-NTQRWYzGNJy4Bnd+0NHNjOdgaETEUG112W+Nei/tPCRTs0Vi/UiW+UkGsQ3KxJozEkwgN8od39bVWohGTOPcpA==", + "node_modules/@graphql-codegen/gql-tag-operations": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-4.0.6.tgz", + "integrity": "sha512-y6iXEDpDNjwNxJw3WZqX1/Znj0QHW7+y8O+t2V8qvbTT+3kb2lr9ntc8By7vCr6ctw9tXI4XKaJgpTstJDOwFA==", + "dev": true, "dependencies": { - "@graphql-inspector/commands": "5.0.4", - "open": "8.4.2", - "tslib": "2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-codegen/visitor-plugin-common": "5.1.0", + "@graphql-tools/utils": "^10.0.0", + "auto-bind": "~4.0.0", + "tslib": "~2.6.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-inspector/git-loader": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@graphql-inspector/git-loader/-/git-loader-5.0.1.tgz", - "integrity": "sha512-eZFNU/y1z4sZ9Axu8mB/J7mW+e78JnWgXG2vcT1TT2E1uzFm0x2oNONM2lgLCZGEJuwQDEnreok5CoHumIdE4Q==", + "node_modules/@graphql-codegen/plugin-helpers": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-5.0.4.tgz", + "integrity": "sha512-MOIuHFNWUnFnqVmiXtrI+4UziMTYrcquljaI5f/T/Bc7oO7sXcfkAvgkNWEEi9xWreYwvuer3VHCuPI/lAFWbw==", + "dev": true, "dependencies": { - "@graphql-tools/git-loader": "8.0.6", - "tslib": "2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "@graphql-tools/utils": "^10.0.0", + "change-case-all": "1.0.15", + "common-tags": "1.8.2", + "import-from": "4.0.0", + "lodash": "~4.17.0", + "tslib": "~2.6.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-inspector/github-loader": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@graphql-inspector/github-loader/-/github-loader-5.0.1.tgz", - "integrity": "sha512-CDsY4V1pEDzr5z5FlYTxcPa/7pKsuT/6xQmo1JghHQuYQPZ5TjtGsyNZwgQOjISMCw7pknXfifPBrFQKt6IOEA==", + "node_modules/@graphql-codegen/schema-ast": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/schema-ast/-/schema-ast-4.0.2.tgz", + "integrity": "sha512-5mVAOQQK3Oz7EtMl/l3vOQdc2aYClUzVDHHkMvZlunc+KlGgl81j8TLa+X7ANIllqU4fUEsQU3lJmk4hXP6K7Q==", + "dev": true, "dependencies": { - "@graphql-tools/github-loader": "8.0.1", - "tslib": "2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-tools/utils": "^10.0.0", + "tslib": "~2.6.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-inspector/graphql-loader": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@graphql-inspector/graphql-loader/-/graphql-loader-5.0.1.tgz", - "integrity": "sha512-VZIcbkMhgak3sW4GehVIX/Qnwu1TmQidvaWs8YUiT+czPxKK1rqY/c/G3arwQDtqAdPMx8IwY1bT83ykfIyxfg==", + "node_modules/@graphql-codegen/typed-document-node": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-5.0.6.tgz", + "integrity": "sha512-US0J95hOE2/W/h42w4oiY+DFKG7IetEN1mQMgXXeat1w6FAR5PlIz4JrRrEkiVfVetZ1g7K78SOwBD8/IJnDiA==", + "dev": true, "dependencies": { - "@graphql-tools/graphql-file-loader": "8.0.1", - "tslib": "2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-codegen/visitor-plugin-common": "5.1.0", + "auto-bind": "~4.0.0", + "change-case-all": "1.0.15", + "tslib": "~2.6.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-inspector/introspect-command": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@graphql-inspector/introspect-command/-/introspect-command-5.0.6.tgz", - "integrity": "sha512-qtYZLObzezNR5aS1qFeECwcK+MQGWDDywI3UzSKKtTLfoQJTAv3ECoo9PZxGFC2aEmZAKTEfkFICkbE9OCHu/w==", + "node_modules/@graphql-codegen/typescript": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-4.1.0.tgz", + "integrity": "sha512-/fS53Nh6U6c58GTOxqfyKTLQfQv36P8II/vPw/fg0cdcWbALhRPls69P8vXUWjrElmLKzCrdusBWPp/r+AKUBQ==", + "dev": true, "dependencies": { - "@graphql-inspector/commands": "5.0.4", - "@graphql-inspector/core": "6.1.0", - "@graphql-inspector/logger": "5.0.1", - "tslib": "2.6.2" + "@graphql-codegen/plugin-helpers": "^5.0.4", + "@graphql-codegen/schema-ast": "^4.0.2", + "@graphql-codegen/visitor-plugin-common": "5.4.0", + "auto-bind": "~4.0.0", + "tslib": "~2.6.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=16" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-inspector/json-loader": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@graphql-inspector/json-loader/-/json-loader-5.0.1.tgz", - "integrity": "sha512-ql5zI2E/RNgLKDJ2HilTds2lUTv8ZXQfY5HG29iia85q/CIFslVTDbhzhbXRqmz4jsLd3KCi1LxpAeYQQMhCSQ==", + "node_modules/@graphql-codegen/typescript-operations": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-4.2.0.tgz", + "integrity": "sha512-lmuwYb03XC7LNRS8oo9M4/vlOrq/wOKmTLBHlltK2YJ1BO/4K/Q9Jdv/jDmJpNydHVR1fmeF4wAfsIp1f9JibA==", + "dev": true, "dependencies": { - "@graphql-tools/json-file-loader": "8.0.1", - "tslib": "2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-codegen/typescript": "^4.0.6", + "@graphql-codegen/visitor-plugin-common": "5.1.0", + "auto-bind": "~4.0.0", + "tslib": "~2.6.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-inspector/loaders": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@graphql-inspector/loaders/-/loaders-4.0.5.tgz", - "integrity": "sha512-MQj82Pbo4YVgS1E3IjVvP3ByLQKQ6HHrjK+S21szXx46cKPxlc+MeKHpjfERSCmbdKAinP0MMHxVrmk7hyktow==", + "node_modules/@graphql-codegen/typescript-resolvers": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-resolvers/-/typescript-resolvers-4.2.1.tgz", + "integrity": "sha512-q/ggqNSKNGG9bn49DdZrw2KokagDZmzl1EpxIfzmpHrPa3XaCLfxQuNNEUhqEXtJzQZtLfuYvGy1y+MrTU8WnA==", + "dev": true, "dependencies": { - "@graphql-tools/code-file-loader": "8.1.2", - "@graphql-tools/load": "8.0.2", - "@graphql-tools/utils": "10.2.1", - "tslib": "2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "@graphql-codegen/plugin-helpers": "^5.0.4", + "@graphql-codegen/typescript": "^4.0.9", + "@graphql-codegen/visitor-plugin-common": "5.3.1", + "@graphql-tools/utils": "^10.0.0", + "auto-bind": "~4.0.0", + "tslib": "~2.6.0" }, "peerDependencies": { - "@graphql-inspector/config": "^4.0.0", - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-inspector/loaders/node_modules/@graphql-tools/utils": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.2.1.tgz", - "integrity": "sha512-U8OMdkkEt3Vp3uYHU2pMc6mwId7axVAcSSmcqJcUmWNPqY2pfee5O655ybTI2kNPWAe58Zu6gLu4Oi4QT4BgWA==", + "node_modules/@graphql-codegen/typescript-resolvers/node_modules/@graphql-codegen/visitor-plugin-common": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-5.3.1.tgz", + "integrity": "sha512-MktoBdNZhSmugiDjmFl1z6rEUUaqyxtFJYWnDilE7onkPgyw//O0M+TuPBJPBWdyV6J2ond0Hdqtq+rkghgSIQ==", + "dev": true, "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "cross-inspect": "1.0.0", - "dset": "^3.1.2", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" + "@graphql-codegen/plugin-helpers": "^5.0.4", + "@graphql-tools/optimize": "^2.0.0", + "@graphql-tools/relay-operation-optimizer": "^7.0.0", + "@graphql-tools/utils": "^10.0.0", + "auto-bind": "~4.0.0", + "change-case-all": "1.0.15", + "dependency-graph": "^0.11.0", + "graphql-tag": "^2.11.0", + "parse-filepath": "^1.0.2", + "tslib": "~2.6.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-inspector/logger": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@graphql-inspector/logger/-/logger-5.0.1.tgz", - "integrity": "sha512-rEo+HoQt+qjdayy7p5vcR9GeGTdKXmN0LbIm3W+jKKoXeAMlV4zHxnOW6jEhO6E0eVQxf8Sc1TlcH78i2P2a9w==", + "node_modules/@graphql-codegen/typescript/node_modules/@graphql-codegen/visitor-plugin-common": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-5.4.0.tgz", + "integrity": "sha512-tL7hOrO+4MiNfDiHewhRQCiH9GTAh0M9Y/BZxYGGEdnrfGgqK5pCxtjq7EY/L19VGIyU7hhzYTQ0r1HzEbB4Jw==", + "dev": true, "dependencies": { - "chalk": "4.1.2", - "figures": "3.2.0", - "log-symbols": "4.1.0", - "std-env": "3.7.0", - "tslib": "2.6.2" + "@graphql-codegen/plugin-helpers": "^5.0.4", + "@graphql-tools/optimize": "^2.0.0", + "@graphql-tools/relay-operation-optimizer": "^7.0.0", + "@graphql-tools/utils": "^10.0.0", + "auto-bind": "~4.0.0", + "change-case-all": "1.0.15", + "dependency-graph": "^0.11.0", + "graphql-tag": "^2.11.0", + "parse-filepath": "^1.0.2", + "tslib": "~2.6.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-inspector/serve-command": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@graphql-inspector/serve-command/-/serve-command-5.0.5.tgz", - "integrity": "sha512-dWAg51LHrXoZb5NqE/G/nTaeJ2FrQJZc+mCz6l3fTBL6pU6szyMx4+Cxq+JmGCJVD71N4Fh+h9B4psDpnOFtBQ==", - "dependencies": { - "@graphql-inspector/commands": "5.0.4", - "@graphql-inspector/logger": "5.0.1", - "graphql-yoga": "5.3.1", - "open": "8.4.2", - "tslib": "2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node_modules/@graphql-codegen/visitor-plugin-common": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-5.1.0.tgz", + "integrity": "sha512-eamQxtA9bjJqI2lU5eYoA1GbdMIRT2X8m8vhWYsVQVWD3qM7sx/IqJU0kx0J3Vd4/CSd36BzL6RKwksibytDIg==", + "dev": true, + "dependencies": { + "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-tools/optimize": "^2.0.0", + "@graphql-tools/relay-operation-optimizer": "^7.0.0", + "@graphql-tools/utils": "^10.0.0", + "auto-bind": "~4.0.0", + "change-case-all": "1.0.15", + "dependency-graph": "^0.11.0", + "graphql-tag": "^2.11.0", + "parse-filepath": "^1.0.2", + "tslib": "~2.6.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-inspector/similar-command": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@graphql-inspector/similar-command/-/similar-command-5.0.6.tgz", - "integrity": "sha512-40SaZtxIXEZ0V/EkiG6N2in+PSeVoVcwmtl1ETbysGXZ6xC9Fu+Qi0lE7lbKWKjzw5nv9hYpznDJ1oIsBuN+hQ==", + "node_modules/@graphql-eslint/eslint-plugin": { + "version": "3.20.1", + "resolved": "https://registry.npmjs.org/@graphql-eslint/eslint-plugin/-/eslint-plugin-3.20.1.tgz", + "integrity": "sha512-RbwVlz1gcYG62sECR1u0XqMh8w5e5XMCCZoMvPQ3nJzEBCTfXLGX727GBoRmSvY1x4gJmqNZ1lsOX7lZY14RIw==", + "dev": true, "dependencies": { - "@graphql-inspector/commands": "5.0.4", - "@graphql-inspector/core": "6.1.0", - "@graphql-inspector/logger": "5.0.1", - "tslib": "2.6.2" + "@babel/code-frame": "^7.18.6", + "@graphql-tools/code-file-loader": "^7.3.6", + "@graphql-tools/graphql-tag-pluck": "^7.3.6", + "@graphql-tools/utils": "^9.0.0", + "chalk": "^4.1.2", + "debug": "^4.3.4", + "fast-glob": "^3.2.12", + "graphql-config": "^4.4.0", + "graphql-depth-limit": "^1.1.0", + "lodash.lowercase": "^4.3.0", + "tslib": "^2.4.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=12" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-inspector/url-loader": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@graphql-inspector/url-loader/-/url-loader-5.0.1.tgz", - "integrity": "sha512-7OPJfTJgqptJyfsrpntsn3GEMpSZWxkJO+KaMIZfqDsiWN/zyvNqB0Amogi3d7xxtU1fnB3NCN5VWCFuiRSPXg==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/batch-execute": { + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-8.5.22.tgz", + "integrity": "sha512-hcV1JaY6NJQFQEwCKrYhpfLK8frSXDbtNMoTur98u10Cmecy1zrqNKSqhEyGetpgHxaJRqszGzKeI3RuroDN6A==", + "dev": true, "dependencies": { - "@graphql-tools/url-loader": "8.0.2", - "tslib": "2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "@graphql-tools/utils": "^9.2.1", + "dataloader": "^2.2.2", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-inspector/validate-command": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@graphql-inspector/validate-command/-/validate-command-5.0.6.tgz", - "integrity": "sha512-SO4esOmsdUEueGA2kMjsoXSrvQrtZnJF7wKhcUwJrxIBm8aHf1V5wtorAk4ajIzhlD6a5Yd35GHI9hbjXnIZuQ==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/code-file-loader": { + "version": "7.3.23", + "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-7.3.23.tgz", + "integrity": "sha512-8Wt1rTtyTEs0p47uzsPJ1vAtfAx0jmxPifiNdmo9EOCuUPyQGEbMaik/YkqZ7QUFIEYEQu+Vgfo8tElwOPtx5Q==", + "dev": true, "dependencies": { - "@graphql-inspector/commands": "5.0.4", - "@graphql-inspector/core": "6.1.0", - "@graphql-inspector/logger": "5.0.1", - "@graphql-tools/utils": "10.2.1", - "tslib": "2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "@graphql-tools/graphql-tag-pluck": "7.5.2", + "@graphql-tools/utils": "^9.2.1", + "globby": "^11.0.3", + "tslib": "^2.4.0", + "unixify": "^1.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-inspector/validate-command/node_modules/@graphql-tools/utils": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.2.1.tgz", - "integrity": "sha512-U8OMdkkEt3Vp3uYHU2pMc6mwId7axVAcSSmcqJcUmWNPqY2pfee5O655ybTI2kNPWAe58Zu6gLu4Oi4QT4BgWA==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/delegate": { + "version": "9.0.35", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-9.0.35.tgz", + "integrity": "sha512-jwPu8NJbzRRMqi4Vp/5QX1vIUeUPpWmlQpOkXQD2r1X45YsVceyUUBnktCrlJlDB4jPRVy7JQGwmYo3KFiOBMA==", + "dev": true, "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "cross-inspect": "1.0.0", - "dset": "^3.1.2", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" + "@graphql-tools/batch-execute": "^8.5.22", + "@graphql-tools/executor": "^0.0.20", + "@graphql-tools/schema": "^9.0.19", + "@graphql-tools/utils": "^9.2.1", + "dataloader": "^2.2.2", + "tslib": "^2.5.0", + "value-or-promise": "^1.0.12" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/apollo-engine-loader": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-8.0.1.tgz", - "integrity": "sha512-NaPeVjtrfbPXcl+MLQCJLWtqe2/E4bbAqcauEOQ+3sizw1Fc2CNmhHRF8a6W4D0ekvTRRXAMptXYgA2uConbrA==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/executor": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-0.0.20.tgz", + "integrity": "sha512-GdvNc4vszmfeGvUqlcaH1FjBoguvMYzxAfT6tDd4/LgwymepHhinqLNA5otqwVLW+JETcDaK7xGENzFomuE6TA==", "dev": true, "dependencies": { - "@ardatan/sync-fetch": "^0.0.1", - "@graphql-tools/utils": "^10.0.13", - "@whatwg-node/fetch": "^0.9.0", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" + "@graphql-tools/utils": "^9.2.1", + "@graphql-typed-document-node/core": "3.2.0", + "@repeaterjs/repeater": "^3.0.4", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/apollo-engine-loader/node_modules/@whatwg-node/events": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.1.tgz", - "integrity": "sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==", - "dev": true, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@graphql-tools/apollo-engine-loader/node_modules/@whatwg-node/fetch": { - "version": "0.9.17", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.9.17.tgz", - "integrity": "sha512-TDYP3CpCrxwxpiNY0UMNf096H5Ihf67BK1iKGegQl5u9SlpEDYrvnV71gWBGJm+Xm31qOy8ATgma9rm8Pe7/5Q==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/executor-graphql-ws": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-0.0.14.tgz", + "integrity": "sha512-P2nlkAsPZKLIXImFhj0YTtny5NQVGSsKnhi7PzXiaHSXc6KkzqbWZHKvikD4PObanqg+7IO58rKFpGXP7eeO+w==", "dev": true, "dependencies": { - "@whatwg-node/node-fetch": "^0.5.7", - "urlpattern-polyfill": "^10.0.0" + "@graphql-tools/utils": "^9.2.1", + "@repeaterjs/repeater": "3.0.4", + "@types/ws": "^8.0.0", + "graphql-ws": "5.12.1", + "isomorphic-ws": "5.0.0", + "tslib": "^2.4.0", + "ws": "8.13.0" }, - "engines": { - "node": ">=16.0.0" + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/apollo-engine-loader/node_modules/@whatwg-node/node-fetch": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.5.10.tgz", - "integrity": "sha512-KIAHepie/T1PRkUfze4t+bPlyvpxlWiXTPtcGlbIZ0vWkBJMdRmCg4ZrJ2y4XaO1eTPo1HlWYUuj1WvoIpumqg==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/executor-http": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-0.1.10.tgz", + "integrity": "sha512-hnAfbKv0/lb9s31LhWzawQ5hghBfHS+gYWtqxME6Rl0Aufq9GltiiLBcl7OVVOnkLF0KhwgbYP1mB5VKmgTGpg==", "dev": true, "dependencies": { - "@kamilkisiela/fast-url-parser": "^1.1.4", - "@whatwg-node/events": "^0.1.0", - "busboy": "^1.6.0", - "fast-querystring": "^1.1.1", - "tslib": "^2.3.1" + "@graphql-tools/utils": "^9.2.1", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/fetch": "^0.8.1", + "dset": "^3.1.2", + "extract-files": "^11.0.0", + "meros": "^1.2.1", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" }, - "engines": { - "node": ">=16.0.0" + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/apollo-engine-loader/node_modules/urlpattern-polyfill": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", - "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", - "dev": true - }, - "node_modules/@graphql-tools/batch-execute": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.4.tgz", - "integrity": "sha512-kkebDLXgDrep5Y0gK1RN3DMUlLqNhg60OAz0lTCqrYeja6DshxLtLkj+zV4mVbBA4mQOEoBmw6g1LZs3dA84/w==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/executor-legacy-ws": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-0.0.11.tgz", + "integrity": "sha512-4ai+NnxlNfvIQ4c70hWFvOZlSUN8lt7yc+ZsrwtNFbFPH/EroIzFMapAxM9zwyv9bH38AdO3TQxZ5zNxgBdvUw==", + "dev": true, "dependencies": { - "@graphql-tools/utils": "^10.0.13", - "dataloader": "^2.2.2", + "@graphql-tools/utils": "^9.2.1", + "@types/ws": "^8.0.0", + "isomorphic-ws": "5.0.0", "tslib": "^2.4.0", - "value-or-promise": "^1.0.12" - }, - "engines": { - "node": ">=16.0.0" + "ws": "8.13.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/code-file-loader": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-8.1.2.tgz", - "integrity": "sha512-GrLzwl1QV2PT4X4TEEfuTmZYzIZHLqoTGBjczdUzSqgCCcqwWzLB3qrJxFQfI8e5s1qZ1bhpsO9NoMn7tvpmyA==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/graphql-file-loader": { + "version": "7.5.17", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-7.5.17.tgz", + "integrity": "sha512-hVwwxPf41zOYgm4gdaZILCYnKB9Zap7Ys9OhY1hbwuAuC4MMNY9GpUjoTU3CQc3zUiPoYStyRtUGkHSJZ3HxBw==", + "dev": true, "dependencies": { - "@graphql-tools/graphql-tag-pluck": "8.3.1", - "@graphql-tools/utils": "^10.0.13", + "@graphql-tools/import": "6.7.18", + "@graphql-tools/utils": "^9.2.1", "globby": "^11.0.3", "tslib": "^2.4.0", "unixify": "^1.0.0" }, - "engines": { - "node": ">=16.0.0" - }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/code-file-loader/node_modules/@graphql-tools/graphql-tag-pluck": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.1.tgz", - "integrity": "sha512-ujits9tMqtWQQq4FI4+qnVPpJvSEn7ogKtyN/gfNT+ErIn6z1e4gyVGQpTK5sgAUXq1lW4gU/5fkFFC5/sL2rQ==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/import": { + "version": "6.7.18", + "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-6.7.18.tgz", + "integrity": "sha512-XQDdyZTp+FYmT7as3xRWH/x8dx0QZA2WZqfMF5EWb36a0PiH7WwlRQYIdyYXj8YCLpiWkeBXgBRHmMnwEYR8iQ==", + "dev": true, "dependencies": { - "@babel/core": "^7.22.9", - "@babel/parser": "^7.16.8", - "@babel/plugin-syntax-import-assertions": "^7.20.0", - "@babel/traverse": "^7.16.8", - "@babel/types": "^7.16.8", - "@graphql-tools/utils": "^10.0.13", + "@graphql-tools/utils": "^9.2.1", + "resolve-from": "5.0.0", "tslib": "^2.4.0" }, - "engines": { - "node": ">=16.0.0" - }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/delegate": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.0.4.tgz", - "integrity": "sha512-WswZRbQZMh/ebhc8zSomK9DIh6Pd5KbuiMsyiKkKz37TWTrlCOe+4C/fyrBFez30ksq6oFyCeSKMwfrCbeGo0Q==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/json-file-loader": { + "version": "7.4.18", + "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-7.4.18.tgz", + "integrity": "sha512-AJ1b6Y1wiVgkwsxT5dELXhIVUPs/u3VZ8/0/oOtpcoyO/vAeM5rOvvWegzicOOnQw8G45fgBRMkkRfeuwVt6+w==", + "dev": true, "dependencies": { - "@graphql-tools/batch-execute": "^9.0.4", - "@graphql-tools/executor": "^1.2.1", - "@graphql-tools/schema": "^10.0.3", - "@graphql-tools/utils": "^10.0.13", - "dataloader": "^2.2.2", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=16.0.0" + "@graphql-tools/utils": "^9.2.1", + "globby": "^11.0.3", + "tslib": "^2.4.0", + "unixify": "^1.0.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/documents": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/documents/-/documents-1.0.0.tgz", - "integrity": "sha512-rHGjX1vg/nZ2DKqRGfDPNC55CWZBMldEVcH+91BThRa6JeT80NqXknffLLEZLRUxyikCfkwMsk6xR3UNMqG0Rg==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/load": { + "version": "7.8.14", + "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-7.8.14.tgz", + "integrity": "sha512-ASQvP+snHMYm+FhIaLxxFgVdRaM0vrN9wW2BKInQpktwWTXVyk+yP5nQUCEGmn0RTdlPKrffBaigxepkEAJPrg==", "dev": true, "dependencies": { - "lodash.sortby": "^4.7.0", + "@graphql-tools/schema": "^9.0.18", + "@graphql-tools/utils": "^9.2.1", + "p-limit": "3.1.0", "tslib": "^2.4.0" }, - "engines": { - "node": ">=16.0.0" + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/merge": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.4.2.tgz", + "integrity": "sha512-XbrHAaj8yDuINph+sAfuq3QCZ/tKblrTLOpirK0+CAgNlZUCHs0Fa+xtMUURgwCVThLle1AF7svJCxFizygLsw==", + "dev": true, + "dependencies": { + "@graphql-tools/utils": "^9.2.1", + "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/executor": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.2.6.tgz", - "integrity": "sha512-+1kjfqzM5T2R+dCw7F4vdJ3CqG+fY/LYJyhNiWEFtq0ToLwYzR/KKyD8YuzTirEjSxWTVlcBh7endkx5n5F6ew==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/schema": { + "version": "9.0.19", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.19.tgz", + "integrity": "sha512-oBRPoNBtCkk0zbUsyP4GaIzCt8C0aCI4ycIRUL67KK5pOHljKLBBtGT+Jr6hkzA74C8Gco8bpZPe7aWFjiaK2w==", + "dev": true, "dependencies": { - "@graphql-tools/utils": "^10.1.1", - "@graphql-typed-document-node/core": "3.2.0", - "@repeaterjs/repeater": "^3.0.4", + "@graphql-tools/merge": "^8.4.1", + "@graphql-tools/utils": "^9.2.1", "tslib": "^2.4.0", "value-or-promise": "^1.0.12" }, - "engines": { - "node": ">=16.0.0" - }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/executor-graphql-ws": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-1.1.2.tgz", - "integrity": "sha512-+9ZK0rychTH1LUv4iZqJ4ESbmULJMTsv3XlFooPUngpxZkk00q6LqHKJRrsLErmQrVaC7cwQCaRBJa0teK17Lg==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/url-loader": { + "version": "7.17.18", + "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-7.17.18.tgz", + "integrity": "sha512-ear0CiyTj04jCVAxi7TvgbnGDIN2HgqzXzwsfcqiVg9cvjT40NcMlZ2P1lZDgqMkZ9oyLTV8Bw6j+SyG6A+xPw==", + "dev": true, "dependencies": { - "@graphql-tools/utils": "^10.0.13", + "@ardatan/sync-fetch": "^0.0.1", + "@graphql-tools/delegate": "^9.0.31", + "@graphql-tools/executor-graphql-ws": "^0.0.14", + "@graphql-tools/executor-http": "^0.1.7", + "@graphql-tools/executor-legacy-ws": "^0.0.11", + "@graphql-tools/utils": "^9.2.1", + "@graphql-tools/wrap": "^9.4.2", "@types/ws": "^8.0.0", - "graphql-ws": "^5.14.0", + "@whatwg-node/fetch": "^0.8.0", "isomorphic-ws": "^5.0.0", "tslib": "^2.4.0", - "ws": "^8.13.0" + "value-or-promise": "^1.0.11", + "ws": "^8.12.0" }, - "engines": { - "node": ">=16.0.0" + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/utils": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", + "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", + "dev": true, + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/executor-http": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-1.0.9.tgz", - "integrity": "sha512-+NXaZd2MWbbrWHqU4EhXcrDbogeiCDmEbrAN+rMn4Nu2okDjn2MTFDbTIab87oEubQCH4Te1wDkWPKrzXup7+Q==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@graphql-tools/wrap": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-9.4.2.tgz", + "integrity": "sha512-DFcd9r51lmcEKn0JW43CWkkI2D6T9XI1juW/Yo86i04v43O9w2/k4/nx2XTJv4Yv+iXwUw7Ok81PGltwGJSDSA==", + "dev": true, "dependencies": { - "@graphql-tools/utils": "^10.0.13", - "@repeaterjs/repeater": "^3.0.4", - "@whatwg-node/fetch": "^0.9.0", - "extract-files": "^11.0.0", - "meros": "^1.2.1", + "@graphql-tools/delegate": "^9.0.31", + "@graphql-tools/schema": "^9.0.18", + "@graphql-tools/utils": "^9.2.1", "tslib": "^2.4.0", "value-or-promise": "^1.0.12" }, - "engines": { - "node": ">=16.0.0" - }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/executor-http/node_modules/@whatwg-node/events": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.1.tgz", - "integrity": "sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==", - "engines": { - "node": ">=16.0.0" - } + "node_modules/@graphql-eslint/eslint-plugin/node_modules/@repeaterjs/repeater": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.4.tgz", + "integrity": "sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==", + "dev": true }, - "node_modules/@graphql-tools/executor-http/node_modules/@whatwg-node/fetch": { - "version": "0.9.17", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.9.17.tgz", - "integrity": "sha512-TDYP3CpCrxwxpiNY0UMNf096H5Ihf67BK1iKGegQl5u9SlpEDYrvnV71gWBGJm+Xm31qOy8ATgma9rm8Pe7/5Q==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { - "@whatwg-node/node-fetch": "^0.5.7", - "urlpattern-polyfill": "^10.0.0" - }, - "engines": { - "node": ">=16.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@graphql-tools/executor-http/node_modules/@whatwg-node/node-fetch": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.5.10.tgz", - "integrity": "sha512-KIAHepie/T1PRkUfze4t+bPlyvpxlWiXTPtcGlbIZ0vWkBJMdRmCg4ZrJ2y4XaO1eTPo1HlWYUuj1WvoIpumqg==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/cosmiconfig": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.0.0.tgz", + "integrity": "sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ==", + "dev": true, "dependencies": { - "@kamilkisiela/fast-url-parser": "^1.1.4", - "@whatwg-node/events": "^0.1.0", - "busboy": "^1.6.0", - "fast-querystring": "^1.1.1", - "tslib": "^2.3.1" + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=14" } }, - "node_modules/@graphql-tools/executor-http/node_modules/urlpattern-polyfill": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", - "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==" - }, - "node_modules/@graphql-tools/executor-legacy-ws": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.0.6.tgz", - "integrity": "sha512-lDSxz9VyyquOrvSuCCnld3256Hmd+QI2lkmkEv7d4mdzkxkK4ddAWW1geQiWrQvWmdsmcnGGlZ7gDGbhEExwqg==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/graphql-config": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-4.5.0.tgz", + "integrity": "sha512-x6D0/cftpLUJ0Ch1e5sj1TZn6Wcxx4oMfmhaG9shM0DKajA9iR+j1z86GSTQ19fShbGvrSSvbIQsHku6aQ6BBw==", + "dev": true, "dependencies": { - "@graphql-tools/utils": "^10.0.13", - "@types/ws": "^8.0.0", - "isomorphic-ws": "^5.0.0", - "tslib": "^2.4.0", - "ws": "^8.15.0" + "@graphql-tools/graphql-file-loader": "^7.3.7", + "@graphql-tools/json-file-loader": "^7.3.7", + "@graphql-tools/load": "^7.5.5", + "@graphql-tools/merge": "^8.2.6", + "@graphql-tools/url-loader": "^7.9.7", + "@graphql-tools/utils": "^9.0.0", + "cosmiconfig": "8.0.0", + "jiti": "1.17.1", + "minimatch": "4.2.3", + "string-env-interpolation": "1.0.1", + "tslib": "^2.4.0" }, "engines": { - "node": ">=16.0.0" + "node": ">= 10.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "cosmiconfig-toml-loader": "^1.0.0", + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "cosmiconfig-toml-loader": { + "optional": true + } } }, - "node_modules/@graphql-tools/git-loader": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-8.0.6.tgz", - "integrity": "sha512-FQFO4H5wHAmHVyuUQrjvPE8re3qJXt50TWHuzrK3dEaief7JosmlnkLMDMbMBwtwITz9u1Wpl6doPhT2GwKtlw==", - "dependencies": { - "@graphql-tools/graphql-tag-pluck": "8.3.1", - "@graphql-tools/utils": "^10.0.13", - "is-glob": "4.0.3", - "micromatch": "^4.0.4", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - }, + "node_modules/@graphql-eslint/eslint-plugin/node_modules/graphql-ws": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.12.1.tgz", + "integrity": "sha512-umt4f5NnMK46ChM2coO36PTFhHouBrK9stWWBczERguwYrGnPNxJ9dimU6IyOBfOkC6Izhkg4H8+F51W/8CYDg==", + "dev": true, "engines": { - "node": ">=16.0.0" + "node": ">=10" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "graphql": ">=0.11 <=16" } }, - "node_modules/@graphql-tools/git-loader/node_modules/@graphql-tools/graphql-tag-pluck": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.1.tgz", - "integrity": "sha512-ujits9tMqtWQQq4FI4+qnVPpJvSEn7ogKtyN/gfNT+ErIn6z1e4gyVGQpTK5sgAUXq1lW4gU/5fkFFC5/sL2rQ==", + "node_modules/@graphql-eslint/eslint-plugin/node_modules/jiti": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.17.1.tgz", + "integrity": "sha512-NZIITw8uZQFuzQimqjUxIrIcEdxYDFIe/0xYfIlVXTkiBjjyBEvgasj5bb0/cHtPRD/NziPbT312sFrkI5ALpw==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/@graphql-eslint/eslint-plugin/node_modules/minimatch": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.3.tgz", + "integrity": "sha512-lIUdtK5hdofgCTu3aT0sOaHsYR37viUuIc0rwnnDXImbwFRcumyLMeZaM0t0I/fgxS6s6JMfu0rLD1Wz9pv1ng==", + "dev": true, "dependencies": { - "@babel/core": "^7.22.9", - "@babel/parser": "^7.16.8", - "@babel/plugin-syntax-import-assertions": "^7.20.0", - "@babel/traverse": "^7.16.8", - "@babel/types": "^7.16.8", - "@graphql-tools/utils": "^10.0.13", - "tslib": "^2.4.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16.0.0" + "node": ">=10" + } + }, + "node_modules/@graphql-eslint/eslint-plugin/node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "engines": { + "node": ">=10.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, - "node_modules/@graphql-tools/github-loader": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-8.0.1.tgz", - "integrity": "sha512-W4dFLQJ5GtKGltvh/u1apWRFKBQOsDzFxO9cJkOYZj1VzHCpRF43uLST4VbCfWve+AwBqOuKr7YgkHoxpRMkcg==", + "node_modules/@graphql-inspector/audit-command": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@graphql-inspector/audit-command/-/audit-command-5.0.6.tgz", + "integrity": "sha512-XfQIKoQj9TTNjEQOKUzTXVhjdJ97zJAqQi4M17OiYKsS8HpiSTmbPEzs+3GcJ/B3OZz+BtW4Oh0OGHTgImDzuw==", "dependencies": { - "@ardatan/sync-fetch": "^0.0.1", - "@graphql-tools/executor-http": "^1.0.9", - "@graphql-tools/graphql-tag-pluck": "^8.0.0", - "@graphql-tools/utils": "^10.0.13", - "@whatwg-node/fetch": "^0.9.0", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.12" + "@graphql-inspector/commands": "5.0.4", + "@graphql-inspector/core": "6.1.0", + "@graphql-inspector/logger": "5.0.1", + "@graphql-tools/utils": "10.2.1", + "cli-table3": "0.6.3", + "tslib": "2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/github-loader/node_modules/@graphql-tools/graphql-tag-pluck": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.0.tgz", - "integrity": "sha512-gNqukC+s7iHC7vQZmx1SEJQmLnOguBq+aqE2zV2+o1hxkExvKqyFli1SY/9gmukFIKpKutCIj+8yLOM+jARutw==", + "node_modules/@graphql-inspector/audit-command/node_modules/@graphql-tools/utils": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.2.1.tgz", + "integrity": "sha512-U8OMdkkEt3Vp3uYHU2pMc6mwId7axVAcSSmcqJcUmWNPqY2pfee5O655ybTI2kNPWAe58Zu6gLu4Oi4QT4BgWA==", "dependencies": { - "@babel/core": "^7.22.9", - "@babel/parser": "^7.16.8", - "@babel/plugin-syntax-import-assertions": "^7.20.0", - "@babel/traverse": "^7.16.8", - "@babel/types": "^7.16.8", - "@graphql-tools/utils": "^10.0.13", + "@graphql-typed-document-node/core": "^3.1.1", + "cross-inspect": "1.0.0", + "dset": "^3.1.2", "tslib": "^2.4.0" }, "engines": { @@ -3714,150 +3850,138 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/github-loader/node_modules/@whatwg-node/events": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.1.tgz", - "integrity": "sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@graphql-tools/github-loader/node_modules/@whatwg-node/fetch": { - "version": "0.9.17", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.9.17.tgz", - "integrity": "sha512-TDYP3CpCrxwxpiNY0UMNf096H5Ihf67BK1iKGegQl5u9SlpEDYrvnV71gWBGJm+Xm31qOy8ATgma9rm8Pe7/5Q==", + "node_modules/@graphql-inspector/cli": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@graphql-inspector/cli/-/cli-5.0.6.tgz", + "integrity": "sha512-yawOs8fY9AC6eBeeH1CNw1DyAQmBe9dNvAMjS+bG1k9haSn7erzfeWWkoPbqYcFty6FWLlHwVwA00nfpOdJhzQ==", "dependencies": { - "@whatwg-node/node-fetch": "^0.5.7", - "urlpattern-polyfill": "^10.0.0" + "@babel/core": "7.24.6", + "@graphql-inspector/audit-command": "5.0.6", + "@graphql-inspector/code-loader": "5.0.1", + "@graphql-inspector/commands": "5.0.4", + "@graphql-inspector/config": "4.0.2", + "@graphql-inspector/coverage-command": "6.1.0", + "@graphql-inspector/diff-command": "5.0.6", + "@graphql-inspector/docs-command": "5.0.4", + "@graphql-inspector/git-loader": "5.0.1", + "@graphql-inspector/github-loader": "5.0.1", + "@graphql-inspector/graphql-loader": "5.0.1", + "@graphql-inspector/introspect-command": "5.0.6", + "@graphql-inspector/json-loader": "5.0.1", + "@graphql-inspector/loaders": "4.0.5", + "@graphql-inspector/serve-command": "5.0.5", + "@graphql-inspector/similar-command": "5.0.6", + "@graphql-inspector/url-loader": "5.0.1", + "@graphql-inspector/validate-command": "5.0.6", + "tslib": "2.6.2", + "yargs": "17.7.2" }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@graphql-tools/github-loader/node_modules/@whatwg-node/node-fetch": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.5.10.tgz", - "integrity": "sha512-KIAHepie/T1PRkUfze4t+bPlyvpxlWiXTPtcGlbIZ0vWkBJMdRmCg4ZrJ2y4XaO1eTPo1HlWYUuj1WvoIpumqg==", - "dependencies": { - "@kamilkisiela/fast-url-parser": "^1.1.4", - "@whatwg-node/events": "^0.1.0", - "busboy": "^1.6.0", - "fast-querystring": "^1.1.1", - "tslib": "^2.3.1" + "bin": { + "graphql-inspector": "cjs/index.js" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/github-loader/node_modules/urlpattern-polyfill": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", - "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==" - }, - "node_modules/@graphql-tools/graphql-file-loader": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.0.1.tgz", - "integrity": "sha512-7gswMqWBabTSmqbaNyWSmRRpStWlcCkBc73E6NZNlh4YNuiyKOwbvSkOUYFOqFMfEL+cFsXgAvr87Vz4XrYSbA==", + "node_modules/@graphql-inspector/code-loader": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@graphql-inspector/code-loader/-/code-loader-5.0.1.tgz", + "integrity": "sha512-kdyP76g0QrtOFRda67+aNshSf0PXYyGJLiGxoVBogpAbkzDRhZQZAsdQVKP0tdEQAn4w0zN6VBdmpF/PAeBO5A==", "dependencies": { - "@graphql-tools/import": "7.0.1", - "@graphql-tools/utils": "^10.0.13", - "globby": "^11.0.3", - "tslib": "^2.4.0", - "unixify": "^1.0.0" + "@graphql-tools/code-file-loader": "8.1.2", + "tslib": "2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/graphql-tag-pluck": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-7.5.2.tgz", - "integrity": "sha512-RW+H8FqOOLQw0BPXaahYepVSRjuOHw+7IL8Opaa5G5uYGOBxoXR7DceyQ7BcpMgktAOOmpDNQ2WtcboChOJSRA==", - "dev": true, + "node_modules/@graphql-inspector/commands": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@graphql-inspector/commands/-/commands-5.0.4.tgz", + "integrity": "sha512-m6SzYxjkKhor7pV33r1FSL2Wq/epzeWDE1cfPT/eFJ4qKavTBcglr+Vpien6PK1a2vy69GhviFhMoJEakrlZMA==", "dependencies": { - "@babel/parser": "^7.16.8", - "@babel/plugin-syntax-import-assertions": "^7.20.0", - "@babel/traverse": "^7.16.8", - "@babel/types": "^7.16.8", - "@graphql-tools/utils": "^9.2.1", - "tslib": "^2.4.0" + "tslib": "2.6.2" }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/graphql-tag-pluck/node_modules/@graphql-tools/utils": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", - "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", - "dev": true, - "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "tslib": "^2.4.0" + "engines": { + "node": ">=18.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "@graphql-inspector/config": "^4.0.0", + "@graphql-inspector/loaders": "^4.0.0", + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", + "yargs": "17.7.2" } }, - "node_modules/@graphql-tools/import": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-7.0.1.tgz", - "integrity": "sha512-935uAjAS8UAeXThqHfYVr4HEAp6nHJ2sximZKO1RzUTq5WoALMAhhGARl0+ecm6X+cqNUwIChJbjtaa6P/ML0w==", + "node_modules/@graphql-inspector/config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@graphql-inspector/config/-/config-4.0.2.tgz", + "integrity": "sha512-fnIwVpGM5AtTr4XyV8NJkDnwpXxZSBzi3BopjuXwBPXXD1F3tcVkCKNT6/5WgUQGfNPskBVbitcOPtM4hIYAOQ==", "dependencies": { - "@graphql-tools/utils": "^10.0.13", - "resolve-from": "5.0.0", - "tslib": "^2.4.0" + "tslib": "2.6.2" }, "engines": { "node": ">=16.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/json-file-loader": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-8.0.1.tgz", - "integrity": "sha512-lAy2VqxDAHjVyqeJonCP6TUemrpYdDuKt25a10X6zY2Yn3iFYGnuIDQ64cv3ytyGY6KPyPB+Kp+ZfOkNDG3FQA==", + "node_modules/@graphql-inspector/core": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-inspector/core/-/core-6.1.0.tgz", + "integrity": "sha512-5/kqD5330duUsfMBfhMc0iVld76JwSKTkKi7aOr1x9MvSnP8p1anQo7BCNZ5VY9+EvWn4njHbkNfdS/lrqsi+A==", "dependencies": { - "@graphql-tools/utils": "^10.0.13", - "globby": "^11.0.3", - "tslib": "^2.4.0", - "unixify": "^1.0.0" + "dependency-graph": "1.0.0", + "object-inspect": "1.13.1", + "tslib": "2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/load": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-8.0.2.tgz", - "integrity": "sha512-S+E/cmyVmJ3CuCNfDuNF2EyovTwdWfQScXv/2gmvJOti2rGD8jTt9GYVzXaxhblLivQR9sBUCNZu/w7j7aXUCA==", + "node_modules/@graphql-inspector/core/node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@graphql-inspector/coverage-command": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-inspector/coverage-command/-/coverage-command-6.1.0.tgz", + "integrity": "sha512-c5/nERSACMkVkjlKF6ggUwI4nuKTyO991fqQiM9Dm36Heahu1BsH5BjtHWY6zlOg5b7e0v7X0+wqsLjNUsYGEA==", "dependencies": { - "@graphql-tools/schema": "^10.0.3", - "@graphql-tools/utils": "^10.0.13", - "p-limit": "3.1.0", - "tslib": "^2.4.0" + "@graphql-inspector/commands": "5.0.4", + "@graphql-inspector/core": "6.1.0", + "@graphql-inspector/logger": "5.0.1", + "@graphql-tools/utils": "10.2.1", + "tslib": "2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/merge": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.0.6.tgz", - "integrity": "sha512-TmkzFTFVieHnqu9mPTF6RxAQltaprpDQnM5HMTPSyMLXnJGMTvdWejV0yORKj7DW1YSi791/sUnKf8HytepBFQ==", + "node_modules/@graphql-inspector/coverage-command/node_modules/@graphql-tools/utils": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.2.1.tgz", + "integrity": "sha512-U8OMdkkEt3Vp3uYHU2pMc6mwId7axVAcSSmcqJcUmWNPqY2pfee5O655ybTI2kNPWAe58Zu6gLu4Oi4QT4BgWA==", "dependencies": { - "@graphql-tools/utils": "^10.5.4", + "@graphql-typed-document-node/core": "^3.1.1", + "cross-inspect": "1.0.0", + "dset": "^3.1.2", "tslib": "^2.4.0" }, "engines": { @@ -3867,221 +3991,141 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/optimize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-2.0.0.tgz", - "integrity": "sha512-nhdT+CRGDZ+bk68ic+Jw1OZ99YCDIKYA5AlVAnBHJvMawSx9YQqQAIj4refNc1/LRieGiuWvhbG3jvPVYho0Dg==", - "dev": true, + "node_modules/@graphql-inspector/diff-command": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@graphql-inspector/diff-command/-/diff-command-5.0.6.tgz", + "integrity": "sha512-DmFfOM5QHQphBvPyyRNmei1IY9DvoqySd/D3iV7/1iOBnFxNkljM8jqbUGe+jZfcrDdM9DRYJseb5d0atSXBRQ==", "dependencies": { - "tslib": "^2.4.0" + "@graphql-inspector/commands": "5.0.4", + "@graphql-inspector/core": "6.1.0", + "@graphql-inspector/logger": "5.0.1", + "tslib": "2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/prisma-loader": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@graphql-tools/prisma-loader/-/prisma-loader-8.0.3.tgz", - "integrity": "sha512-oZhxnMr3Jw2WAW1h9FIhF27xWzIB7bXWM8olz4W12oII4NiZl7VRkFw9IT50zME2Bqi9LGh9pkmMWkjvbOpl+Q==", - "dev": true, + "node_modules/@graphql-inspector/docs-command": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@graphql-inspector/docs-command/-/docs-command-5.0.4.tgz", + "integrity": "sha512-NTQRWYzGNJy4Bnd+0NHNjOdgaETEUG112W+Nei/tPCRTs0Vi/UiW+UkGsQ3KxJozEkwgN8od39bVWohGTOPcpA==", "dependencies": { - "@graphql-tools/url-loader": "^8.0.2", - "@graphql-tools/utils": "^10.0.13", - "@types/js-yaml": "^4.0.0", - "@types/json-stable-stringify": "^1.0.32", - "@whatwg-node/fetch": "^0.9.0", - "chalk": "^4.1.0", - "debug": "^4.3.1", - "dotenv": "^16.0.0", - "graphql-request": "^6.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "jose": "^5.0.0", - "js-yaml": "^4.0.0", - "json-stable-stringify": "^1.0.1", - "lodash": "^4.17.20", - "scuid": "^1.1.0", - "tslib": "^2.4.0", - "yaml-ast-parser": "^0.0.43" + "@graphql-inspector/commands": "5.0.4", + "open": "8.4.2", + "tslib": "2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/prisma-loader/node_modules/@whatwg-node/events": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.1.tgz", - "integrity": "sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==", - "dev": true, - "engines": { - "node": ">=16.0.0" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/prisma-loader/node_modules/@whatwg-node/fetch": { - "version": "0.9.17", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.9.17.tgz", - "integrity": "sha512-TDYP3CpCrxwxpiNY0UMNf096H5Ihf67BK1iKGegQl5u9SlpEDYrvnV71gWBGJm+Xm31qOy8ATgma9rm8Pe7/5Q==", - "dev": true, + "node_modules/@graphql-inspector/git-loader": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@graphql-inspector/git-loader/-/git-loader-5.0.1.tgz", + "integrity": "sha512-eZFNU/y1z4sZ9Axu8mB/J7mW+e78JnWgXG2vcT1TT2E1uzFm0x2oNONM2lgLCZGEJuwQDEnreok5CoHumIdE4Q==", "dependencies": { - "@whatwg-node/node-fetch": "^0.5.7", - "urlpattern-polyfill": "^10.0.0" + "@graphql-tools/git-loader": "8.0.6", + "tslib": "2.6.2" }, "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@graphql-tools/prisma-loader/node_modules/@whatwg-node/node-fetch": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.5.10.tgz", - "integrity": "sha512-KIAHepie/T1PRkUfze4t+bPlyvpxlWiXTPtcGlbIZ0vWkBJMdRmCg4ZrJ2y4XaO1eTPo1HlWYUuj1WvoIpumqg==", - "dev": true, - "dependencies": { - "@kamilkisiela/fast-url-parser": "^1.1.4", - "@whatwg-node/events": "^0.1.0", - "busboy": "^1.6.0", - "fast-querystring": "^1.1.1", - "tslib": "^2.3.1" + "node": ">=18.0.0" }, - "engines": { - "node": ">=16.0.0" + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/prisma-loader/node_modules/urlpattern-polyfill": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", - "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", - "dev": true - }, - "node_modules/@graphql-tools/relay-operation-optimizer": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.1.tgz", - "integrity": "sha512-y0ZrQ/iyqWZlsS/xrJfSir3TbVYJTYmMOu4TaSz6F4FRDTQ3ie43BlKkhf04rC28pnUOS4BO9pDcAo1D30l5+A==", - "dev": true, + "node_modules/@graphql-inspector/github-loader": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@graphql-inspector/github-loader/-/github-loader-5.0.1.tgz", + "integrity": "sha512-CDsY4V1pEDzr5z5FlYTxcPa/7pKsuT/6xQmo1JghHQuYQPZ5TjtGsyNZwgQOjISMCw7pknXfifPBrFQKt6IOEA==", "dependencies": { - "@ardatan/relay-compiler": "12.0.0", - "@graphql-tools/utils": "^10.0.13", - "tslib": "^2.4.0" + "@graphql-tools/github-loader": "8.0.1", + "tslib": "2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/resolvers-composition": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@graphql-tools/resolvers-composition/-/resolvers-composition-7.0.2.tgz", - "integrity": "sha512-ipTV1xjJSSx8iI0AQ5beZfUSlV08R8B+FGsbBnGCKiZ1Li4Y+egxR7wOcI9F3TlQlMExdSq3CjtoVfPpeYWsPw==", + "node_modules/@graphql-inspector/graphql-loader": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@graphql-inspector/graphql-loader/-/graphql-loader-5.0.1.tgz", + "integrity": "sha512-VZIcbkMhgak3sW4GehVIX/Qnwu1TmQidvaWs8YUiT+czPxKK1rqY/c/G3arwQDtqAdPMx8IwY1bT83ykfIyxfg==", "dependencies": { - "@graphql-tools/utils": "^10.5.5", - "lodash": "4.17.21", - "micromatch": "^4.0.8", - "tslib": "^2.4.0" + "@graphql-tools/graphql-file-loader": "8.0.1", + "tslib": "2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/schema": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.6.tgz", - "integrity": "sha512-EIJgPRGzpvDFEjVp+RF1zNNYIC36BYuIeZ514jFoJnI6IdxyVyIRDLx/ykgMdaa1pKQerpfdqDnsF4JnZoDHSQ==", + "node_modules/@graphql-inspector/introspect-command": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@graphql-inspector/introspect-command/-/introspect-command-5.0.6.tgz", + "integrity": "sha512-qtYZLObzezNR5aS1qFeECwcK+MQGWDDywI3UzSKKtTLfoQJTAv3ECoo9PZxGFC2aEmZAKTEfkFICkbE9OCHu/w==", "dependencies": { - "@graphql-tools/merge": "^9.0.6", - "@graphql-tools/utils": "^10.5.4", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.12" + "@graphql-inspector/commands": "5.0.4", + "@graphql-inspector/core": "6.1.0", + "@graphql-inspector/logger": "5.0.1", + "tslib": "2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/url-loader": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-8.0.2.tgz", - "integrity": "sha512-1dKp2K8UuFn7DFo1qX5c1cyazQv2h2ICwA9esHblEqCYrgf69Nk8N7SODmsfWg94OEaI74IqMoM12t7eIGwFzQ==", + "node_modules/@graphql-inspector/json-loader": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@graphql-inspector/json-loader/-/json-loader-5.0.1.tgz", + "integrity": "sha512-ql5zI2E/RNgLKDJ2HilTds2lUTv8ZXQfY5HG29iia85q/CIFslVTDbhzhbXRqmz4jsLd3KCi1LxpAeYQQMhCSQ==", "dependencies": { - "@ardatan/sync-fetch": "^0.0.1", - "@graphql-tools/delegate": "^10.0.4", - "@graphql-tools/executor-graphql-ws": "^1.1.2", - "@graphql-tools/executor-http": "^1.0.9", - "@graphql-tools/executor-legacy-ws": "^1.0.6", - "@graphql-tools/utils": "^10.0.13", - "@graphql-tools/wrap": "^10.0.2", - "@types/ws": "^8.0.0", - "@whatwg-node/fetch": "^0.9.0", - "isomorphic-ws": "^5.0.0", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.11", - "ws": "^8.12.0" + "@graphql-tools/json-file-loader": "8.0.1", + "tslib": "2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/url-loader/node_modules/@whatwg-node/events": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.1.tgz", - "integrity": "sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==", - "engines": { - "node": ">=16.0.0" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/url-loader/node_modules/@whatwg-node/fetch": { - "version": "0.9.17", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.9.17.tgz", - "integrity": "sha512-TDYP3CpCrxwxpiNY0UMNf096H5Ihf67BK1iKGegQl5u9SlpEDYrvnV71gWBGJm+Xm31qOy8ATgma9rm8Pe7/5Q==", + "node_modules/@graphql-inspector/loaders": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@graphql-inspector/loaders/-/loaders-4.0.5.tgz", + "integrity": "sha512-MQj82Pbo4YVgS1E3IjVvP3ByLQKQ6HHrjK+S21szXx46cKPxlc+MeKHpjfERSCmbdKAinP0MMHxVrmk7hyktow==", "dependencies": { - "@whatwg-node/node-fetch": "^0.5.7", - "urlpattern-polyfill": "^10.0.0" + "@graphql-tools/code-file-loader": "8.1.2", + "@graphql-tools/load": "8.0.2", + "@graphql-tools/utils": "10.2.1", + "tslib": "2.6.2" }, "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@graphql-tools/url-loader/node_modules/@whatwg-node/node-fetch": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.5.10.tgz", - "integrity": "sha512-KIAHepie/T1PRkUfze4t+bPlyvpxlWiXTPtcGlbIZ0vWkBJMdRmCg4ZrJ2y4XaO1eTPo1HlWYUuj1WvoIpumqg==", - "dependencies": { - "@kamilkisiela/fast-url-parser": "^1.1.4", - "@whatwg-node/events": "^0.1.0", - "busboy": "^1.6.0", - "fast-querystring": "^1.1.1", - "tslib": "^2.3.1" + "node": ">=18.0.0" }, - "engines": { - "node": ">=16.0.0" + "peerDependencies": { + "@graphql-inspector/config": "^4.0.0", + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/url-loader/node_modules/urlpattern-polyfill": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", - "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==" - }, - "node_modules/@graphql-tools/utils": { - "version": "10.5.5", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.5.5.tgz", - "integrity": "sha512-LF/UDWmMT0mnobL2UZETwYghV7HYBzNaGj0SAkCYOMy/C3+6sQdbcTksnoFaKR9XIVD78jNXEGfivbB8Zd+cwA==", + "node_modules/@graphql-inspector/loaders/node_modules/@graphql-tools/utils": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.2.1.tgz", + "integrity": "sha512-U8OMdkkEt3Vp3uYHU2pMc6mwId7axVAcSSmcqJcUmWNPqY2pfee5O655ybTI2kNPWAe58Zu6gLu4Oi4QT4BgWA==", "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", - "cross-inspect": "1.0.1", + "cross-inspect": "1.0.0", "dset": "^3.1.2", "tslib": "^2.4.0" }, @@ -4092,1341 +4136,2817 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/utils/node_modules/cross-inspect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.1.tgz", - "integrity": "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==", + "node_modules/@graphql-inspector/logger": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@graphql-inspector/logger/-/logger-5.0.1.tgz", + "integrity": "sha512-rEo+HoQt+qjdayy7p5vcR9GeGTdKXmN0LbIm3W+jKKoXeAMlV4zHxnOW6jEhO6E0eVQxf8Sc1TlcH78i2P2a9w==", "dependencies": { - "tslib": "^2.4.0" + "chalk": "4.1.2", + "figures": "3.2.0", + "log-symbols": "4.1.0", + "std-env": "3.7.0", + "tslib": "2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@graphql-tools/wrap": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.0.5.tgz", - "integrity": "sha512-Cbr5aYjr3HkwdPvetZp1cpDWTGdD1Owgsb3z/ClzhmrboiK86EnQDxDvOJiQkDCPWE9lNBwj8Y4HfxroY0D9DQ==", + "node_modules/@graphql-inspector/serve-command": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@graphql-inspector/serve-command/-/serve-command-5.0.5.tgz", + "integrity": "sha512-dWAg51LHrXoZb5NqE/G/nTaeJ2FrQJZc+mCz6l3fTBL6pU6szyMx4+Cxq+JmGCJVD71N4Fh+h9B4psDpnOFtBQ==", "dependencies": { - "@graphql-tools/delegate": "^10.0.4", - "@graphql-tools/schema": "^10.0.3", - "@graphql-tools/utils": "^10.1.1", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.12" + "@graphql-inspector/commands": "5.0.4", + "@graphql-inspector/logger": "5.0.1", + "graphql-yoga": "5.3.1", + "open": "8.4.2", + "tslib": "2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-typed-document-node/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", - "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-yoga/logger": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@graphql-yoga/logger/-/logger-2.0.0.tgz", - "integrity": "sha512-Mg8psdkAp+YTG1OGmvU+xa6xpsAmSir0hhr3yFYPyLNwzUj95DdIwsMpKadDj9xDpYgJcH3Hp/4JMal9DhQimA==", + "node_modules/@graphql-inspector/similar-command": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@graphql-inspector/similar-command/-/similar-command-5.0.6.tgz", + "integrity": "sha512-40SaZtxIXEZ0V/EkiG6N2in+PSeVoVcwmtl1ETbysGXZ6xC9Fu+Qi0lE7lbKWKjzw5nv9hYpznDJ1oIsBuN+hQ==", "dependencies": { - "tslib": "^2.5.2" + "@graphql-inspector/commands": "5.0.4", + "@graphql-inspector/core": "6.1.0", + "@graphql-inspector/logger": "5.0.1", + "tslib": "2.6.2" }, "engines": { "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-yoga/subscription": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@graphql-yoga/subscription/-/subscription-5.0.0.tgz", - "integrity": "sha512-Ri7sK8hmxd/kwaEa0YT8uqQUb2wOLsmBMxI90QDyf96lzOMJRgBuNYoEkU1pSgsgmW2glceZ96sRYfaXqwVxUw==", + "node_modules/@graphql-inspector/url-loader": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@graphql-inspector/url-loader/-/url-loader-5.0.1.tgz", + "integrity": "sha512-7OPJfTJgqptJyfsrpntsn3GEMpSZWxkJO+KaMIZfqDsiWN/zyvNqB0Amogi3d7xxtU1fnB3NCN5VWCFuiRSPXg==", "dependencies": { - "@graphql-yoga/typed-event-target": "^3.0.0", - "@repeaterjs/repeater": "^3.0.4", - "@whatwg-node/events": "^0.1.0", - "tslib": "^2.5.2" + "@graphql-tools/url-loader": "8.0.2", + "tslib": "2.6.2" }, "engines": { "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-yoga/subscription/node_modules/@whatwg-node/events": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.1.tgz", - "integrity": "sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@graphql-yoga/typed-event-target": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@graphql-yoga/typed-event-target/-/typed-event-target-3.0.0.tgz", - "integrity": "sha512-w+liuBySifrstuHbFrHoHAEyVnDFVib+073q8AeAJ/qqJfvFvAwUPLLtNohR/WDVRgSasfXtl3dcNuVJWN+rjg==", + "node_modules/@graphql-inspector/validate-command": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@graphql-inspector/validate-command/-/validate-command-5.0.6.tgz", + "integrity": "sha512-SO4esOmsdUEueGA2kMjsoXSrvQrtZnJF7wKhcUwJrxIBm8aHf1V5wtorAk4ajIzhlD6a5Yd35GHI9hbjXnIZuQ==", "dependencies": { - "@repeaterjs/repeater": "^3.0.4", - "tslib": "^2.5.2" + "@graphql-inspector/commands": "5.0.4", + "@graphql-inspector/core": "6.1.0", + "@graphql-inspector/logger": "5.0.1", + "@graphql-tools/utils": "10.2.1", + "tslib": "2.6.2" }, "engines": { "node": ">=18.0.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "engines": { - "node": ">=12.22" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "license": "Apache-2.0", + "node_modules/@graphql-inspector/validate-command/node_modules/@graphql-tools/utils": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.2.1.tgz", + "integrity": "sha512-U8OMdkkEt3Vp3uYHU2pMc6mwId7axVAcSSmcqJcUmWNPqY2pfee5O655ybTI2kNPWAe58Zu6gLu4Oi4QT4BgWA==", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "cross-inspect": "1.0.0", + "dset": "^3.1.2", + "tslib": "^2.4.0" + }, "engines": { - "node": ">=18.18" + "node": ">=16.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@graphql-tools/apollo-engine-loader": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-8.0.1.tgz", + "integrity": "sha512-NaPeVjtrfbPXcl+MLQCJLWtqe2/E4bbAqcauEOQ+3sizw1Fc2CNmhHRF8a6W4D0ekvTRRXAMptXYgA2uConbrA==", "dev": true, "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@ardatan/sync-fetch": "^0.0.1", + "@graphql-tools/utils": "^10.0.13", + "@whatwg-node/fetch": "^0.9.0", + "tslib": "^2.4.0" }, "engines": { - "node": ">=12" + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/@graphql-tools/apollo-engine-loader/node_modules/@whatwg-node/events": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.1.tgz", + "integrity": "sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=16.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/@graphql-tools/apollo-engine-loader/node_modules/@whatwg-node/fetch": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.9.17.tgz", + "integrity": "sha512-TDYP3CpCrxwxpiNY0UMNf096H5Ihf67BK1iKGegQl5u9SlpEDYrvnV71gWBGJm+Xm31qOy8ATgma9rm8Pe7/5Q==", "dev": true, - "engines": { - "node": ">=12" + "dependencies": { + "@whatwg-node/node-fetch": "^0.5.7", + "urlpattern-polyfill": "^10.0.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@graphql-tools/apollo-engine-loader/node_modules/@whatwg-node/node-fetch": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.5.10.tgz", + "integrity": "sha512-KIAHepie/T1PRkUfze4t+bPlyvpxlWiXTPtcGlbIZ0vWkBJMdRmCg4ZrJ2y4XaO1eTPo1HlWYUuj1WvoIpumqg==", "dev": true, "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "@kamilkisiela/fast-url-parser": "^1.1.4", + "@whatwg-node/events": "^0.1.0", + "busboy": "^1.6.0", + "fast-querystring": "^1.1.1", + "tslib": "^2.3.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=16.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, + "node_modules/@graphql-tools/apollo-engine-loader/node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "dev": true + }, + "node_modules/@graphql-tools/batch-execute": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.4.tgz", + "integrity": "sha512-kkebDLXgDrep5Y0gK1RN3DMUlLqNhg60OAz0lTCqrYeja6DshxLtLkj+zV4mVbBA4mQOEoBmw6g1LZs3dA84/w==", "dependencies": { - "ansi-regex": "^6.0.1" + "@graphql-tools/utils": "^10.0.13", + "dataloader": "^2.2.2", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" }, "engines": { - "node": ">=12" + "node": ">=16.0.0" }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, + "node_modules/@graphql-tools/code-file-loader": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-8.1.2.tgz", + "integrity": "sha512-GrLzwl1QV2PT4X4TEEfuTmZYzIZHLqoTGBjczdUzSqgCCcqwWzLB3qrJxFQfI8e5s1qZ1bhpsO9NoMn7tvpmyA==", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "@graphql-tools/graphql-tag-pluck": "8.3.1", + "@graphql-tools/utils": "^10.0.13", + "globby": "^11.0.3", + "tslib": "^2.4.0", + "unixify": "^1.0.0" }, "engines": { - "node": ">=12" + "node": ">=16.0.0" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, + "node_modules/@graphql-tools/code-file-loader/node_modules/@graphql-tools/graphql-tag-pluck": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.1.tgz", + "integrity": "sha512-ujits9tMqtWQQq4FI4+qnVPpJvSEn7ogKtyN/gfNT+ErIn6z1e4gyVGQpTK5sgAUXq1lW4gU/5fkFFC5/sL2rQ==", + "dependencies": { + "@babel/core": "^7.22.9", + "@babel/parser": "^7.16.8", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/traverse": "^7.16.8", + "@babel/types": "^7.16.8", + "@graphql-tools/utils": "^10.0.13", + "tslib": "^2.4.0" + }, "engines": { - "node": ">=8" + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "node_modules/@graphql-tools/delegate": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.0.4.tgz", + "integrity": "sha512-WswZRbQZMh/ebhc8zSomK9DIh6Pd5KbuiMsyiKkKz37TWTrlCOe+4C/fyrBFez30ksq6oFyCeSKMwfrCbeGo0Q==", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "@graphql-tools/batch-execute": "^9.0.4", + "@graphql-tools/executor": "^1.2.1", + "@graphql-tools/schema": "^10.0.3", + "@graphql-tools/utils": "^10.0.13", + "dataloader": "^2.2.2", + "tslib": "^2.5.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@graphql-tools/documents": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/documents/-/documents-1.0.0.tgz", + "integrity": "sha512-rHGjX1vg/nZ2DKqRGfDPNC55CWZBMldEVcH+91BThRa6JeT80NqXknffLLEZLRUxyikCfkwMsk6xR3UNMqG0Rg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tslib": "^2.4.0" + }, "engines": { - "node": ">=6.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "node_modules/@graphql-tools/executor": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.2.6.tgz", + "integrity": "sha512-+1kjfqzM5T2R+dCw7F4vdJ3CqG+fY/LYJyhNiWEFtq0ToLwYzR/KKyD8YuzTirEjSxWTVlcBh7endkx5n5F6ew==", + "dependencies": { + "@graphql-tools/utils": "^10.1.1", + "@graphql-typed-document-node/core": "3.2.0", + "@repeaterjs/repeater": "^3.0.4", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" + }, "engines": { - "node": ">=6.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "node_modules/@graphql-tools/executor-graphql-ws": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-1.1.2.tgz", + "integrity": "sha512-+9ZK0rychTH1LUv4iZqJ4ESbmULJMTsv3XlFooPUngpxZkk00q6LqHKJRrsLErmQrVaC7cwQCaRBJa0teK17Lg==", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@graphql-tools/utils": "^10.0.13", + "@types/ws": "^8.0.0", + "graphql-ws": "^5.14.0", + "isomorphic-ws": "^5.0.0", + "tslib": "^2.4.0", + "ws": "^8.13.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@kamilkisiela/fast-url-parser": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@kamilkisiela/fast-url-parser/-/fast-url-parser-1.1.4.tgz", - "integrity": "sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew==" - }, - "node_modules/@messageformat/core": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@messageformat/core/-/core-3.3.0.tgz", - "integrity": "sha512-YcXd3remTDdeMxAlbvW6oV9d/01/DZ8DHUFwSttO3LMzIZj3iO0NRw+u1xlsNNORFI+u0EQzD52ZX3+Udi0T3g==", + "node_modules/@graphql-tools/executor-http": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-1.0.9.tgz", + "integrity": "sha512-+NXaZd2MWbbrWHqU4EhXcrDbogeiCDmEbrAN+rMn4Nu2okDjn2MTFDbTIab87oEubQCH4Te1wDkWPKrzXup7+Q==", "dependencies": { - "@messageformat/date-skeleton": "^1.0.0", - "@messageformat/number-skeleton": "^1.0.0", - "@messageformat/parser": "^5.1.0", - "@messageformat/runtime": "^3.0.1", - "make-plural": "^7.0.0", - "safe-identifier": "^0.4.1" + "@graphql-tools/utils": "^10.0.13", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/fetch": "^0.9.0", + "extract-files": "^11.0.0", + "meros": "^1.2.1", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@messageformat/date-skeleton": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@messageformat/date-skeleton/-/date-skeleton-1.0.1.tgz", - "integrity": "sha512-jPXy8fg+WMPIgmGjxSlnGJn68h/2InfT0TNSkVx0IGXgp4ynnvYkbZ51dGWmGySEK+pBiYUttbQdu5XEqX5CRg==" - }, - "node_modules/@messageformat/number-skeleton": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz", - "integrity": "sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg==" + "node_modules/@graphql-tools/executor-http/node_modules/@whatwg-node/events": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.1.tgz", + "integrity": "sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==", + "engines": { + "node": ">=16.0.0" + } }, - "node_modules/@messageformat/parser": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@messageformat/parser/-/parser-5.1.0.tgz", - "integrity": "sha512-jKlkls3Gewgw6qMjKZ9SFfHUpdzEVdovKFtW1qRhJ3WI4FW5R/NnGDqr8SDGz+krWDO3ki94boMmQvGke1HwUQ==", + "node_modules/@graphql-tools/executor-http/node_modules/@whatwg-node/fetch": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.9.17.tgz", + "integrity": "sha512-TDYP3CpCrxwxpiNY0UMNf096H5Ihf67BK1iKGegQl5u9SlpEDYrvnV71gWBGJm+Xm31qOy8ATgma9rm8Pe7/5Q==", "dependencies": { - "moo": "^0.5.1" + "@whatwg-node/node-fetch": "^0.5.7", + "urlpattern-polyfill": "^10.0.0" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@messageformat/runtime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@messageformat/runtime/-/runtime-3.0.1.tgz", - "integrity": "sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg==", + "node_modules/@graphql-tools/executor-http/node_modules/@whatwg-node/node-fetch": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.5.10.tgz", + "integrity": "sha512-KIAHepie/T1PRkUfze4t+bPlyvpxlWiXTPtcGlbIZ0vWkBJMdRmCg4ZrJ2y4XaO1eTPo1HlWYUuj1WvoIpumqg==", "dependencies": { - "make-plural": "^7.0.0" + "@kamilkisiela/fast-url-parser": "^1.1.4", + "@whatwg-node/events": "^0.1.0", + "busboy": "^1.6.0", + "fast-querystring": "^1.1.1", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@microsoft/tsdoc": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", - "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==", - "dev": true + "node_modules/@graphql-tools/executor-http/node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==" }, - "node_modules/@microsoft/tsdoc-config": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.0.tgz", - "integrity": "sha512-v/EYRXnCAIHxOHW+Plb6OWuUoMotxTN0GLatnpOb1xq0KuTNw/WI3pamJx/UbsoJP5k9MCw1QxvvhPcF9pH3Zg==", - "dev": true, + "node_modules/@graphql-tools/executor-legacy-ws": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.0.6.tgz", + "integrity": "sha512-lDSxz9VyyquOrvSuCCnld3256Hmd+QI2lkmkEv7d4mdzkxkK4ddAWW1geQiWrQvWmdsmcnGGlZ7gDGbhEExwqg==", "dependencies": { - "@microsoft/tsdoc": "0.15.0", - "ajv": "~8.12.0", - "jju": "~1.4.0", - "resolve": "~1.22.2" + "@graphql-tools/utils": "^10.0.13", + "@types/ws": "^8.0.0", + "isomorphic-ws": "^5.0.0", + "tslib": "^2.4.0", + "ws": "^8.15.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@microsoft/tsdoc-config/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, + "node_modules/@graphql-tools/git-loader": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-8.0.6.tgz", + "integrity": "sha512-FQFO4H5wHAmHVyuUQrjvPE8re3qJXt50TWHuzrK3dEaief7JosmlnkLMDMbMBwtwITz9u1Wpl6doPhT2GwKtlw==", "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "@graphql-tools/graphql-tag-pluck": "8.3.1", + "@graphql-tools/utils": "^10.0.13", + "is-glob": "4.0.3", + "micromatch": "^4.0.4", + "tslib": "^2.4.0", + "unixify": "^1.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.5.tgz", - "integrity": "sha512-XLNOMH66KhJzUJNwT/qlMnS4WsNDWD5ASdyaSH3EtK+F4r/CFGa3jT4GNi4mfOitGvWXtdLgQJkQjxSVrio+jA==", - "dependencies": { - "sparse-bitfield": "^3.0.3" - } - }, - "node_modules/@mui/base": { - "version": "5.0.0-beta.40", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", - "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", + "node_modules/@graphql-tools/git-loader/node_modules/@graphql-tools/graphql-tag-pluck": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.1.tgz", + "integrity": "sha512-ujits9tMqtWQQq4FI4+qnVPpJvSEn7ogKtyN/gfNT+ErIn6z1e4gyVGQpTK5sgAUXq1lW4gU/5fkFFC5/sL2rQ==", "dependencies": { - "@babel/runtime": "^7.23.9", - "@floating-ui/react-dom": "^2.0.8", - "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", - "@popperjs/core": "^2.11.8", - "clsx": "^2.1.0", - "prop-types": "^15.8.1" + "@babel/core": "^7.22.9", + "@babel/parser": "^7.16.8", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/traverse": "^7.16.8", + "@babel/types": "^7.16.8", + "@graphql-tools/utils": "^10.0.13", + "tslib": "^2.4.0" }, "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" + "node": ">=16.0.0" }, "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/core-downloads-tracker": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz", - "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@mui/icons-material": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", - "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", + "node_modules/@graphql-tools/github-loader": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-8.0.1.tgz", + "integrity": "sha512-W4dFLQJ5GtKGltvh/u1apWRFKBQOsDzFxO9cJkOYZj1VzHCpRF43uLST4VbCfWve+AwBqOuKr7YgkHoxpRMkcg==", "dependencies": { - "@babel/runtime": "^7.23.9" + "@ardatan/sync-fetch": "^0.0.1", + "@graphql-tools/executor-http": "^1.0.9", + "@graphql-tools/graphql-tag-pluck": "^8.0.0", + "@graphql-tools/utils": "^10.0.13", + "@whatwg-node/fetch": "^0.9.0", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" }, "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" + "node": ">=16.0.0" }, "peerDependencies": { - "@mui/material": "^5.0.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@mui/lab": { - "version": "5.0.0-alpha.169", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.169.tgz", - "integrity": "sha512-h6xe1K6ISKUbyxTDgdvql4qoDP6+q8ad5fg9nXQxGLUrIeT2jVrBuT/jRECSTufbnhzP+V5kulvYxaMfM8rEdA==", + "node_modules/@graphql-tools/github-loader/node_modules/@graphql-tools/graphql-tag-pluck": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.0.tgz", + "integrity": "sha512-gNqukC+s7iHC7vQZmx1SEJQmLnOguBq+aqE2zV2+o1hxkExvKqyFli1SY/9gmukFIKpKutCIj+8yLOM+jARutw==", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/base": "5.0.0-beta.40", - "@mui/system": "^5.15.14", - "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", - "clsx": "^2.1.0", - "prop-types": "^15.8.1" + "@babel/core": "^7.22.9", + "@babel/parser": "^7.16.8", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/traverse": "^7.16.8", + "@babel/types": "^7.16.8", + "@graphql-tools/utils": "^10.0.13", + "tslib": "^2.4.0" }, "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" + "node": ">=16.0.0" }, "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material": ">=5.15.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@mui/material": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", - "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", + "node_modules/@graphql-tools/github-loader/node_modules/@whatwg-node/events": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.1.tgz", + "integrity": "sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@graphql-tools/github-loader/node_modules/@whatwg-node/fetch": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.9.17.tgz", + "integrity": "sha512-TDYP3CpCrxwxpiNY0UMNf096H5Ihf67BK1iKGegQl5u9SlpEDYrvnV71gWBGJm+Xm31qOy8ATgma9rm8Pe7/5Q==", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/core-downloads-tracker": "^5.16.7", - "@mui/system": "^5.16.7", - "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.6", - "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.10", - "clsx": "^2.1.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1", - "react-is": "^18.3.1", - "react-transition-group": "^4.4.5" + "@whatwg-node/node-fetch": "^0.5.7", + "urlpattern-polyfill": "^10.0.0" }, "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } + "node": ">=16.0.0" } }, - "node_modules/@mui/private-theming": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz", - "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==", + "node_modules/@graphql-tools/github-loader/node_modules/@whatwg-node/node-fetch": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.5.10.tgz", + "integrity": "sha512-KIAHepie/T1PRkUfze4t+bPlyvpxlWiXTPtcGlbIZ0vWkBJMdRmCg4ZrJ2y4XaO1eTPo1HlWYUuj1WvoIpumqg==", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.16.6", - "prop-types": "^15.8.1" + "@kamilkisiela/fast-url-parser": "^1.1.4", + "@whatwg-node/events": "^0.1.0", + "busboy": "^1.6.0", + "fast-querystring": "^1.1.1", + "tslib": "^2.3.1" }, "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "node": ">=16.0.0" } }, - "node_modules/@mui/styled-engine": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz", - "integrity": "sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==", + "node_modules/@graphql-tools/github-loader/node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==" + }, + "node_modules/@graphql-tools/graphql-file-loader": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.0.1.tgz", + "integrity": "sha512-7gswMqWBabTSmqbaNyWSmRRpStWlcCkBc73E6NZNlh4YNuiyKOwbvSkOUYFOqFMfEL+cFsXgAvr87Vz4XrYSbA==", "dependencies": { - "@babel/runtime": "^7.23.9", - "@emotion/cache": "^11.11.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" + "@graphql-tools/import": "7.0.1", + "@graphql-tools/utils": "^10.0.13", + "globby": "^11.0.3", + "tslib": "^2.4.0", + "unixify": "^1.0.0" }, "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" + "node": ">=16.0.0" }, "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@mui/system": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", - "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", + "node_modules/@graphql-tools/graphql-tag-pluck": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-7.5.2.tgz", + "integrity": "sha512-RW+H8FqOOLQw0BPXaahYepVSRjuOHw+7IL8Opaa5G5uYGOBxoXR7DceyQ7BcpMgktAOOmpDNQ2WtcboChOJSRA==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.16.6", - "@mui/styled-engine": "^5.16.6", - "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.6", - "clsx": "^2.1.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" + "@babel/parser": "^7.16.8", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/traverse": "^7.16.8", + "@babel/types": "^7.16.8", + "@graphql-tools/utils": "^9.2.1", + "tslib": "^2.4.0" }, "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@mui/types": { - "version": "7.2.18", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.18.tgz", - "integrity": "sha512-uvK9dWeyCJl/3ocVnTOS6nlji/Knj8/tVqVX03UVTpdmTJYu/s4jtDd9Kvv0nRGE0CUSNW1UYAci7PYypjealg==", - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + "node_modules/@graphql-tools/graphql-tag-pluck/node_modules/@graphql-tools/utils": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", + "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", + "dev": true, + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "tslib": "^2.4.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@mui/utils": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", - "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", + "node_modules/@graphql-tools/import": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-7.0.1.tgz", + "integrity": "sha512-935uAjAS8UAeXThqHfYVr4HEAp6nHJ2sximZKO1RzUTq5WoALMAhhGARl0+ecm6X+cqNUwIChJbjtaa6P/ML0w==", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/types": "^7.2.15", - "@types/prop-types": "^15.7.12", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^18.3.1" + "@graphql-tools/utils": "^10.0.13", + "resolve-from": "5.0.0", + "tslib": "^2.4.0" }, "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" + "node": ">=16.0.0" }, "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@graphql-tools/json-file-loader": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-8.0.1.tgz", + "integrity": "sha512-lAy2VqxDAHjVyqeJonCP6TUemrpYdDuKt25a10X6zY2Yn3iFYGnuIDQ64cv3ytyGY6KPyPB+Kp+ZfOkNDG3FQA==", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@graphql-tools/utils": "^10.0.13", + "globby": "^11.0.3", + "tslib": "^2.4.0", + "unixify": "^1.0.0" }, "engines": { - "node": ">= 8" + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@graphql-tools/load": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-8.0.2.tgz", + "integrity": "sha512-S+E/cmyVmJ3CuCNfDuNF2EyovTwdWfQScXv/2gmvJOti2rGD8jTt9GYVzXaxhblLivQR9sBUCNZu/w7j7aXUCA==", + "dependencies": { + "@graphql-tools/schema": "^10.0.3", + "@graphql-tools/utils": "^10.0.13", + "p-limit": "3.1.0", + "tslib": "^2.4.0" + }, "engines": { - "node": ">= 8" + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@graphql-tools/merge": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.0.6.tgz", + "integrity": "sha512-TmkzFTFVieHnqu9mPTF6RxAQltaprpDQnM5HMTPSyMLXnJGMTvdWejV0yORKj7DW1YSi791/sUnKf8HytepBFQ==", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@graphql-tools/utils": "^10.5.4", + "tslib": "^2.4.0" }, "engines": { - "node": ">= 8" + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@parcel/watcher": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", - "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "node_modules/@graphql-tools/optimize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-2.0.0.tgz", + "integrity": "sha512-nhdT+CRGDZ+bk68ic+Jw1OZ99YCDIKYA5AlVAnBHJvMawSx9YQqQAIj4refNc1/LRieGiuWvhbG3jvPVYho0Dg==", "dev": true, "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "tslib": "^2.4.0" }, "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=16.0.0" }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.4.1", - "@parcel/watcher-darwin-arm64": "2.4.1", - "@parcel/watcher-darwin-x64": "2.4.1", - "@parcel/watcher-freebsd-x64": "2.4.1", - "@parcel/watcher-linux-arm-glibc": "2.4.1", - "@parcel/watcher-linux-arm64-glibc": "2.4.1", - "@parcel/watcher-linux-arm64-musl": "2.4.1", - "@parcel/watcher-linux-x64-glibc": "2.4.1", - "@parcel/watcher-linux-x64-musl": "2.4.1", - "@parcel/watcher-win32-arm64": "2.4.1", - "@parcel/watcher-win32-ia32": "2.4.1", - "@parcel/watcher-win32-x64": "2.4.1" + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", - "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", - "cpu": [ - "x64" - ], + "node_modules/@graphql-tools/prisma-loader": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@graphql-tools/prisma-loader/-/prisma-loader-8.0.3.tgz", + "integrity": "sha512-oZhxnMr3Jw2WAW1h9FIhF27xWzIB7bXWM8olz4W12oII4NiZl7VRkFw9IT50zME2Bqi9LGh9pkmMWkjvbOpl+Q==", "dev": true, - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@graphql-tools/url-loader": "^8.0.2", + "@graphql-tools/utils": "^10.0.13", + "@types/js-yaml": "^4.0.0", + "@types/json-stable-stringify": "^1.0.32", + "@whatwg-node/fetch": "^0.9.0", + "chalk": "^4.1.0", + "debug": "^4.3.1", + "dotenv": "^16.0.0", + "graphql-request": "^6.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "jose": "^5.0.0", + "js-yaml": "^4.0.0", + "json-stable-stringify": "^1.0.1", + "lodash": "^4.17.20", + "scuid": "^1.1.0", + "tslib": "^2.4.0", + "yaml-ast-parser": "^0.0.43" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=16.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz", - "integrity": "sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==", + "node_modules/@graphql-tools/prisma-loader/node_modules/@whatwg-node/events": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.1.tgz", + "integrity": "sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==", "dev": true, - "dependencies": { - "asn1js": "^3.0.5", - "pvtsutils": "^1.3.5", - "tslib": "^2.6.2" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@peculiar/json-schema": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", - "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "node_modules/@graphql-tools/prisma-loader/node_modules/@whatwg-node/fetch": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.9.17.tgz", + "integrity": "sha512-TDYP3CpCrxwxpiNY0UMNf096H5Ihf67BK1iKGegQl5u9SlpEDYrvnV71gWBGJm+Xm31qOy8ATgma9rm8Pe7/5Q==", "dev": true, "dependencies": { - "tslib": "^2.0.0" + "@whatwg-node/node-fetch": "^0.5.7", + "urlpattern-polyfill": "^10.0.0" }, "engines": { - "node": ">=8.0.0" + "node": ">=16.0.0" } }, - "node_modules/@peculiar/webcrypto": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.5.tgz", - "integrity": "sha512-oDk93QCDGdxFRM8382Zdminzs44dg3M2+E5Np+JWkpqLDyJC9DviMh8F8mEJkYuUcUOGA5jHO5AJJ10MFWdbZw==", - "dev": true, + "node_modules/@graphql-tools/prisma-loader/node_modules/@whatwg-node/node-fetch": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.5.10.tgz", + "integrity": "sha512-KIAHepie/T1PRkUfze4t+bPlyvpxlWiXTPtcGlbIZ0vWkBJMdRmCg4ZrJ2y4XaO1eTPo1HlWYUuj1WvoIpumqg==", + "dev": true, "dependencies": { - "@peculiar/asn1-schema": "^2.3.8", - "@peculiar/json-schema": "^1.1.12", - "pvtsutils": "^1.3.5", - "tslib": "^2.6.2", - "webcrypto-core": "^1.7.8" + "@kamilkisiela/fast-url-parser": "^1.1.4", + "@whatwg-node/events": "^0.1.0", + "busboy": "^1.6.0", + "fast-querystring": "^1.1.1", + "tslib": "^2.3.1" }, "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=14" + "node": ">=16.0.0" } }, - "node_modules/@pm2/agent": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.0.3.tgz", - "integrity": "sha512-xkqqCoTf5VsciMqN0vb9jthW7olVAi4KRFNddCc7ZkeJZ3i8QwZANr4NSH2H5DvseRFHq7MiPspRY/EWAFWWTg==", - "dependencies": { - "async": "~3.2.0", - "chalk": "~3.0.0", - "dayjs": "~1.8.24", - "debug": "~4.3.1", - "eventemitter2": "~5.0.1", - "fast-json-patch": "^3.0.0-1", - "fclone": "~1.0.11", - "nssocket": "0.6.0", - "pm2-axon": "~4.0.1", - "pm2-axon-rpc": "~0.7.0", - "proxy-agent": "~6.3.0", - "semver": "~7.5.0", - "ws": "~7.4.0" - } + "node_modules/@graphql-tools/prisma-loader/node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "dev": true }, - "node_modules/@pm2/agent/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "node_modules/@graphql-tools/relay-operation-optimizer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.1.tgz", + "integrity": "sha512-y0ZrQ/iyqWZlsS/xrJfSir3TbVYJTYmMOu4TaSz6F4FRDTQ3ie43BlKkhf04rC28pnUOS4BO9pDcAo1D30l5+A==", + "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@ardatan/relay-compiler": "12.0.0", + "@graphql-tools/utils": "^10.0.13", + "tslib": "^2.4.0" }, "engines": { - "node": ">=8" + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@pm2/agent/node_modules/dayjs": { - "version": "1.8.36", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.36.tgz", - "integrity": "sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw==" - }, - "node_modules/@pm2/agent/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@graphql-tools/resolvers-composition": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/resolvers-composition/-/resolvers-composition-7.0.2.tgz", + "integrity": "sha512-ipTV1xjJSSx8iI0AQ5beZfUSlV08R8B+FGsbBnGCKiZ1Li4Y+egxR7wOcI9F3TlQlMExdSq3CjtoVfPpeYWsPw==", "dependencies": { - "yallist": "^4.0.0" + "@graphql-tools/utils": "^10.5.5", + "lodash": "4.17.21", + "micromatch": "^4.0.8", + "tslib": "^2.4.0" }, "engines": { - "node": ">=10" + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@pm2/agent/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "node_modules/@graphql-tools/schema": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.6.tgz", + "integrity": "sha512-EIJgPRGzpvDFEjVp+RF1zNNYIC36BYuIeZ514jFoJnI6IdxyVyIRDLx/ykgMdaa1pKQerpfdqDnsF4JnZoDHSQ==", "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "@graphql-tools/merge": "^9.0.6", + "@graphql-tools/utils": "^10.5.4", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" }, "engines": { - "node": ">=10" - } - }, - "node_modules/@pm2/agent/node_modules/ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", - "engines": { - "node": ">=8.3.0" + "node": ">=16.0.0" }, "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@pm2/io": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.0.0.tgz", - "integrity": "sha512-sKUEgZoQ5/jRwTyMB1I7u2wXL6dG0j/F/M4ANJ7dJCApfW8nWC0RElMW2siEKvZ79iplIPAaWV27oyBoerEflw==", + "node_modules/@graphql-tools/url-loader": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-8.0.2.tgz", + "integrity": "sha512-1dKp2K8UuFn7DFo1qX5c1cyazQv2h2ICwA9esHblEqCYrgf69Nk8N7SODmsfWg94OEaI74IqMoM12t7eIGwFzQ==", "dependencies": { - "async": "~2.6.1", - "debug": "~4.3.1", - "eventemitter2": "^6.3.1", - "require-in-the-middle": "^5.0.0", - "semver": "~7.5.4", - "shimmer": "^1.2.0", - "signal-exit": "^3.0.3", - "tslib": "1.9.3" + "@ardatan/sync-fetch": "^0.0.1", + "@graphql-tools/delegate": "^10.0.4", + "@graphql-tools/executor-graphql-ws": "^1.1.2", + "@graphql-tools/executor-http": "^1.0.9", + "@graphql-tools/executor-legacy-ws": "^1.0.6", + "@graphql-tools/utils": "^10.0.13", + "@graphql-tools/wrap": "^10.0.2", + "@types/ws": "^8.0.0", + "@whatwg-node/fetch": "^0.9.0", + "isomorphic-ws": "^5.0.0", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.11", + "ws": "^8.12.0" }, "engines": { - "node": ">=6.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@pm2/io/node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dependencies": { - "lodash": "^4.17.14" + "node_modules/@graphql-tools/url-loader/node_modules/@whatwg-node/events": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.1.tgz", + "integrity": "sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==", + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@pm2/io/node_modules/eventemitter2": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", - "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==" - }, - "node_modules/@pm2/io/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@graphql-tools/url-loader/node_modules/@whatwg-node/fetch": { + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.9.17.tgz", + "integrity": "sha512-TDYP3CpCrxwxpiNY0UMNf096H5Ihf67BK1iKGegQl5u9SlpEDYrvnV71gWBGJm+Xm31qOy8ATgma9rm8Pe7/5Q==", "dependencies": { - "yallist": "^4.0.0" + "@whatwg-node/node-fetch": "^0.5.7", + "urlpattern-polyfill": "^10.0.0" }, "engines": { - "node": ">=10" + "node": ">=16.0.0" } }, - "node_modules/@pm2/io/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "node_modules/@graphql-tools/url-loader/node_modules/@whatwg-node/node-fetch": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.5.10.tgz", + "integrity": "sha512-KIAHepie/T1PRkUfze4t+bPlyvpxlWiXTPtcGlbIZ0vWkBJMdRmCg4ZrJ2y4XaO1eTPo1HlWYUuj1WvoIpumqg==", "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "@kamilkisiela/fast-url-parser": "^1.1.4", + "@whatwg-node/events": "^0.1.0", + "busboy": "^1.6.0", + "fast-querystring": "^1.1.1", + "tslib": "^2.3.1" }, "engines": { - "node": ">=10" + "node": ">=16.0.0" } }, - "node_modules/@pm2/io/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/@pm2/io/node_modules/tslib": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" + "node_modules/@graphql-tools/url-loader/node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==" }, - "node_modules/@pm2/js-api": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@pm2/js-api/-/js-api-0.8.0.tgz", - "integrity": "sha512-nmWzrA/BQZik3VBz+npRcNIu01kdBhWL0mxKmP1ciF/gTcujPTQqt027N9fc1pK9ERM8RipFhymw7RcmCyOEYA==", + "node_modules/@graphql-tools/utils": { + "version": "10.5.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.5.5.tgz", + "integrity": "sha512-LF/UDWmMT0mnobL2UZETwYghV7HYBzNaGj0SAkCYOMy/C3+6sQdbcTksnoFaKR9XIVD78jNXEGfivbB8Zd+cwA==", "dependencies": { - "async": "^2.6.3", - "debug": "~4.3.1", - "eventemitter2": "^6.3.1", - "extrareqp2": "^1.0.0", - "ws": "^7.0.0" + "@graphql-typed-document-node/core": "^3.1.1", + "cross-inspect": "1.0.1", + "dset": "^3.1.2", + "tslib": "^2.4.0" }, "engines": { - "node": ">=4.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@pm2/js-api/node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "node_modules/@graphql-tools/utils/node_modules/cross-inspect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.1.tgz", + "integrity": "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==", "dependencies": { - "lodash": "^4.17.14" + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@pm2/js-api/node_modules/eventemitter2": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", - "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==" - }, - "node_modules/@pm2/js-api/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "node_modules/@graphql-tools/wrap": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.0.5.tgz", + "integrity": "sha512-Cbr5aYjr3HkwdPvetZp1cpDWTGdD1Owgsb3z/ClzhmrboiK86EnQDxDvOJiQkDCPWE9lNBwj8Y4HfxroY0D9DQ==", + "dependencies": { + "@graphql-tools/delegate": "^10.0.4", + "@graphql-tools/schema": "^10.0.3", + "@graphql-tools/utils": "^10.1.1", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" + }, "engines": { - "node": ">=8.3.0" + "node": ">=16.0.0" }, "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@pm2/pm2-version-check": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", - "integrity": "sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==", - "dependencies": { - "debug": "^4.3.1" + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" + "node_modules/@graphql-yoga/logger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@graphql-yoga/logger/-/logger-2.0.0.tgz", + "integrity": "sha512-Mg8psdkAp+YTG1OGmvU+xa6xpsAmSir0hhr3yFYPyLNwzUj95DdIwsMpKadDj9xDpYgJcH3Hp/4JMal9DhQimA==", + "dependencies": { + "tslib": "^2.5.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "node_modules/@graphql-yoga/subscription": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@graphql-yoga/subscription/-/subscription-5.0.0.tgz", + "integrity": "sha512-Ri7sK8hmxd/kwaEa0YT8uqQUb2wOLsmBMxI90QDyf96lzOMJRgBuNYoEkU1pSgsgmW2glceZ96sRYfaXqwVxUw==", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@graphql-yoga/typed-event-target": "^3.0.0", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/events": "^0.1.0", + "tslib": "^2.5.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" - }, - "node_modules/@redis/bloom": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", - "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", - "peerDependencies": { - "@redis/client": "^1.0.0" + "node_modules/@graphql-yoga/subscription/node_modules/@whatwg-node/events": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.1.tgz", + "integrity": "sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==", + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@redis/client": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", - "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "node_modules/@graphql-yoga/typed-event-target": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@graphql-yoga/typed-event-target/-/typed-event-target-3.0.0.tgz", + "integrity": "sha512-w+liuBySifrstuHbFrHoHAEyVnDFVib+073q8AeAJ/qqJfvFvAwUPLLtNohR/WDVRgSasfXtl3dcNuVJWN+rjg==", "dependencies": { - "cluster-key-slot": "1.1.2", - "generic-pool": "3.9.0", - "yallist": "4.0.0" + "@repeaterjs/repeater": "^3.0.4", + "tslib": "^2.5.2" }, "engines": { - "node": ">=14" + "node": ">=18.0.0" } }, - "node_modules/@redis/graph": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", - "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", - "peerDependencies": { - "@redis/client": "^1.0.0" + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@redis/json": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", - "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", - "peerDependencies": { - "@redis/client": "^1.0.0" + "node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@redis/search": { + "node_modules/@ioredis/commands": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", - "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", - "peerDependencies": { - "@redis/client": "^1.0.0" - } + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" }, - "node_modules/@redis/time-series": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", - "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", - "peerDependencies": { - "@redis/client": "^1.0.0" + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" } }, - "node_modules/@repeaterjs/repeater": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.5.tgz", - "integrity": "sha512-l3YHBLAol6d/IKnB9LhpD0cEZWAoe3eFKUyTYWmFmCO2Q/WOckxLQAUyMZWwZV2M/m3+4vgRoaolFqaII82/TA==" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", - "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", - "cpu": [ - "arm" - ], + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", - "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", - "cpu": [ - "arm64" - ], + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", - "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", - "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", - "cpu": [ - "x64" - ], + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", - "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", - "cpu": [ - "arm" - ], + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", - "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", - "cpu": [ - "arm" - ], + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", - "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", - "cpu": [ - "arm64" - ], + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=8" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", - "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", - "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", - "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", - "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kamilkisiela/fast-url-parser": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@kamilkisiela/fast-url-parser/-/fast-url-parser-1.1.4.tgz", + "integrity": "sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew==" + }, + "node_modules/@messageformat/core": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@messageformat/core/-/core-3.3.0.tgz", + "integrity": "sha512-YcXd3remTDdeMxAlbvW6oV9d/01/DZ8DHUFwSttO3LMzIZj3iO0NRw+u1xlsNNORFI+u0EQzD52ZX3+Udi0T3g==", + "dependencies": { + "@messageformat/date-skeleton": "^1.0.0", + "@messageformat/number-skeleton": "^1.0.0", + "@messageformat/parser": "^5.1.0", + "@messageformat/runtime": "^3.0.1", + "make-plural": "^7.0.0", + "safe-identifier": "^0.4.1" + } + }, + "node_modules/@messageformat/date-skeleton": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@messageformat/date-skeleton/-/date-skeleton-1.0.1.tgz", + "integrity": "sha512-jPXy8fg+WMPIgmGjxSlnGJn68h/2InfT0TNSkVx0IGXgp4ynnvYkbZ51dGWmGySEK+pBiYUttbQdu5XEqX5CRg==" + }, + "node_modules/@messageformat/number-skeleton": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz", + "integrity": "sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg==" + }, + "node_modules/@messageformat/parser": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@messageformat/parser/-/parser-5.1.0.tgz", + "integrity": "sha512-jKlkls3Gewgw6qMjKZ9SFfHUpdzEVdovKFtW1qRhJ3WI4FW5R/NnGDqr8SDGz+krWDO3ki94boMmQvGke1HwUQ==", + "dependencies": { + "moo": "^0.5.1" + } + }, + "node_modules/@messageformat/runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@messageformat/runtime/-/runtime-3.0.1.tgz", + "integrity": "sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg==", + "dependencies": { + "make-plural": "^7.0.0" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", + "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==", + "dev": true + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.0.tgz", + "integrity": "sha512-v/EYRXnCAIHxOHW+Plb6OWuUoMotxTN0GLatnpOb1xq0KuTNw/WI3pamJx/UbsoJP5k9MCw1QxvvhPcF9pH3Zg==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.15.0", + "ajv": "~8.12.0", + "jju": "~1.4.0", + "resolve": "~1.22.2" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.5.tgz", + "integrity": "sha512-XLNOMH66KhJzUJNwT/qlMnS4WsNDWD5ASdyaSH3EtK+F4r/CFGa3jT4GNi4mfOitGvWXtdLgQJkQjxSVrio+jA==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz", + "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", + "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/lab": { + "version": "5.0.0-alpha.169", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.169.tgz", + "integrity": "sha512-h6xe1K6ISKUbyxTDgdvql4qoDP6+q8ad5fg9nXQxGLUrIeT2jVrBuT/jRECSTufbnhzP+V5kulvYxaMfM8rEdA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.40", + "@mui/system": "^5.15.14", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": ">=5.15.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", + "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/core-downloads-tracker": "^5.16.7", + "@mui/system": "^5.16.7", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.6", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^18.3.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz", + "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.16.6", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz", + "integrity": "sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", + "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.16.6", + "@mui/styled-engine": "^5.16.6", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.6", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.18", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.18.tgz", + "integrity": "sha512-uvK9dWeyCJl/3ocVnTOS6nlji/Knj8/tVqVX03UVTpdmTJYu/s4jtDd9Kvv0nRGE0CUSNW1UYAci7PYypjealg==", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", + "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/types": "^7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.3.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dev": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz", + "integrity": "sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==", + "dev": true, + "dependencies": { + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dev": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.5.tgz", + "integrity": "sha512-oDk93QCDGdxFRM8382Zdminzs44dg3M2+E5Np+JWkpqLDyJC9DviMh8F8mEJkYuUcUOGA5jHO5AJJ10MFWdbZw==", + "dev": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/json-schema": "^1.1.12", + "pvtsutils": "^1.3.5", + "tslib": "^2.6.2", + "webcrypto-core": "^1.7.8" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pm2/agent": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.0.3.tgz", + "integrity": "sha512-xkqqCoTf5VsciMqN0vb9jthW7olVAi4KRFNddCc7ZkeJZ3i8QwZANr4NSH2H5DvseRFHq7MiPspRY/EWAFWWTg==", + "dependencies": { + "async": "~3.2.0", + "chalk": "~3.0.0", + "dayjs": "~1.8.24", + "debug": "~4.3.1", + "eventemitter2": "~5.0.1", + "fast-json-patch": "^3.0.0-1", + "fclone": "~1.0.11", + "nssocket": "0.6.0", + "pm2-axon": "~4.0.1", + "pm2-axon-rpc": "~0.7.0", + "proxy-agent": "~6.3.0", + "semver": "~7.5.0", + "ws": "~7.4.0" + } + }, + "node_modules/@pm2/agent/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@pm2/agent/node_modules/dayjs": { + "version": "1.8.36", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.36.tgz", + "integrity": "sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw==" + }, + "node_modules/@pm2/agent/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/agent/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/agent/node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@pm2/io": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.0.0.tgz", + "integrity": "sha512-sKUEgZoQ5/jRwTyMB1I7u2wXL6dG0j/F/M4ANJ7dJCApfW8nWC0RElMW2siEKvZ79iplIPAaWV27oyBoerEflw==", + "dependencies": { + "async": "~2.6.1", + "debug": "~4.3.1", + "eventemitter2": "^6.3.1", + "require-in-the-middle": "^5.0.0", + "semver": "~7.5.4", + "shimmer": "^1.2.0", + "signal-exit": "^3.0.3", + "tslib": "1.9.3" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@pm2/io/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/@pm2/io/node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==" + }, + "node_modules/@pm2/io/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/io/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/io/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/@pm2/io/node_modules/tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" + }, + "node_modules/@pm2/js-api": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@pm2/js-api/-/js-api-0.8.0.tgz", + "integrity": "sha512-nmWzrA/BQZik3VBz+npRcNIu01kdBhWL0mxKmP1ciF/gTcujPTQqt027N9fc1pK9ERM8RipFhymw7RcmCyOEYA==", + "dependencies": { + "async": "^2.6.3", + "debug": "~4.3.1", + "eventemitter2": "^6.3.1", + "extrareqp2": "^1.0.0", + "ws": "^7.0.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@pm2/js-api/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/@pm2/js-api/node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==" + }, + "node_modules/@pm2/js-api/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@pm2/pm2-version-check": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", + "integrity": "sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==", + "dependencies": { + "debug": "^4.3.1" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@repeaterjs/repeater": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.5.tgz", + "integrity": "sha512-l3YHBLAol6d/IKnB9LhpD0cEZWAoe3eFKUyTYWmFmCO2Q/WOckxLQAUyMZWwZV2M/m3+4vgRoaolFqaII82/TA==" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, + "node_modules/@shikijs/core": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.10.3.tgz", + "integrity": "sha512-D45PMaBaeDHxww+EkcDQtDAtzv00Gcsp72ukBtaLSmqRvh0WgGMq3Al0rl1QQBZfuneO75NXMIzEZGFitThWbg==", + "peer": true, + "dependencies": { + "@types/hast": "^3.0.4" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.5.tgz", + "integrity": "sha512-DhNPnqTqPoG8aZ5dWkFOgsuY+i0GQ3CI6hMmvCoduNsnU9gUZWZBwGfDQsTTB7NvFPkom1df7jMIJWU90kuXXg==", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-3.0.0.tgz", + "integrity": "sha512-sbnURCwjF0gSToGlsBiAmd1lRCmSn72nu9axfJu5lIx6RUEgHu6GwTMbqCdhQSi0Pumcm5vFxsi9XWXb2mTaoA==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-3.0.0.tgz", + "integrity": "sha512-VDkpCYW+peSuM4zJip5WDfqvg2Mo/e8yxOv3VF1m11y7B8KKMKVFtmZWDe36Fvk8rGuWrPZHHXZ7rR7uM5yWyg==", + "dependencies": { + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.9.tgz", + "integrity": "sha512-5d9oBf40qC7n2xUoHmntKLdqsyTMMo/r49+eqSIjJ73eDfEtljAxEhzIQ3bkgXJtR3xiv7YzMT/3FF3ORkjWdg==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.4.8.tgz", + "integrity": "sha512-x4qWk7p/a4dcf7Vxb2MODIf4OIcqNbK182WxRvZ/3oKPrf/6Fdic5sSElhO1UtXpWKBazWfqg0ZEK9xN1DsuHA==", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.23", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.4.tgz", + "integrity": "sha512-S9bb0EIokfYEuar4kEbLta+ivlKCWOCFsLZuilkNy9i0uEUEHSi47IFLPaxqqCl+0ftKmcOTHayY5nQhAuq7+w==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.8", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-3.1.6.tgz", + "integrity": "sha512-SBiOYPBH+5wOyPS7lfI150ePfGLhnp/eTu5RnV9xvhGvRiKfnl6HzRK9wehBph+il8FxS9KTeadx7Rcmf1GLPQ==", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^3.5.0", + "@smithy/util-hex-encoding": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.10.tgz", + "integrity": "sha512-1i9aMY6Pl/SmA6NjvidxnfBLHMPzhKu2BP148pEt5VwhMdmXn36PE2kWKGa9Hj8b0XGtCTRucpCncylevCtI7g==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.9", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.7.tgz", + "integrity": "sha512-eVzhGQBPEqXXYHvIUku0jMTxd4gDvenRzUQPTmKVWdRvp9JUCKrbAXGQRYiGxUYq9+cqQckRm0wq3kTWnNtDhw==", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.9.tgz", + "integrity": "sha512-JE0Guqvt0xsmfQ5y1EI342/qtJqznBv8cJqkHZV10PwC8GWGU5KNgFbQnsVCcX+xF+qIqwwfRmeWoJCjuOLmng==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.9", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.9.tgz", + "integrity": "sha512-bydfgSisfepCufw9kCEnWRxqxJFzX/o8ysXWv+W9F2FIyiaEwZ/D8bBKINbh4ONz3i05QJ1xE7A5OKYvgJsXaw==", + "dependencies": { + "@smithy/eventstream-codec": "^3.1.6", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.9.tgz", + "integrity": "sha512-hYNVQOqhFQ6vOpenifFME546f0GfJn2OiQ3M0FDmuUu8V/Uiwy2wej7ZXxFBNqdx0R5DZAqWM1l6VRhGz8oE6A==", + "dependencies": { + "@smithy/protocol-http": "^4.1.4", + "@smithy/querystring-builder": "^3.0.7", + "@smithy/types": "^3.5.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-3.1.6.tgz", + "integrity": "sha512-BKNcMIaeZ9lB67sgo88iCF4YB35KT8X2dNJ8DqrtZNTgN6tUDYBKThzfGtos/mnZkGkW91AYHisESHmSiYQmKw==", + "dependencies": { + "@smithy/chunked-blob-reader": "^3.0.0", + "@smithy/chunked-blob-reader-native": "^3.0.0", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/hash-node": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.7.tgz", + "integrity": "sha512-SAGHN+QkrwcHFjfWzs/czX94ZEjPJ0CrWJS3M43WswDXVEuP4AVy9gJ3+AF6JQHZD13bojmuf/Ap/ItDeZ+Qfw==", + "dependencies": { + "@smithy/types": "^3.5.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-3.1.6.tgz", + "integrity": "sha512-sFSSt7cmCpFWZPfVx7k80Bgb1K2VJ27VmMxH8X+dDhp7Wv8IBgID4K2VK5ehMJROF8hQgcj4WywnkHIwX/xlwQ==", + "dependencies": { + "@smithy/types": "^3.5.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.7.tgz", + "integrity": "sha512-Bq00GsAhHeYSuZX8Kpu4sbI9agH2BNYnqUmmbTGWOhki9NVsWn2jFr896vvoTMH8KAjNX/ErC/8t5QHuEXG+IA==", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-3.0.7.tgz", + "integrity": "sha512-+wco9IN9uOW4tNGkZIqTR6IXyfO7Z8A+IOq82QCRn/f/xcmt7H1fXwmQVbfDSvbeFwfNnhv7s+u0G9PzPG6o2w==", + "dependencies": { + "@smithy/types": "^3.5.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.9.tgz", + "integrity": "sha512-t97PidoGElF9hTtLCrof32wfWMqC5g2SEJNxaVH3NjlatuNGsdxXRYO/t+RPnxA15RpYiS0f+zG7FuE2DeGgjA==", + "dependencies": { + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.1.4.tgz", + "integrity": "sha512-/ChcVHekAyzUbyPRI8CzPPLj6y8QRAfJngWcLMgsWxKVzw/RzBV69mSOzJYDD3pRwushA1+5tHtPF8fjmzBnrQ==", + "dependencies": { + "@smithy/middleware-serde": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-middleware": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.23.tgz", + "integrity": "sha512-x9PbGXxkcXIpm6L26qRSCC+eaYcHwybRmqU8LO/WM2RRlW0g8lz6FIiKbKgGvHuoK3dLZRiQVSQJveiCzwnA5A==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.8", + "@smithy/protocol-http": "^4.1.4", + "@smithy/service-error-classification": "^3.0.7", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.7.tgz", + "integrity": "sha512-VytaagsQqtH2OugzVTq4qvjkLNbWehHfGcGr0JLJmlDRrNCeZoWkWsSOw1nhS/4hyUUWF/TLGGml4X/OnEep5g==", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.7.tgz", + "integrity": "sha512-EyTbMCdqS1DoeQsO4gI7z2Gzq1MoRFAeS8GkFYIwbedB7Lp5zlLHJdg+56tllIIG5Hnf9ZWX48YKSHlsKvugGA==", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.8.tgz", + "integrity": "sha512-E0rU0DglpeJn5ge64mk8wTGEXcQwmpUTY5Zr7IzTpDLmHKiIamINERNZYrPQjg58Ck236sEKSwRSHA4CwshU6Q==", + "dependencies": { + "@smithy/property-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.2.4.tgz", + "integrity": "sha512-49reY3+JgLMFNm7uTAKBWiKCA6XSvkNp9FqhVmusm2jpVnHORYFeFZ704LShtqWfjZW/nhX+7Iexyb6zQfXYIQ==", + "dependencies": { + "@smithy/abort-controller": "^3.1.5", + "@smithy/protocol-http": "^4.1.4", + "@smithy/querystring-builder": "^3.0.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.7.tgz", + "integrity": "sha512-QfzLi1GPMisY7bAM5hOUqBdGYnY5S2JAlr201pghksrQv139f8iiiMalXtjczIP5f6owxFn3MINLNUNvUkgtPw==", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.4.tgz", + "integrity": "sha512-MlWK8eqj0JlpZBnWmjQLqmFp71Ug00P+m72/1xQB3YByXD4zZ+y9N4hYrR0EDmrUCZIkyATWHOXFgtavwGDTzQ==", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.7.tgz", + "integrity": "sha512-65RXGZZ20rzqqxTsChdqSpbhA6tdt5IFNgG6o7e1lnPVLCe6TNWQq4rTl4N87hTDD8mV4IxJJnvyE7brbnRkQw==", + "dependencies": { + "@smithy/types": "^3.5.0", + "@smithy/util-uri-escape": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.7.tgz", + "integrity": "sha512-Fouw4KJVWqqUVIu1gZW8BH2HakwLz6dvdrAhXeXfeymOBrZw+hcqaWs+cS1AZPVp4nlbeIujYrKA921ZW2WMPA==", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.7.tgz", + "integrity": "sha512-91PRkTfiBf9hxkIchhRKJfl1rsplRDyBnmyFca3y0Z3x/q0JJN480S83LBd8R6sBCkm2bBbqw2FHp0Mbh+ecSA==", + "dependencies": { + "@smithy/types": "^3.5.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.8.tgz", + "integrity": "sha512-0NHdQiSkeGl0ICQKcJQ2lCOKH23Nb0EaAa7RDRId6ZqwXkw4LJyIyZ0t3iusD4bnKYDPLGy2/5e2rfUhrt0Acw==", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.2.0.tgz", + "integrity": "sha512-LafbclHNKnsorMgUkKm7Tk7oJ7xizsZ1VwqhGKqoCIrXh4fqDDp73fK99HOEEgcsQbtemmeY/BPv0vTVYYUNEQ==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.4.0.tgz", + "integrity": "sha512-nOfJ1nVQsxiP6srKt43r2My0Gp5PLWCW2ASqUioxIiGmu6d32v4Nekidiv5qOmmtzIrmaD+ADX5SKHUuhReeBQ==", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "@smithy/util-stream": "^3.1.9", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.5.0.tgz", + "integrity": "sha512-QN0twHNfe8mNJdH9unwsCK13GURU7oEAZqkBI+rsvpv1jrmserO+WnLE7jidR9W/1dxwZ0u/CB01mV2Gms/K2Q==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.7.tgz", + "integrity": "sha512-70UbSSR8J97c1rHZOWhl+VKiZDqHWxs/iW8ZHrHp5fCCPLSBE7GcUlUvKSle3Ca+J9LLbYCj/A79BxztBvAfpA==", + "dependencies": { + "@smithy/querystring-parser": "^3.0.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/util-base64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", + "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz", + "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz", + "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", + "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.23.tgz", + "integrity": "sha512-Y07qslyRtXDP/C5aWKqxTPBl4YxplEELG3xRrz2dnAQ6Lq/FgNrcKWmV561nNaZmFH+EzeGOX3ZRMbU8p1T6Nw==", + "dependencies": { + "@smithy/property-provider": "^3.1.7", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.23.tgz", + "integrity": "sha512-9Y4WH7f0vnDGuHUa4lGX9e2p+sMwODibsceSV6rfkZOvMC+BY3StB2LdO1NHafpsyHJLpwAgChxQ38tFyd6vkg==", + "dependencies": { + "@smithy/config-resolver": "^3.0.9", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/property-provider": "^3.1.7", + "@smithy/smithy-client": "^3.4.0", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.3.tgz", + "integrity": "sha512-34eACeKov6jZdHqS5hxBMJ4KyWKztTMulhuQ2UdOoP6vVxMLrOKUqIXAwJe/wiWMhXhydLW664B02CNpQBQ4Aw==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", - "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", + "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", - "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@smithy/util-middleware": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.7.tgz", + "integrity": "sha512-OVA6fv/3o7TMJTpTgOi1H5OTwnuUa8hzRzhSFDtZyNxi6OZ70L/FHattSmhE212I7b6WSOJAAmbYnvcjTHOJCA==", + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", - "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "node_modules/@smithy/util-retry": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.7.tgz", + "integrity": "sha512-nh1ZO1vTeo2YX1plFPSe/OXaHkLAHza5jpokNiiKX2M5YpNUv6RxGJZhpfmiR4jSvVHCjIDmILjrxKmP+/Ghug==", + "dependencies": { + "@smithy/service-error-classification": "^3.0.7", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", - "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "node_modules/@smithy/util-stream": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.1.9.tgz", + "integrity": "sha512-7YAR0Ub3MwTMjDfjnup4qa6W8gygZMxikBhFMPESi6ASsl/rZJhwLpF/0k9TuezScCojsM0FryGdz4LZtjKPPQ==", + "dependencies": { + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/types": "^3.5.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", - "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", + "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true + "node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } }, - "node_modules/@shikijs/core": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.10.3.tgz", - "integrity": "sha512-D45PMaBaeDHxww+EkcDQtDAtzv00Gcsp72ukBtaLSmqRvh0WgGMq3Al0rl1QQBZfuneO75NXMIzEZGFitThWbg==", - "peer": true, + "node_modules/@smithy/util-waiter": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.1.6.tgz", + "integrity": "sha512-xs/KAwWOeCklq8aMlnpk25LgxEYHKOEodfjfKclDMLcBJEVEKzDLxZxBQyztcuPJ7F54213NJS8PxoiHNMdItQ==", "dependencies": { - "@types/hast": "^3.0.4" + "@smithy/abort-controller": "^3.1.5", + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, "node_modules/@tokenizer/token": { @@ -5492,6 +7012,12 @@ "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.8.tgz", "integrity": "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==" }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true + }, "node_modules/@types/cookies": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz", @@ -5691,6 +7217,12 @@ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -5715,6 +7247,15 @@ "@types/node": "*" } }, + "node_modules/@types/multer": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "22.7.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.8.tgz", @@ -5802,6 +7343,28 @@ "@types/node": "*" } }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", + "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", + "dev": true, + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/through": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", @@ -6636,6 +8199,11 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -7130,9 +8698,9 @@ "integrity": "sha512-Ylo+MAo5BDUq1KA3f3R/MFhh+g8cnHmo8bz3YPGhI1znrMaf77ol1sfvYJzsw3nTE+Y2GryfDxBaR+AqpAkEHQ==" }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -7142,7 +8710,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -7170,6 +8738,11 @@ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==" }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -7817,6 +9390,15 @@ "node": "*" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7928,9 +9510,9 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -7940,6 +9522,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, "node_modules/copy-paste": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/copy-paste/-/copy-paste-1.5.3.tgz", @@ -8326,6 +9914,16 @@ "node": ">=0.10" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diacritics-map": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/diacritics-map/-/diacritics-map-0.1.0.tgz", @@ -8440,9 +10038,9 @@ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -9534,36 +11132,36 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -9737,6 +11335,12 @@ "fast-decode-uri-component": "^1.0.1" } }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "node_modules/fast-url-parser": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", @@ -9746,6 +11350,27 @@ "punycode": "^1.3.2" } }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -9860,12 +11485,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -10006,6 +11631,20 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", + "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", + "dev": true, + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^2.0.0", + "once": "^1.4.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -10900,6 +12539,15 @@ "node": ">=18.0.0" } }, + "node_modules/hexoid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", + "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -12910,9 +14558,12 @@ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -13284,6 +14935,34 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mustache": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", @@ -13991,9 +15670,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/path-type": { "version": "4.0.0", @@ -14435,11 +16114,11 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -15224,9 +16903,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -15259,6 +16938,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/sentence-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", @@ -15271,14 +16958,14 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -15830,6 +17517,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "node_modules/strtok3": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", @@ -15851,6 +17543,51 @@ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, + "node_modules/superagent": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "dev": true, + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "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.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz", + "integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==", + "dev": true, + "dependencies": { + "methods": "^1.1.2", + "superagent": "^9.0.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index 835c08100c..17fb96343a 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "homepage": "https://github.com/PalisadoesFoundation/talawa-api#readme", "dependencies": { "@apollo/server": "^4.11.0", + "@aws-sdk/client-s3": "^3.675.0", "@faker-js/faker": "^9.0.1", "@graphql-inspector/cli": "^5.0.6", "@graphql-tools/resolvers-composition": "^7.0.2", @@ -90,6 +91,7 @@ "mongoose": "^8.3.2", "mongoose-paginate-v2": "^1.8.3", "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", "nanoid": "^5.0.7", "nodemailer": "^6.9.15", "pm2": "^5.4.0", @@ -126,8 +128,10 @@ "@types/lodash": "^4.17.12", "@types/mongoose-paginate-v2": "^1.6.5", "@types/morgan": "^1.9.9", + "@types/multer": "^1.4.12", "@types/node": "^22.7.8", "@types/nodemailer": "^6.4.15", + "@types/supertest": "^6.0.2", "@types/uuid": "^10.0.0", "@types/validator": "^13.12.2", "@vitest/coverage-v8": "^2.1.3", @@ -144,6 +148,7 @@ "lint-staged": "^15.2.10", "prettier": "^3.3.3", "rimraf": "^6.0.1", + "supertest": "^7.0.0", "tsx": "^4.19.1", "typescript": "^5.5.4", "vitest": "^2.1.3" diff --git a/schema.graphql b/schema.graphql index 4d94d1d4c7..a28cc18bf9 100644 --- a/schema.graphql +++ b/schema.graphql @@ -874,6 +874,35 @@ interface FieldError { path: [String!]! } +type File { + _id: ID! + archived: Boolean! + archivedAt: DateTime + backupStatus: String! + createdAt: DateTime! + encryption: Boolean! + fileName: String! + hash: Hash! + metadata: FileMetadata! + mimeType: String! + referenceCount: Int! + size: Int! + status: Status! + updatedAt: DateTime! + uri: String! + visibility: FileVisibility! +} + +type FileMetadata { + bucketName: String! + objectKey: String! +} + +enum FileVisibility { + PRIVATE + PUBLIC +} + input ForgotPasswordData { newPassword: String! otpToken: String! @@ -978,6 +1007,11 @@ type Group { updatedAt: DateTime! } +type Hash { + algorithm: String! + value: String! +} + type HoursHistory { date: Date! hours: Float! @@ -1418,7 +1452,7 @@ type Post { comments: [Comment] createdAt: DateTime! creator: User - imageUrl: URL + file: File likeCount: Int likedBy: [User] organization: Organization! @@ -1426,7 +1460,6 @@ type Post { text: String! title: String updatedAt: DateTime! - videoUrl: URL } type PostEdge { @@ -1518,7 +1551,6 @@ type Query { customDataByOrganization(organizationId: ID!): [UserCustomData!]! customFieldsByOrganization(id: ID!): [OrganizationCustomField] event(id: ID!): Event - eventVolunteersByEvent(id: ID!): [EventVolunteer] eventsAttendedByUser(id: ID, orderBy: EventOrderByInput): [Event] eventsByOrganization(id: ID, orderBy: EventOrderByInput): [Event] eventsByOrganizationConnection(first: Int, orderBy: EventOrderByInput, skip: Int, upcomingOnly: Boolean, where: EventWhereInput): [Event!]! @@ -1852,6 +1884,7 @@ type User { employmentStatus: EmploymentStatus eventAdmin: [Event] eventsAttended: [Event] + file: File firstName: String! gender: Gender identifier: Int! diff --git a/src/REST/controllers/mutation/createPost.ts b/src/REST/controllers/mutation/createPost.ts new file mode 100644 index 0000000000..4a59978317 --- /dev/null +++ b/src/REST/controllers/mutation/createPost.ts @@ -0,0 +1,262 @@ +import type { Response } from "express"; +import mongoose from "mongoose"; +import { + INTERNAL_SERVER_ERROR, + LENGTH_VALIDATION_ERROR, + ORGANIZATION_NOT_FOUND_ERROR, + PLEASE_PROVIDE_TITLE, + USER_NOT_AUTHORIZED_ERROR, + USER_NOT_AUTHORIZED_TO_PIN, + USER_NOT_FOUND_ERROR, + USER_NOT_MEMBER_FOR_ORGANIZATION, +} from "../../../constants"; +import { errors, requestContext } from "../../../libraries"; +import { isValidString } from "../../../libraries/validators/validateString"; +import type { + InterfaceAppUserProfile, + InterfaceOrganization, + InterfaceUser, +} from "../../../models"; +import { AppUserProfile, Organization, Post, User } from "../../../models"; +import { cacheAppUserProfile } from "../../../services/AppUserProfileCache/cacheAppUserProfile"; +import { findAppUserProfileCache } from "../../../services/AppUserProfileCache/findAppUserProfileCache"; +import { cacheOrganizations } from "../../../services/OrganizationCache/cacheOrganizations"; +import { findOrganizationsInCache } from "../../../services/OrganizationCache/findOrganizationsInCache"; +import { cachePosts } from "../../../services/PostCache/cachePosts"; +import { cacheUsers } from "../../../services/UserCache/cacheUser"; +import { findUserInCache } from "../../../services/UserCache/findUserInCache"; +import type { InterfaceAuthenticatedRequest } from "../../../middleware"; +import { uploadFile } from "../../services/file"; + +interface InterfaceCreatePostRequestBody { + organizationId: string; + title?: string; + text: string; + pinned?: boolean; +} + +/** + * Controller for creating posts within organizations + */ + +/** + * Creates a new post within an organization + * async + * function - createPost + * @param req - Express request object with authenticated user + * @param res - Express response object + * @throws NotFoundError - When user or organization is not found + * @throws UnauthorizedError - When user is not authorized or lacks permissions + * @throws InputValidationError - When title or text validation fails + * @returns Promise - Responds with created post or error + * + * Description + * This controller handles post creation with the following features: + * - Validates user membership in the organization + * - Supports file attachments + * - Handles post pinning with proper authorization + * - Validates title and text length + * - Caches created posts and updated organizations + * + * Request body expects: + * ```typescript + * { + * organizationId: string; + * title?: string; + * text: string; + * pinned?: boolean; + * } + * ``` + */ +export const createPost = async ( + req: InterfaceAuthenticatedRequest, + res: Response, +): Promise => { + const userId = req.userId; + const { + organizationId, + title, + text, + pinned, + }: InterfaceCreatePostRequestBody = req.body; + + try { + // Get the current user + let currentUser: InterfaceUser | null; + const userFoundInCache = await findUserInCache([userId as string]); + currentUser = userFoundInCache[0]; + if (currentUser === null) { + currentUser = await User.findOne({ _id: userId }).lean(); + if (currentUser !== null) { + await cacheUsers([currentUser]); + } + } + + // Check if currentUser exists + if (!currentUser) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM, + ); + } + + // Get current user's app profile + let currentUserAppProfile: InterfaceAppUserProfile | null; + const appUserProfileFoundInCache = await findAppUserProfileCache([ + currentUser.appUserProfileId?.toString(), + ]); + currentUserAppProfile = appUserProfileFoundInCache[0]; + if (currentUserAppProfile === null) { + currentUserAppProfile = await AppUserProfile.findOne({ + userId: currentUser._id, + }).lean(); + if (currentUserAppProfile !== null) { + await cacheAppUserProfile([currentUserAppProfile]); + } + } + + if (!currentUserAppProfile) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM, + ); + } + + // Get the organization + let organization: InterfaceOrganization | null; + const organizationFoundInCache = await findOrganizationsInCache([ + organizationId, + ]); + organization = organizationFoundInCache[0]; + if (organization === null) { + organization = await Organization.findOne({ _id: organizationId }).lean(); + if (organization) { + await cacheOrganizations([organization]); + } + } + + if (!organization) { + throw new errors.NotFoundError( + requestContext.translate(ORGANIZATION_NOT_FOUND_ERROR.MESSAGE), + ORGANIZATION_NOT_FOUND_ERROR.CODE, + ORGANIZATION_NOT_FOUND_ERROR.PARAM, + ); + } + + // Check if user is a member of the organization or a super admin + const isSuperAdmin = currentUserAppProfile.isSuperAdmin; + const currentUserIsOrganizationMember = organization.members.some( + (memberId) => + new mongoose.Types.ObjectId(memberId?.toString()).equals(userId), + ); + + if (!currentUserIsOrganizationMember && !isSuperAdmin) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_MEMBER_FOR_ORGANIZATION.MESSAGE), + USER_NOT_MEMBER_FOR_ORGANIZATION.CODE, + USER_NOT_MEMBER_FOR_ORGANIZATION.PARAM, + ); + } + + let fileUploadResponse; + if (req.file) { + fileUploadResponse = await uploadFile(req, res); + } + + // Validate title and pinned status + if (!title && pinned) { + throw new errors.InputValidationError( + requestContext.translate(PLEASE_PROVIDE_TITLE.MESSAGE), + PLEASE_PROVIDE_TITLE.CODE, + ); + } + + // Validate title and text length + if (title) { + const validationResultTitle = isValidString(title, 256); + if (!validationResultTitle.isLessThanMaxLength) { + throw new errors.InputValidationError( + requestContext.translate( + `${LENGTH_VALIDATION_ERROR.MESSAGE} 256 characters in title`, + ), + LENGTH_VALIDATION_ERROR.CODE, + ); + } + } + + const validationResultText = isValidString(text, 500); + if (!validationResultText.isLessThanMaxLength) { + throw new errors.InputValidationError( + requestContext.translate( + `${LENGTH_VALIDATION_ERROR.MESSAGE} 500 characters in information`, + ), + LENGTH_VALIDATION_ERROR.CODE, + ); + } + + // Check permissions for pinning + if (pinned) { + const currentUserIsOrganizationAdmin = + currentUserAppProfile.adminFor.some((orgId) => + new mongoose.Types.ObjectId(orgId?.toString()).equals(organizationId), + ); + + if ( + !(currentUserAppProfile.isSuperAdmin || currentUserIsOrganizationAdmin) + ) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_TO_PIN.MESSAGE), + USER_NOT_AUTHORIZED_TO_PIN.CODE, + USER_NOT_AUTHORIZED_TO_PIN.PARAM, + ); + } + } + + // Create the post + const createdPost = await Post.create({ + title, + text, + pinned: pinned || false, + creatorId: userId, + organization: organizationId, + file: fileUploadResponse?._id, + }); + + if (createdPost !== null) { + await cachePosts([createdPost]); + } + + // Update organization if post is pinned + if (pinned) { + const updatedOrganization = await Organization.findOneAndUpdate( + { _id: organizationId }, + { + $push: { + pinnedPosts: createdPost._id, + }, + }, + { + new: true, + }, + ); + + await cacheOrganizations([updatedOrganization as InterfaceOrganization]); + } + + // Send response + res.status(201).json({ + post: createdPost, + }); + } catch (error) { + console.error(error); + if (error instanceof Error) { + res.status(500).json({ error: error.message }); + } else { + res.status(500).json({ + error: requestContext.translate(INTERNAL_SERVER_ERROR.MESSAGE), + }); + } + } +}; diff --git a/src/REST/controllers/mutation/index.ts b/src/REST/controllers/mutation/index.ts new file mode 100644 index 0000000000..d39f54f2c9 --- /dev/null +++ b/src/REST/controllers/mutation/index.ts @@ -0,0 +1,2 @@ +export * from "./createPost"; +export * from "./updatePost"; diff --git a/src/REST/controllers/mutation/updatePost.ts b/src/REST/controllers/mutation/updatePost.ts new file mode 100644 index 0000000000..400ff5e084 --- /dev/null +++ b/src/REST/controllers/mutation/updatePost.ts @@ -0,0 +1,246 @@ +import type { Response } from "express"; +import { Types } from "mongoose"; +import { + INTERNAL_SERVER_ERROR, + LENGTH_VALIDATION_ERROR, + PLEASE_PROVIDE_TITLE, + POST_NEEDS_TO_BE_PINNED, + POST_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, + USER_NOT_FOUND_ERROR, +} from "../../../constants"; +import { errors, requestContext } from "../../../libraries"; +import { isValidString } from "../../../libraries/validators/validateString"; +import type { + InterfaceAppUserProfile, + InterfacePost, + InterfaceUser, +} from "../../../models"; +import { AppUserProfile, Post, User } from "../../../models"; +import { cachePosts } from "../../../services/PostCache/cachePosts"; +import { findPostsInCache } from "../../../services/PostCache/findPostsInCache"; +import { findUserInCache } from "../../../services/UserCache/findUserInCache"; +import { cacheUsers } from "../../../services/UserCache/cacheUser"; +import { findAppUserProfileCache } from "../../../services/AppUserProfileCache/findAppUserProfileCache"; +import { cacheAppUserProfile } from "../../../services/AppUserProfileCache/cacheAppUserProfile"; +import type { InterfaceAuthenticatedRequest } from "../../../middleware"; +import { deleteFile, uploadFile } from "../../services/file"; + +interface InterfaceUpdatePostRequestBody { + title?: string; + text?: string; + pinned?: boolean; +} + +/** + * Controller for updating existing posts within organizations + */ + +/** + * Updates an existing post + * async + * function - updatePost + * @param req - Express request object with authenticated user + * @param res - Express response object + * @throws NotFoundError - When user or post is not found + * @throws UnauthorizedError - When user lacks permissions to update the post + * @throws InputValidationError - When title/text validation fails or pinned status requirements aren't met + * @returns Promise - Responds with updated post or error + * + * Description + * This controller handles post updates with the following features: + * - Validates user permissions (creator, organization admin, or super admin) + * - Supports file attachment updates with cleanup of old files + * - Enforces business rules for pinned posts and titles + * - Validates content length restrictions + * - Maintains cache consistency + * + * Request body expects: + * ```typescript + * { + * title?: string; + * text?: string; + * pinned?: boolean; + * } + * ``` + * + * Authorization Rules: + * - Post creator can edit their own posts + * - Organization admins can edit posts in their organizations + * - Super admins can edit any post + */ + +export const updatePost = async ( + req: InterfaceAuthenticatedRequest, + res: Response, +): Promise => { + const userId = req.userId; + const postId = req.params.id; + const { title, text, pinned }: InterfaceUpdatePostRequestBody = req.body; + + try { + // Get the current user + let currentUser: InterfaceUser | null; + const userFoundInCache = await findUserInCache([userId as string]); + currentUser = userFoundInCache[0]; + if (currentUser === null) { + currentUser = await User.findOne({ _id: userId }).lean(); + if (currentUser !== null) { + await cacheUsers([currentUser]); + } + } + + if (!currentUser) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM, + ); + } + + // Get current user's app profile + let currentUserAppProfile: InterfaceAppUserProfile | null; + const appUserProfileFoundInCache = await findAppUserProfileCache([ + currentUser.appUserProfileId?.toString(), + ]); + currentUserAppProfile = appUserProfileFoundInCache[0]; + if (currentUserAppProfile === null) { + currentUserAppProfile = await AppUserProfile.findOne({ + userId: currentUser._id, + }).lean(); + if (currentUserAppProfile !== null) { + await cacheAppUserProfile([currentUserAppProfile]); + } + } + + if (!currentUserAppProfile) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM, + ); + } + + // Get the post + let post: InterfacePost | null; + const postFoundInCache = await findPostsInCache([postId]); + post = postFoundInCache[0]; + if (post === null) { + post = await Post.findOne({ _id: postId }).populate("file").lean(); + if (post !== null) { + await cachePosts([post]); + } + } + + if (!post) { + throw new errors.NotFoundError( + requestContext.translate(POST_NOT_FOUND_ERROR.MESSAGE), + POST_NOT_FOUND_ERROR.CODE, + POST_NOT_FOUND_ERROR.PARAM, + ); + } + + // Check if the user has the right to update the post + const currentUserIsPostCreator = post.creatorId.equals(userId); + const isSuperAdmin = currentUserAppProfile.isSuperAdmin; + const isAdminOfPostOrganization = currentUserAppProfile?.adminFor.some( + (orgID) => + orgID && + new Types.ObjectId(orgID?.toString()).equals(post?.organization), + ); + + if ( + !currentUserIsPostCreator && + !isAdminOfPostOrganization && + !isSuperAdmin + ) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM, + ); + } + + // Handle file upload and cleanup + let fileId: string | undefined; + const oldFileId = post.file?._id?.toString(); + const oldObjectKey = post.file.metadata?.objectKey; + + if (req.file) { + // Upload new file + const response = await uploadFile(req, res); + fileId = response._id?.toString(); + + // Clean up old file if it exists + if (oldFileId && oldObjectKey) { + await deleteFile(oldObjectKey, oldFileId); + } + } + + // Validate title and pinned status + if (title && !post.pinned) { + throw new errors.InputValidationError( + requestContext.translate(POST_NEEDS_TO_BE_PINNED.MESSAGE), + POST_NEEDS_TO_BE_PINNED.CODE, + ); + } else if (!title && post.pinned) { + throw new errors.InputValidationError( + requestContext.translate(PLEASE_PROVIDE_TITLE.MESSAGE), + PLEASE_PROVIDE_TITLE.CODE, + ); + } + + // Validate input lengths + if (title) { + const validationResultTitle = isValidString(title, 256); + if (!validationResultTitle.isLessThanMaxLength) { + throw new errors.InputValidationError( + requestContext.translate( + `${LENGTH_VALIDATION_ERROR.MESSAGE} 256 characters in title`, + ), + LENGTH_VALIDATION_ERROR.CODE, + ); + } + } + + if (text) { + const validationResultText = isValidString(text, 500); + if (!validationResultText.isLessThanMaxLength) { + throw new errors.InputValidationError( + requestContext.translate( + `${LENGTH_VALIDATION_ERROR.MESSAGE} 500 characters in information`, + ), + LENGTH_VALIDATION_ERROR.CODE, + ); + } + } + + const updatedPost = await Post.findOneAndUpdate( + { _id: postId }, + { + ...(title && { title }), + ...(text && { text }), + ...(pinned !== undefined && { pinned }), + ...(fileId && { file: fileId }), + }, + { new: true }, + ).lean(); + + if (updatedPost !== null) { + await cachePosts([updatedPost]); + } + + res.status(200).json({ + post: updatedPost, + }); + } catch (error) { + console.error(error); + if (error instanceof Error) { + res.status(500).json({ error: error.message }); + } else { + res.status(500).json({ + error: requestContext.translate(INTERNAL_SERVER_ERROR.MESSAGE), + }); + } + } +}; diff --git a/src/REST/controllers/query/getFile.ts b/src/REST/controllers/query/getFile.ts new file mode 100644 index 0000000000..171c53ca48 --- /dev/null +++ b/src/REST/controllers/query/getFile.ts @@ -0,0 +1,40 @@ +import type { Request, Response } from "express"; +import { s3Client, BUCKET_NAME } from "../../../config/minio"; +import { GetObjectCommand } from "@aws-sdk/client-s3"; +import type { Readable } from "stream"; + +/** + * Middleware to retrieve a file from S3 storage. + * + * This function retrieves a file from an S3-compatible storage service using the provided key from the request parameters. + * If the file is found, it streams the file's content back to the client with the appropriate content type. + * If an error occurs during the retrieval, it logs the error and sends a 500 status code response. + * + * @param req - The Express request object, containing the key for the file in the parameters. + * @param res - The Express response object used to send the file back to the client. + * + * @returns A promise that resolves to void. The function either streams the file or sends an error response. + * + * @example + * ```typescript + * app.get("/file/:key*", getFile); + * ``` + */ +export const getFile = async (req: Request, res: Response): Promise => { + const key = req.params[0]; + const command = new GetObjectCommand({ + Bucket: BUCKET_NAME, + Key: key, + }); + + try { + const data = await s3Client.send(command); + const stream = data.Body as Readable; + res.setHeader("Content-Type", data.ContentType as string); + res.setHeader("Cross-Origin-Resource-Policy", "same-site"); + stream.pipe(res); + } catch (error) { + console.error("Error fetching file:", error); + res.status(500).send("Error occurred while fetching file"); + } +}; diff --git a/src/REST/controllers/query/index.ts b/src/REST/controllers/query/index.ts new file mode 100644 index 0000000000..1a94d7580d --- /dev/null +++ b/src/REST/controllers/query/index.ts @@ -0,0 +1 @@ +export * from "./getFile"; diff --git a/src/REST/routes/index.ts b/src/REST/routes/index.ts new file mode 100644 index 0000000000..54b74a44cf --- /dev/null +++ b/src/REST/routes/index.ts @@ -0,0 +1,31 @@ +// routes/fileRoutes.ts +import express from "express"; + +import { getFile } from "../controllers/query/getFile"; +import { createPost, updatePost } from "../controllers/mutation"; + +import { isAuthMiddleware } from "../../middleware"; +import { fileUpload } from "../../middleware/fileUpload"; + +const router = express.Router(); + +// Routes + +// Routes +/** + * Retrieves a file by its key. + * isAuthMiddleware - Authenticates the user. + * getFile - Handles fetching the requested file. + */ +router.get("/file/*", getFile); + +router.post("/create-post", isAuthMiddleware, fileUpload("file"), createPost); + +router.post( + "/update-post/:id", + isAuthMiddleware, + fileUpload("file"), + updatePost, +); + +export default router; diff --git a/src/REST/services/file/createFile.ts b/src/REST/services/file/createFile.ts new file mode 100644 index 0000000000..21586598a9 --- /dev/null +++ b/src/REST/services/file/createFile.ts @@ -0,0 +1,55 @@ +import { BUCKET_NAME } from "../../../config/minio"; +import { BASE_URL } from "../../../constants"; +import type { InterfaceFile } from "../../../models"; +import { File } from "../../../models"; +import type { InterfaceUploadResult } from "../minio"; + +/** + * Creates or updates a file document in the database based on the upload result. + * + * This function checks if a file with the same hash already exists. If it does, the reference count of the file is incremented. + * If not, a new file document is created and saved to the database. + * + * @param uploadResult - The result from the file upload containing the hash, object key, and hash algorithm. + * @param originalname - The original name of the uploaded file. + * @param mimetype - The MIME type of the uploaded file. + * @param size - The size of the uploaded file in bytes. + * @returns A promise that resolves to the created or updated file document. + * + * @example + * ```typescript + * const file = await createFile(uploadResult, "image.png", "image/png", 2048); + * console.log(file); + * ``` + */ +export const createFile = async ( + uploadResult: InterfaceUploadResult, + originalname: string, + mimetype: string, + size: number, +): Promise => { + const existingFile = await File.findOne({ "hash.value": uploadResult.hash }); + + if (existingFile) { + existingFile.referenceCount += 1; + await existingFile.save(); + return existingFile; + } + + const newFileDoc = await File.create({ + fileName: originalname, + mimeType: mimetype, + size: size, + hash: { + value: uploadResult.hash, + algorithm: uploadResult.hashAlgorithm, + }, + uri: `${BASE_URL}api/file/${uploadResult.objectKey}`, + metadata: { + objectKey: uploadResult.objectKey, + bucketName: BUCKET_NAME, + }, + }); + + return newFileDoc; +}; diff --git a/src/REST/services/file/deleteFile.ts b/src/REST/services/file/deleteFile.ts new file mode 100644 index 0000000000..363da6fcd9 --- /dev/null +++ b/src/REST/services/file/deleteFile.ts @@ -0,0 +1,35 @@ +import { File } from "../../../models"; +import { deleteFile as deleteFileFromBucket } from "../minio"; +import { BUCKET_NAME } from "../../../config/minio"; + +export const deleteFile = async ( + objectKey: string, + fileId: string, +): Promise<{ success: boolean; message: string }> => { + try { + const file = await File.findOne({ + _id: fileId, + "metadata.objectKey": objectKey, + }); + + if (!file) { + return { success: false, message: "File not found." }; + } + + if (file.referenceCount > 1) { + file.referenceCount -= 1; + await file.save(); + return { + success: true, + message: "File reference count decreased successfully", + }; + } + + await File.deleteOne({ _id: file.id }); + await deleteFileFromBucket(BUCKET_NAME as string, objectKey); + return { success: true, message: "File deleted successfully" }; + } catch (error) { + console.error("Error deleting file:", error); + return { success: false, message: "Error occurred while deleting file" }; + } +}; diff --git a/src/REST/services/file/index.ts b/src/REST/services/file/index.ts new file mode 100644 index 0000000000..e614f3ec1e --- /dev/null +++ b/src/REST/services/file/index.ts @@ -0,0 +1,3 @@ +export { createFile } from "./createFile"; +export { deleteFile } from "./deleteFile"; +export { uploadFile } from "./uploadFile"; diff --git a/src/REST/services/file/uploadFile.ts b/src/REST/services/file/uploadFile.ts new file mode 100644 index 0000000000..36408b8b05 --- /dev/null +++ b/src/REST/services/file/uploadFile.ts @@ -0,0 +1,79 @@ +import type { Request, Response } from "express"; + +import { uploadMedia } from "../minio"; +import { createFile } from "./createFile"; +import { BUCKET_NAME } from "../../../config/minio"; + +import { isValidMimeType } from "../../../utilities/isValidMimeType"; + +import type { InterfaceFile } from "../../../models"; +import { errors, requestContext } from "../../../libraries"; +import { + FILE_NOT_FOUND, + INTERNAL_SERVER_ERROR, + INVALID_FILE_TYPE, +} from "../../../constants"; + +export interface InterfaceUploadedFileResponse extends Partial { + objectKey: string; +} + +/** + * Handles file upload. + * @param req - The HTTP request object containing the file. + * @param res - The HTTP response object used to send the response. + * @throws Error - Throws an error if no file is uploaded or if the file type is invalid. + * @returns UploadedFileResponse - The response containing file ID and object key. + */ +export const uploadFile = async ( + req: Request, + res: Response, +): Promise => { + if (!req.file) { + res + .status(400) + .json({ error: requestContext.translate(FILE_NOT_FOUND.MESSAGE) }); + throw new errors.InputValidationError( + requestContext.translate(FILE_NOT_FOUND.MESSAGE), + FILE_NOT_FOUND.CODE, + ); + } + + const { mimetype, originalname, buffer, size } = req.file; + + if (!isValidMimeType(mimetype)) { + throw new errors.InputValidationError( + requestContext.translate(INVALID_FILE_TYPE.MESSAGE), + INVALID_FILE_TYPE.CODE, + ); + } + + try { + const contentType = { ContentType: mimetype }; + const uploadedFile = await uploadMedia( + BUCKET_NAME as string, + buffer, + originalname, + contentType, + ); + const fileDoc = await createFile( + uploadedFile, + originalname, + mimetype, + size, + ); + + return { + uri: fileDoc.uri, + _id: fileDoc._id, + visibility: fileDoc.visibility, + objectKey: fileDoc.metadata.objectKey, + }; + } catch (error) { + console.error("Error", error); + throw new errors.InternalServerError( + requestContext.translate(INTERNAL_SERVER_ERROR.MESSAGE), + INTERNAL_SERVER_ERROR.CODE, + ); + } +}; diff --git a/src/REST/services/minio/index.ts b/src/REST/services/minio/index.ts new file mode 100644 index 0000000000..e9d37beb65 --- /dev/null +++ b/src/REST/services/minio/index.ts @@ -0,0 +1,130 @@ +// Import third-party modules +import crypto from "crypto"; +import path from "path"; + +// Import AWS SDK S3 client and commands +import { + DeleteObjectCommand, + HeadObjectCommand, + PutObjectCommand, +} from "@aws-sdk/client-s3"; +import type { DeleteObjectCommandOutput } from "@aws-sdk/client-s3"; + +// Import project configuration +import { s3Client } from "../../../config/minio"; + +export interface InterfaceUploadResult { + exists: boolean; + objectKey: string; + hash: string; + hashAlgorithm: string; +} + +/** + * Uploads a media file to a specified S3 bucket, calculating its hash for naming and uniqueness. + * + * The `uploadMedia` function calculates the SHA-256 hash of the provided buffer to generate a unique object key. + * It first checks if a file with the same hash already exists in the bucket using the `HeadObjectCommand`. + * If the file does not exist, it uploads the file using the `PutObjectCommand`. It supports both image and video uploads + * by assigning appropriate prefixes to the object key. + * + * @param bucketName - The name of the S3 bucket where the file will be uploaded. + * @param buffer - The file content as a buffer. + * @param originalname - The original file name, used to determine the file extension. + * @param contentType - An object specifying the content type of the file. + * @returns A promise that resolves to an object containing the file's existence status, object key, hash, and hash algorithm. + * + * @example + * ```typescript + * const result = await uploadMedia("my-bucket", fileBuffer, "image.png", { ContentType: "image/png" }); + * console.log(result); + * ``` + */ +export const uploadMedia = async ( + bucketName: string, + buffer: Buffer, + originalname: string, + contentType: { ContentType: string }, +): Promise => { + const hash = crypto.createHash("sha256").update(buffer).digest("hex"); + const fileExtension = path.extname(originalname); + + let prefix = ""; + if (contentType.ContentType.startsWith("image/")) { + prefix = "image/"; + } else if (contentType.ContentType.startsWith("video/")) { + prefix = "video/"; + } + + const objectKey = `${prefix}${hash}${fileExtension}`; + + const headParams = { + Bucket: bucketName, + Key: objectKey, + }; + const headCommand = new HeadObjectCommand(headParams); + + try { + await s3Client.send(headCommand); + return { exists: true, objectKey, hash, hashAlgorithm: "sha256" }; + } catch (error: unknown) { + if ( + error instanceof Error && + "name" in error && + error.name === "NotFound" + ) { + const params = { + Bucket: bucketName, + Key: objectKey, + Body: buffer, + ...contentType, + }; + + try { + const command = new PutObjectCommand(params); + await s3Client.send(command); + return { exists: false, objectKey, hash, hashAlgorithm: "sha256" }; + } catch (uploadError: unknown) { + console.error("Error uploading the file:", uploadError); + throw uploadError; + } + } else { + console.error("Error checking file existence:", error); + throw error; + } + } +}; + +/** + * Deletes a file from a specified S3 bucket. + * + * The `deleteFile` function deletes an object in an S3 bucket using the `DeleteObjectCommand`. + * If an error occurs during the deletion process, it logs the error and rethrows it. + * + * @param bucketName - The name of the S3 bucket from which the file will be deleted. + * @param objectKey - The key of the object to be deleted in the S3 bucket. + * @returns A promise that resolves to the output of the `DeleteObjectCommand`. + * + * @example + * ```typescript + * const response = await deleteFile("my-bucket", "image123.png"); + * console.log(response); + * ``` + */ +export const deleteFile = async ( + bucketName: string, + objectKey: string, +): Promise => { + const params = { + Bucket: bucketName, + Key: objectKey, + }; + const command = new DeleteObjectCommand(params); + try { + const response = await s3Client.send(command); + return response; + } catch (error) { + console.error("Error deleting file:", error); + throw error; + } +}; diff --git a/src/REST/types/index.ts b/src/REST/types/index.ts new file mode 100644 index 0000000000..4725dcca52 --- /dev/null +++ b/src/REST/types/index.ts @@ -0,0 +1,9 @@ +/** + * Allowed MIME types for files. + */ +export type FileMimeType = + | "image/jpeg" + | "image/png" + | "image/gif" + | "image/webp" + | "video/mp4"; diff --git a/src/app.ts b/src/app.ts index a5571597a4..e8e0f4eb86 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,19 +6,21 @@ import { express as voyagerMiddleware } from "graphql-voyager/middleware"; import helmet from "helmet"; import i18n from "i18n"; import requestLogger from "morgan"; -import path from "path"; import { appConfig } from "./config"; import { requestContext, requestTracing, stream } from "./libraries"; -import graphqlUploadExpress from "graphql-upload/graphqlUploadExpress.mjs"; +import routes from "./REST/routes"; + +import * as enLocale from "../locales/en.json"; +import * as hiLocale from "../locales/hi.json"; +import * as zhLocale from "../locales/zh.json"; +import * as spLocale from "../locales/sp.json"; +import * as frLocale from "../locales/fr.json"; const app = express(); // Middleware for tracing requests app.use(requestTracing.middleware()); -// Initialize i18n for internationalization -app.use(i18n.init); - // Rate limiting middleware to prevent abuse const apiLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour window @@ -27,7 +29,6 @@ const apiLimiter = rateLimit({ }); app.use(apiLimiter); -// eslint-disable-next-line @typescript-eslint/no-unused-vars const corsOptions: cors.CorsOptions = { origin: (origin, next) => { if (process.env.NODE_ENV === "development") { @@ -42,17 +43,18 @@ const corsOptions: cors.CorsOptions = { } next(new Error("Unauthorized")); // Reject other origins }, + optionsSuccessStatus: 200, }; // Configure i18n settings i18n.configure({ directory: `${__dirname}/../locales`, staticCatalog: { - en: require("../locales/en.json"), - hi: require("../locales/hi.json"), - zh: require("../locales/zh.json"), - sp: require("../locales/sp.json"), - fr: require("../locales/fr.json"), + en: enLocale, + hi: hiLocale, + zh: zhLocale, + sp: spLocale, + fr: frLocale, }, queryParameter: "lang", defaultLocale: appConfig.defaultLocale, @@ -73,20 +75,11 @@ app.use( // Sanitize data to prevent MongoDB operator injection app.use(mongoSanitize()); -app.use(cors()); - -// Serve static files with Cross-Origin-Resource-Policy header set -app.use("/images", (req, res, next) => { - res.setHeader("Cross-Origin-Resource-Policy", "cross-origin"); - next(); -}); +app.use(cors(corsOptions)); // Parse JSON requests with a size limit of 50mb app.use(express.json({ limit: "50mb" })); -// Handle file uploads using graphql-upload -app.use(graphqlUploadExpress()); - // Parse URL-encoded requests with a size limit of 50mb app.use(express.urlencoded({ limit: "50mb", extended: true })); @@ -100,13 +93,11 @@ app.use( ), ); -// Serve static files for images and videos -app.use("/images", express.static(path.join(__dirname, "./../images"))); -app.use("/videos", express.static(path.join(__dirname, "./../videos"))); - // Middleware for managing request context (e.g., user session) app.use(requestContext.middleware()); +app.use("/api", routes); + // Enable GraphQL Voyager visualization in development if (process.env.NODE_ENV !== "production") { app.use("/voyager", voyagerMiddleware({ endpointUrl: "/graphql" })); diff --git a/src/config/minio/index.ts b/src/config/minio/index.ts new file mode 100644 index 0000000000..64fc4911d7 --- /dev/null +++ b/src/config/minio/index.ts @@ -0,0 +1,47 @@ +import { S3Client } from "@aws-sdk/client-s3"; + +/** + * Initializes and exports an S3 client instance using AWS SDK for connecting to MinIO storage. + * + * The `s3Client` is an instance of the AWS S3 client configured to interact with a MinIO storage service. + * The client uses custom endpoint, credentials, and region details from environment variables to + * establish the connection. It also forces path-style access to ensure compatibility with MinIO. + * + * **Environment Variables:** + * - `MINIO_ENDPOINT`: The MinIO storage endpoint URL. + * - `MINIO_ROOT_USER`: The access key ID for the MinIO instance. + * - `MINIO_ROOT_PASSWORD`: The secret access key for the MinIO instance. + * - `MINIO_BUCKET`: The default bucket name in MinIO. + * + * @example + * ```typescript + * import { s3Client } from './path/to/file'; + * + * // Example usage + * const data = await s3Client.send(new ListBucketsCommand({})); + * console.log(data.Buckets); + * ``` + * + * @returns S3Client - an instance of the AWS S3 client configured for MinIO storage. + */ +export const s3Client = new S3Client({ + endpoint: process.env.MINIO_ENDPOINT, + credentials: { + accessKeyId: process.env.MINIO_ROOT_USER as string, + secretAccessKey: process.env.MINIO_ROOT_PASSWORD as string, + }, + region: process.env.MNIO_REGION, + forcePathStyle: true, +}); + +/** + * The name of the bucket used in the MinIO storage, defined via an environment variable. + * + * @example + * ```typescript + * console.log(BUCKET_NAME); // Logs the bucket name from the environment + * ``` + * + * @returns The name of the MinIO bucket. + */ +export const BUCKET_NAME = process.env.MINIO_BUCKET; diff --git a/src/config/multer/index.ts b/src/config/multer/index.ts new file mode 100644 index 0000000000..ce13a2a9d4 --- /dev/null +++ b/src/config/multer/index.ts @@ -0,0 +1,69 @@ +import multer from "multer"; +import { + VIDEO_SIZE_LIMIT, + ALLOWED_IMAGE_TYPES, + ALLOWED_VIDEO_TYPES, + INVALID_FILE_TYPE, +} from "../../constants"; +import type { Request } from "express"; +import { errors, requestContext } from "../../libraries"; + +/** + * File filter function for multer. + * + * This function checks the MIME type of the uploaded file against allowed image and video types. + * If the file type is valid, it calls the callback with `true`. Otherwise, it calls the callback + * with an error message. + * + * @param req - The Express request object. + * @param file - The file being uploaded. + * @param cb - The callback function to indicate if the file is accepted or rejected. + * + * @example + * ```typescript + * fileFilter(req, file, cb); + * ``` + */ +export const fileFilter = ( + req: Request, + file: Express.Multer.File, + cb: multer.FileFilterCallback, +): void => { + if (ALLOWED_IMAGE_TYPES.includes(file.mimetype)) { + cb(null, true); + } else if (ALLOWED_VIDEO_TYPES.includes(file.mimetype)) { + cb(null, true); + } else { + cb( + new errors.InvalidFileTypeError( + requestContext.translate(INVALID_FILE_TYPE.MESSAGE), + INVALID_FILE_TYPE.CODE, + INVALID_FILE_TYPE.PARAM, + ), + ); + } +}; + +/** + * Multer upload configuration. + * + * This configuration sets up multer to use memory storage, applies the file filter, + * and sets a file size limit for uploads. + * + * @returns A multer instance configured for handling uploads. + * + * @example + * ```typescript + * const uploadMiddleware = upload.single("file"); + * app.post("/upload", uploadMiddleware, (req, res) => { + * res.send("File uploaded successfully!"); + * }); + * ``` + */ +export const upload = multer({ + storage: multer.memoryStorage(), + fileFilter, + limits: { + fileSize: VIDEO_SIZE_LIMIT + 2, + }, +}); diff --git a/src/constants.ts b/src/constants.ts index 992b471a65..b30914dd5d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -731,6 +731,36 @@ export const PRELOGIN_IMAGERY_FIELD_EMPTY = Object.freeze({ PARAM: "preLoginImagery.empty", }); +export const CONTENT_TYPE_SHOULD_BE_MULTIPART_FORM_DATA = Object.freeze({ + DESC: "Invalid content type. Expected multipart/form-data", + MESSAGE: "invalid.contentType", + CODE: "invalid.contentType", +}); + +export const INVALID_FILE_FIELD_NAME = Object.freeze({ + DESC: "Invalid file input field name received.", + MESSAGE: "invalid.fieldFileName", + CODE: "invalid.fieldFileName", +}); + +export const FILE_SIZE_EXCEEDED = Object.freeze({ + DESC: "File size exceeds the allowable limit", + MESSAGE: "file.sizeExceeded", + CODE: "file.sizeExceeded", +}); + +export const FILE_NOT_FOUND = Object.freeze({ + DESC: "File not found.", + MESSAGE: "file.notFound", + CODE: "file.notFound", +}); + +export const INVALID_ARGUMENT_RECEIVED = Object.freeze({ + DESC: "Invalid argument received.", + MESSAGE: "invalid.argument", + CODE: "invalid.argument", +}); + export const MINIMUM_TIMEOUT_MINUTES = 15; export const MAXIMUM_TIMEOUT_MINUTES = 60; @@ -829,3 +859,14 @@ export const DEFAULT_COMMUNITY = { name: "Palisadoes Foundation", description: "An open source application by Palisadoes Foundation volunteers", }; + +export const ALLOWED_IMAGE_TYPES = [ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", +]; +export const ALLOWED_VIDEO_TYPES = ["video/mp4", "video/mpeg"]; + +export const VIDEO_SIZE_LIMIT = 50 * 1024 * 1024; +export const IMAGE_SIZE_LIMIT = 5 * 1024 * 1024; diff --git a/src/middleware/fileUpload.ts b/src/middleware/fileUpload.ts new file mode 100644 index 0000000000..085d73d35f --- /dev/null +++ b/src/middleware/fileUpload.ts @@ -0,0 +1,74 @@ +import type { Request, Response, NextFunction, RequestHandler } from "express"; +import multer from "multer"; + +import { upload } from "../config/multer"; +import { + ALLOWED_IMAGE_TYPES, + CONTENT_TYPE_SHOULD_BE_MULTIPART_FORM_DATA, + FILE_SIZE_EXCEEDED, + IMAGE_SIZE_LIMIT, + INVALID_FILE_FIELD_NAME, + VIDEO_SIZE_LIMIT, +} from "../constants"; +import { requestContext } from "../libraries"; + +/** + * A middleware for handling optional file uploads. + * All data must be sent as multipart/form-data, but the file field is optional. + * + * @param fieldName - The name of the file field in the form + * @returns Express middleware for handling file upload + */ +export const fileUpload = (fieldName: string): RequestHandler => { + return (req: Request, res: Response, next: NextFunction): void => { + // Validate content type is multipart/form-data + const contentType = req.get("content-type"); + if (contentType && !contentType.includes("multipart/form-data")) { + res.status(400).json({ + error: requestContext.translate( + CONTENT_TYPE_SHOULD_BE_MULTIPART_FORM_DATA.MESSAGE, + ), + }); + return; + } + + // Handle file upload + upload.single(fieldName)(req, res, (err) => { + if (err instanceof multer.MulterError) { + if (err.code === "LIMIT_UNEXPECTED_FILE") { + res.status(400).json({ + error: requestContext.translate(INVALID_FILE_FIELD_NAME.MESSAGE), + }); + return; + } + res.status(400).json({ error: err.message }); + return; + } else if (err) { + res.status(500).json({ error: "File upload failed" }); + return; + } + + // If no file uploaded, continue + if (!req.file) { + next(); + return; + } + + // Validate file size if file was uploaded + const isImage = ALLOWED_IMAGE_TYPES.includes(req.file.mimetype); + const sizeLimit = isImage ? IMAGE_SIZE_LIMIT : VIDEO_SIZE_LIMIT; + + if (req.file.size > sizeLimit) { + const typeStr = isImage ? "Image" : "Video"; + const sizeMB = sizeLimit / (1024 * 1024); + res.status(400).json({ + error: requestContext.translate(FILE_SIZE_EXCEEDED.MESSAGE), + description: `${typeStr} size exceeds the limit of ${sizeMB}MB`, + }); + return; + } + + next(); + }); + }; +}; diff --git a/src/middleware/index.ts b/src/middleware/index.ts index 065f1a55bc..e4518e8e42 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -1,2 +1,3 @@ // Export everything from this module, including isAuth function export * from "./isAuth"; +export * from "./fileUpload"; diff --git a/src/middleware/isAuth.ts b/src/middleware/isAuth.ts index 7e96dcdf39..504abf1cda 100644 --- a/src/middleware/isAuth.ts +++ b/src/middleware/isAuth.ts @@ -1,7 +1,7 @@ -import type { Request } from "express"; +import type { NextFunction, Request, Response } from "express"; import jwt from "jsonwebtoken"; -import { ACCESS_TOKEN_SECRET } from "../constants"; -import { logger } from "../libraries"; +import { ACCESS_TOKEN_SECRET, UNAUTHENTICATED_ERROR } from "../constants"; +import { logger, requestContext } from "../libraries"; // This interface represents the type of data object returned by isAuth function. export interface InterfaceAuthData { @@ -58,7 +58,8 @@ export const isAuth = (request: Request): InterfaceAuthData => { authData.expired = true; return authData; } - } catch (e) { + } catch (err) { + logger.error(err); authData.expired = true; return authData; } @@ -76,3 +77,53 @@ export const isAuth = (request: Request): InterfaceAuthData => { // Return the finalized authData object return authData; }; + +// Extend the Express Request interface locally +export interface InterfaceAuthenticatedRequest extends Request { + isAuth?: boolean; + userId?: string; + tokenExpired?: boolean; +} + +/** + * Middleware for REST APIs to authenticate users based on the JWT token in the Authorization header. + * + * This middleware checks if the incoming request has a valid JWT token. It sets the authentication + * status, user ID, and token expiration status on the `req` object for downstream middleware and + * route handlers to use. + * + * @param req - The incoming request object. The JWT token is expected in the `Authorization` header. + * @param res - The response object. If authentication fails, an HTTP 401 response will be sent. + * @param next - The next middleware function in the stack. It is called if the user is authenticated. + * + * @returns Returns a 401 Unauthorized response if the user is not authenticated or the token has expired. + * + * @example + * ```typescript + * app.use("/api/protected-route", isAuthMiddleware, (req, res) => { + * if (req.isAuth) { + * res.json({ message: "This is a protected route" }); + * } + * }); + * ``` + */ +export const isAuthMiddleware = ( + req: InterfaceAuthenticatedRequest, + res: Response, + next: NextFunction, +): void => { + const authData: InterfaceAuthData = isAuth(req); + req.isAuth = authData.isAuth; + req.userId = authData.userId; + req.tokenExpired = authData.expired; + + if (!authData.isAuth) { + res.status(401).json({ + message: requestContext.translate(UNAUTHENTICATED_ERROR.MESSAGE), + expired: authData.expired, + }); + return; + } + + next(); +}; diff --git a/src/models/File.ts b/src/models/File.ts index bd8550f2dc..a7336ce08d 100644 --- a/src/models/File.ts +++ b/src/models/File.ts @@ -1,52 +1,119 @@ -import type { Types, Model } from "mongoose"; -import { Schema, model, models } from "mongoose"; +// Import third-party modules import { v4 as uuidv4 } from "uuid"; -import { createLoggingMiddleware } from "../libraries/dbLogger"; -/** - * This is an interface representing a document for a file in the database(MongoDB). - */ +import { Schema, model, models } from "mongoose"; +import type { Types, Model } from "mongoose"; + +// Interface definition for a file document export interface InterfaceFile { _id: Types.ObjectId; - name: string; - url: string | undefined; - size: number | undefined; - secret: string; - contentType: string | undefined; - status: string; + fileName: string; + mimeType: string; + size: number; + hash: { + value: string; + algorithm: string; + }; + uri: string; + referenceCount: number; + metadata: Record; // eslint-disable-line @typescript-eslint/no-explicit-any + encryption: boolean; + archived: boolean; + visibility: "PRIVATE" | "PUBLIC"; + backupStatus: string; + status: "ACTIVE" | "BLOCKED" | "DELETED"; createdAt: Date; updatedAt: Date; + archivedAt?: Date; } /** - * Mongoose schema for a file. - * Defines the structure of the file document stored in MongoDB. - * @param name - The name of the file. - * @param url - The URL where the file is stored. + * Mongoose schema for the `File` collection. + * + * This schema defines the structure for storing files in the database, including details such as + * the file name, size, hash, URI, visibility, and status. It also includes metadata, encryption, and archival details. + * The schema automatically manages `createdAt` and `updatedAt` timestamps using Mongoose's `timestamps` option. + * + * @param fileName - The name of the file (defaults to a UUID if not provided). + * @param mimeType - The MIME type of the file (e.g., `image/png`). * @param size - The size of the file in bytes. - * @param secret - A secret key associated with the file. - * @param contentType - The MIME type of the file. - * @param status - The status of the file (e.g., ACTIVE, BLOCKED, DELETED). - * @param createdAt - The date and time when the file was created. - * @param updatedAt - The date and time when the file was last updated. + * @param hash - An object containing the hash value and the algorithm used for the hash. + * @param uri - The URI of the file location. + * @param referenceCount - The number of references to the file (defaults to 1). + * @param metadata - An object containing additional metadata for the file. + * @param encryption - Indicates whether the file is encrypted. + * @param archived - Indicates whether the file is archived. + * @param visibility - File visibility (`PRIVATE` or `PUBLIC`). + * @param backupStatus - The status of the file's backup. + * @param status - The current status of the file (`ACTIVE`, `BLOCKED`, or `DELETED`). + * @param createdAt - The timestamp when the file was created. + * @param updatedAt - The timestamp when the file was last updated. + * @param archivedAt - The timestamp when the file was archived (if applicable). + * + * @example + * ```typescript + * const newFile = new File({ + * fileName: "example.png", + * mimeType: "image/png", + * size: 2048, + * hash: { value: "abc123", algorithm: "sha256" }, + * uri: "/path/to/file", + * }); + * await newFile.save(); + * ``` */ -const fileSchema = new Schema( +const fileSchema = new Schema( { - name: { + fileName: { type: String, required: true, - default: uuidv4(), // Generates a unique identifier for the name by default + default: uuidv4(), }, - url: { + mimeType: { type: String, + required: true, }, size: { type: Number, + required: true, }, - secret: { + hash: { + value: { + type: String, + required: true, + }, + algorithm: { + type: String, + required: true, + }, + }, + uri: { type: String, required: true, }, - contentType: { + referenceCount: { + type: Number, + default: 1, + }, + metadata: { + type: Schema.Types.Mixed, + }, + archivedAt: { + type: Date, + }, + encryption: { + type: Boolean, + default: false, + }, + archived: { + type: Boolean, + default: false, + }, + visibility: { + type: String, + enum: ["PRIVATE", "PUBLIC"], + default: "PUBLIC", + }, + backupStatus: { type: String, }, status: { @@ -57,26 +124,27 @@ const fileSchema = new Schema( }, }, { - timestamps: true, // Automatically adds `createdAt` and `updatedAt` fields + timestamps: true, }, ); -// Add logging middleware for fileSchema -createLoggingMiddleware(fileSchema, "File"); - /** - * Function to retrieve or create the Mongoose model for the File. - * This is necessary to avoid the OverwriteModelError during testing. - * @returns The Mongoose model for the File. + * Creates and exports the Mongoose `File` model. + * + * The `File` model interacts with the `File` collection in the MongoDB database. + * It allows you to create, retrieve, update, and delete file documents based on the defined schema. + * + * @returns The Mongoose model for the `File` collection. + * + * @example + * ```typescript + * const file = await File.findById(fileId); + * console.log(file); + * ``` */ const fileModel = (): Model => model("File", fileSchema); -/** - * The Mongoose model for the File. - * If the model already exists (e.g., during testing), it uses the existing model. - * Otherwise, it creates a new model. - */ export const File = (models.File || fileModel()) as ReturnType< typeof fileModel >; diff --git a/src/models/Post.ts b/src/models/Post.ts index f3bd86a624..209db8ba62 100644 --- a/src/models/Post.ts +++ b/src/models/Post.ts @@ -3,6 +3,7 @@ import { Schema, model, models } from "mongoose"; import mongoosePaginate from "mongoose-paginate-v2"; import type { InterfaceOrganization } from "./Organization"; import type { InterfaceUser } from "./User"; +import type { InterfaceFile } from "./File"; import { createLoggingMiddleware } from "../libraries/dbLogger"; /** @@ -13,7 +14,7 @@ export interface InterfacePost { commentCount: number; createdAt: Date; creatorId: PopulatedDoc; - imageUrl: string | undefined | null; + file: PopulatedDoc | null; likeCount: number; likedBy: PopulatedDoc[]; organization: PopulatedDoc; @@ -22,7 +23,6 @@ export interface InterfacePost { text: string; title: string | undefined; updatedAt: Date; - videoUrl: string | undefined | null; } /** @@ -54,13 +54,9 @@ const postSchema = new Schema( enum: ["ACTIVE", "BLOCKED", "DELETED"], default: "ACTIVE", }, - imageUrl: { - type: String, - required: false, - }, - videoUrl: { - type: String, - required: false, + file: { + type: Schema.Types.ObjectId, + ref: "File", }, creatorId: { type: Schema.Types.ObjectId, diff --git a/src/resolvers/Mutation/index.ts b/src/resolvers/Mutation/index.ts index 1e97d33522..a02bbf2c92 100644 --- a/src/resolvers/Mutation/index.ts +++ b/src/resolvers/Mutation/index.ts @@ -109,7 +109,6 @@ import { updateFundraisingCampaign } from "./updateFundraisingCampaign"; import { updateLanguage } from "./updateLanguage"; import { updateOrganization } from "./updateOrganization"; import { updatePluginStatus } from "./updatePluginStatus"; -import { updatePost } from "./updatePost"; import { updateSessionTimeout } from "./updateSessionTimeout"; import { updateUserPassword } from "./updateUserPassword"; import { updateUserProfile } from "./updateUserProfile"; @@ -235,7 +234,6 @@ export const Mutation: MutationResolvers = { updateUserPassword, updateUserTag, updateVolunteerMembership, - updatePost, updateAdvertisement, updateFundraisingCampaign, updateFundraisingCampaignPledge, diff --git a/src/resolvers/Mutation/removePost.ts b/src/resolvers/Mutation/removePost.ts index 96669acd6d..cfc22f7809 100644 --- a/src/resolvers/Mutation/removePost.ts +++ b/src/resolvers/Mutation/removePost.ts @@ -18,8 +18,7 @@ import { findPostsInCache } from "../../services/PostCache/findPostsInCache"; import { cacheUsers } from "../../services/UserCache/cacheUser"; import { findUserInCache } from "../../services/UserCache/findUserInCache"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; -import { deletePreviousImage as deleteImage } from "../../utilities/encodedImageStorage/deletePreviousImage"; -import { deletePreviousVideo as deleteVideo } from "../../utilities/encodedVideoStorage/deletePreviousVideo"; +import { deletePreviousFile as deleteFile } from "../../utilities/encodedImageStorage/deletePreviousFile"; import { findAppUserProfileCache } from "../../services/AppUserProfileCache/findAppUserProfileCache"; import { cacheAppUserProfile } from "../../services/AppUserProfileCache/cacheAppUserProfile"; /** @@ -125,18 +124,18 @@ export const removePost: MutationResolvers["removePost"] = async ( // Deletes the post. const deletedPost = await Post.findOneAndDelete({ _id: args.id, - }); + }) + .populate({ path: "file", select: "_id metadata.objectKey" }) + .lean(); await deletePostFromCache(args.id); - //deletes the image in post - if (deletedPost?.imageUrl) { - await deleteImage(deletedPost?.imageUrl); - } - - //deletes the video in post - if (deletedPost?.videoUrl) { - await deleteVideo(deletedPost?.videoUrl); + // Deletes the media in the post + if (deletedPost?.file) { + await deleteFile( + deletedPost.file._id.toString(), + deletedPost.file.metadata.objectKey, + ); } // Removes the post from the organization, doesn't fail if the post wasn't pinned diff --git a/src/resolvers/Mutation/updatePost.ts b/src/resolvers/Mutation/updatePost.ts deleted file mode 100644 index 82d3669a20..0000000000 --- a/src/resolvers/Mutation/updatePost.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { Types } from "mongoose"; -import { - LENGTH_VALIDATION_ERROR, - PLEASE_PROVIDE_TITLE, - POST_NEEDS_TO_BE_PINNED, - POST_NOT_FOUND_ERROR, - USER_NOT_AUTHORIZED_ERROR, - USER_NOT_FOUND_ERROR, -} from "../../constants"; -import { errors, requestContext } from "../../libraries"; -import { isValidString } from "../../libraries/validators/validateString"; -import type { - InterfaceAppUserProfile, - InterfacePost, - InterfaceUser, -} from "../../models"; -import { AppUserProfile, Post, User } from "../../models"; -import { cachePosts } from "../../services/PostCache/cachePosts"; -import { findPostsInCache } from "../../services/PostCache/findPostsInCache"; -import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; -import { uploadEncodedImage } from "../../utilities/encodedImageStorage/uploadEncodedImage"; -import { uploadEncodedVideo } from "../../utilities/encodedVideoStorage/uploadEncodedVideo"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; -import { findAppUserProfileCache } from "../../services/AppUserProfileCache/findAppUserProfileCache"; -import { cacheAppUserProfile } from "../../services/AppUserProfileCache/cacheAppUserProfile"; - -/** - * Updates a post with new details, including handling image and video URL uploads and validating input fields. - * - * This function updates an existing post based on the provided input. It retrieves and validates the current user and their app profile, checks if the user has the necessary permissions, handles media file uploads, and performs input validation before updating the post in the database. The function returns the updated post after applying changes. - * - * @param _parent - This parameter represents the parent resolver in the GraphQL schema and is not used in this function. - * @param args - The arguments passed to the GraphQL mutation, including the post's `id` and data to update, such as `title`, `text`, `imageUrl`, and `videoUrl`. - * @param context - Provides contextual information, including the current user's ID. This is used to authenticate and authorize the request. - * - * @returns The updated post with all its fields. - */ -export const updatePost: MutationResolvers["updatePost"] = async ( - _parent, - args, - context, -) => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); - } - } - - if (!currentUser) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, - ); - } - - let currentUserAppProfile: InterfaceAppUserProfile | null; - const appUserProfileFoundInCache = await findAppUserProfileCache([ - currentUser.appUserProfileId?.toString(), - ]); - currentUserAppProfile = appUserProfileFoundInCache[0]; - if (currentUserAppProfile === null) { - currentUserAppProfile = await AppUserProfile.findOne({ - userId: currentUser._id, - }).lean(); - if (currentUserAppProfile !== null) { - await cacheAppUserProfile([currentUserAppProfile]); - } - } - if (!currentUserAppProfile) { - throw new errors.UnauthorizedError( - requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), - USER_NOT_AUTHORIZED_ERROR.CODE, - USER_NOT_AUTHORIZED_ERROR.PARAM, - ); - } - - let post: InterfacePost | null; - - const postFoundInCache = await findPostsInCache([args.id]); - - post = postFoundInCache[0]; - - if (postFoundInCache[0] === null) { - post = await Post.findOne({ - _id: args.id, - }).lean(); - if (post !== null) { - await cachePosts([post]); - } - } - - // Check if the post exists - if (!post) { - throw new errors.NotFoundError( - requestContext.translate(POST_NOT_FOUND_ERROR.MESSAGE), - POST_NOT_FOUND_ERROR.CODE, - POST_NOT_FOUND_ERROR.PARAM, - ); - } - - // Check if the user has the right to update the post - const currentUserIsPostCreator = post.creatorId.equals(context.userId); - const isSuperAdmin = currentUserAppProfile.isSuperAdmin; - const isAdminOfPostOrganization = currentUserAppProfile?.adminFor.some( - (orgID) => - orgID && new Types.ObjectId(orgID?.toString()).equals(post?.organization), - ); - - // checks if current user is an creator of the post with _id === args.id - if ( - !currentUserIsPostCreator && - !isAdminOfPostOrganization && - !isSuperAdmin - ) { - throw new errors.UnauthorizedError( - requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), - USER_NOT_AUTHORIZED_ERROR.CODE, - USER_NOT_AUTHORIZED_ERROR.PARAM, - ); - } - - // Handle image and video URL uploads - if (args.data?.imageUrl && args.data?.imageUrl !== null) { - args.data.imageUrl = await uploadEncodedImage( - args.data.imageUrl, - post.imageUrl, - ); - } - - if (args.data?.videoUrl && args.data?.videoUrl !== null) { - args.data.videoUrl = await uploadEncodedVideo( - args.data.videoUrl, - post.videoUrl, - ); - } - - // Validate title and pinned status - if (args.data?.title && !post.pinned) { - throw new errors.InputValidationError( - requestContext.translate(POST_NEEDS_TO_BE_PINNED.MESSAGE), - POST_NEEDS_TO_BE_PINNED.CODE, - ); - } else if (!args.data?.title && post.pinned) { - throw new errors.InputValidationError( - requestContext.translate(PLEASE_PROVIDE_TITLE.MESSAGE), - PLEASE_PROVIDE_TITLE.CODE, - ); - } - - // Validate input lengths - const validationResultTitle = isValidString(args.data?.title ?? "", 256); - const validationResultText = isValidString(args.data?.text ?? "", 500); - if (!validationResultTitle.isLessThanMaxLength) { - throw new errors.InputValidationError( - requestContext.translate( - `${LENGTH_VALIDATION_ERROR.MESSAGE} 256 characters in title`, - ), - LENGTH_VALIDATION_ERROR.CODE, - ); - } - if (!validationResultText.isLessThanMaxLength) { - throw new errors.InputValidationError( - requestContext.translate( - `${LENGTH_VALIDATION_ERROR.MESSAGE} 500 characters in information`, - ), - LENGTH_VALIDATION_ERROR.CODE, - ); - } - - // Update the post in the database - const updatedPost = await Post.findOneAndUpdate( - { - _id: args.id, - }, - { - ...(args.data as Record), - }, - { - new: true, - }, - ).lean(); - - if (updatedPost !== null) { - await cachePosts([updatedPost]); - } - - return updatedPost as InterfacePost; -}; diff --git a/src/resolvers/Mutation/updateUserProfile.ts b/src/resolvers/Mutation/updateUserProfile.ts index ee8c0e36ac..62c1e602e2 100644 --- a/src/resolvers/Mutation/updateUserProfile.ts +++ b/src/resolvers/Mutation/updateUserProfile.ts @@ -142,7 +142,7 @@ export const updateUserProfile: MutationResolvers["updateUserProfile"] = async ( }, ).lean(); if (updatedUser != null) { - await deleteUserFromCache(updatedUser?._id.toString() || ""); + await deleteUserFromCache(updatedUser?._id.toString()); await cacheUsers([updatedUser]); } @@ -159,11 +159,6 @@ export const updateUserProfile: MutationResolvers["updateUserProfile"] = async ( ); } - if (updatedUser != null) { - updatedUser.image = updatedUser?.image - ? `${context.apiRootUrl}${updatedUser?.image}` - : null; - } if (args.data == undefined) updatedUser = null; return updatedUser ?? ({} as InterfaceUser); diff --git a/src/resolvers/Organization/image.ts b/src/resolvers/Organization/image.ts index b94afa9975..fed5e163b6 100644 --- a/src/resolvers/Organization/image.ts +++ b/src/resolvers/Organization/image.ts @@ -12,13 +12,9 @@ import type { OrganizationResolvers } from "../../types/generatedGraphQLTypes"; * @see OrganizationResolvers - The type definition for the resolvers of the Organization fields. * */ -export const image: OrganizationResolvers["image"] = ( - parent, - _args, - context, -) => { +export const image: OrganizationResolvers["image"] = (parent) => { if (parent.image) { - return `${context.apiRootUrl}${parent.image}`; + return parent.image; } return null; }; diff --git a/src/resolvers/Organization/posts.ts b/src/resolvers/Organization/posts.ts index 7aadc5f786..9f3b37334f 100644 --- a/src/resolvers/Organization/posts.ts +++ b/src/resolvers/Organization/posts.ts @@ -36,11 +36,7 @@ import { MAXIMUM_FETCH_LIMIT } from "../../constants"; * @see OrganizationResolvers - The type definition for the resolvers of the Organization fields. * */ -export const posts: OrganizationResolvers["posts"] = async ( - parent, - args, - context, -) => { +export const posts: OrganizationResolvers["posts"] = async (parent, args) => { const parseGraphQLConnectionArgumentsResult = await parseGraphQLConnectionArguments({ args, @@ -79,10 +75,13 @@ export const posts: OrganizationResolvers["posts"] = async ( }) .sort(sort) .limit(parsedArgs.limit) - .populate({ - path: "likedBy", - select: "image firstName lastName", - }) + .populate([ + { + path: "likedBy", + select: "image firstName lastName", + }, + { path: "file" }, + ]) .lean() .exec(), @@ -92,21 +91,12 @@ export const posts: OrganizationResolvers["posts"] = async ( .countDocuments() .exec(), ]); - const posts = objectList.map((post: InterfacePost) => ({ - ...post, - imageUrl: post.imageUrl - ? new URL(post.imageUrl, context.apiRootUrl).toString() - : null, - videoUrl: post.videoUrl - ? new URL(post.videoUrl, context.apiRootUrl).toString() - : null, - })); return transformToDefaultGraphQLConnection< ParsedCursor, InterfacePost, InterfacePost >({ - objectList: posts, + objectList, parsedArgs, totalCount, }); diff --git a/src/resolvers/Query/checkAuth.ts b/src/resolvers/Query/checkAuth.ts index 09a0c6a7de..1254fdeb59 100644 --- a/src/resolvers/Query/checkAuth.ts +++ b/src/resolvers/Query/checkAuth.ts @@ -37,11 +37,5 @@ export const checkAuth: QueryResolvers["checkAuth"] = async ( ); } - return { - ...currentUser, - image: currentUser.image - ? `${context.apiRootUrl}${currentUser.image}` - : null, - organizationsBlockedBy: [], - }; + return currentUser; }; diff --git a/src/resolvers/Query/organizationsMemberConnection.ts b/src/resolvers/Query/organizationsMemberConnection.ts index a49434df5e..7ccc1ea805 100644 --- a/src/resolvers/Query/organizationsMemberConnection.ts +++ b/src/resolvers/Query/organizationsMemberConnection.ts @@ -17,7 +17,7 @@ import { getWhere } from "./helperFunctions/getWhere"; * learn more about Connection {@link https://relay.dev/graphql/connections.htm | here}. */ export const organizationsMemberConnection: QueryResolvers["organizationsMemberConnection"] = - async (_parent, args, context) => { + async (_parent, args) => { const where = getWhere(args.where); const sort = getSort(args.orderBy); @@ -130,7 +130,7 @@ export const organizationsMemberConnection: QueryResolvers["organizationsMemberC employmentStatus: user.employmentStatus, firstName: user.firstName, gender: user.gender, - image: user.image ? `${context.apiRootUrl}${user.image}` : null, + image: user.image ?? null, joinedOrganizations: user.joinedOrganizations, lastName: user.lastName, maritalStatus: user.maritalStatus, @@ -165,7 +165,7 @@ export const organizationsMemberConnection: QueryResolvers["organizationsMemberC employmentStatus: user.employmentStatus, firstName: user.firstName, gender: user.gender, - image: user.image ? `${context.apiRootUrl}${user.image}` : null, + image: user.image ?? null, joinedOrganizations: user.joinedOrganizations, lastName: user.lastName, maritalStatus: user.maritalStatus, diff --git a/src/resolvers/Query/post.ts b/src/resolvers/Query/post.ts index 258c591eb9..9eb82f7453 100644 --- a/src/resolvers/Query/post.ts +++ b/src/resolvers/Query/post.ts @@ -8,10 +8,10 @@ import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; * @param args - An object that contains `id` of the Post. * @returns An object `post`. If the `appLanguageCode` field not found then it throws a `NotFoundError` error. */ -export const post: QueryResolvers["post"] = async (_parent, args, context) => { +export const post: QueryResolvers["post"] = async (_parent, args) => { const post = await Post.findOne({ _id: args.id }) .populate("organization") - .populate("likedBy") + .populate(["likedBy", "file"]) .lean(); if (!post) { @@ -21,12 +21,6 @@ export const post: QueryResolvers["post"] = async (_parent, args, context) => { POST_NOT_FOUND_ERROR.PARAM, ); } - post.imageUrl = post.imageUrl - ? `${context.apiRootUrl}${post.imageUrl}` - : null; - post.videoUrl = post.videoUrl - ? `${context.apiRootUrl}${post.videoUrl}` - : null; return post; }; diff --git a/src/resolvers/Query/user.ts b/src/resolvers/Query/user.ts index c0716be0c3..dc6233a011 100644 --- a/src/resolvers/Query/user.ts +++ b/src/resolvers/Query/user.ts @@ -43,7 +43,6 @@ export const user: QueryResolvers["user"] = async (_parent, args, context) => { return { user: { ...user, - image: user?.image ? `${context.apiRootUrl}${user.image}` : null, organizationsBlockedBy: [], }, appUserProfile: userAppProfile, diff --git a/src/typeDefs/enums.ts b/src/typeDefs/enums.ts index f08f9e06bd..e8e294b0ab 100644 --- a/src/typeDefs/enums.ts +++ b/src/typeDefs/enums.ts @@ -218,6 +218,11 @@ export const enums = gql` MENU } + enum FileVisibility { + PRIVATE + PUBLIC + } + enum ItemType { Regular Note diff --git a/src/typeDefs/types.ts b/src/typeDefs/types.ts index 5e4ab332dd..25b6f12b0f 100644 --- a/src/typeDefs/types.ts +++ b/src/typeDefs/types.ts @@ -335,6 +335,30 @@ export const types = gql` updatedAt: DateTime! } + type File { + _id: ID! + fileName: String! + mimeType: String! + size: Int! + hash: Hash! + uri: String! + referenceCount: Int! + metadata: FileMetadata! + encryption: Boolean! + archived: Boolean! + visibility: FileVisibility! + backupStatus: String! + status: Status! + createdAt: DateTime! + updatedAt: DateTime! + archivedAt: DateTime + } + + type FileMetadata { + objectKey: String! + bucketName: String! + } + type Fund { _id: ID! organizationId: ID! @@ -381,6 +405,11 @@ export const types = gql` admins: [User!]! } + type Hash { + value: String! + algorithm: String! + } + type Language { _id: ID! en: String! @@ -544,8 +573,7 @@ export const types = gql` createdAt: DateTime! creator: User updatedAt: DateTime! - imageUrl: URL - videoUrl: URL + file: File organization: Organization! likedBy: [User] comments: [Comment] @@ -623,6 +651,7 @@ export const types = gql` firstName: String! gender: Gender image: String + file: File joinedOrganizations: [Organization] lastName: String! maritalStatus: MaritalStatus diff --git a/src/types/generatedGraphQLTypes.ts b/src/types/generatedGraphQLTypes.ts index 31761aa2fd..f3421b206c 100644 --- a/src/types/generatedGraphQLTypes.ts +++ b/src/types/generatedGraphQLTypes.ts @@ -954,6 +954,36 @@ export type FieldError = { path: Array; }; +export type File = { + __typename?: 'File'; + _id: Scalars['ID']['output']; + archived: Scalars['Boolean']['output']; + archivedAt?: Maybe; + backupStatus: Scalars['String']['output']; + createdAt: Scalars['DateTime']['output']; + encryption: Scalars['Boolean']['output']; + fileName: Scalars['String']['output']; + hash: Hash; + metadata: FileMetadata; + mimeType: Scalars['String']['output']; + referenceCount: Scalars['Int']['output']; + size: Scalars['Int']['output']; + status: Status; + updatedAt: Scalars['DateTime']['output']; + uri: Scalars['String']['output']; + visibility: FileVisibility; +}; + +export type FileMetadata = { + __typename?: 'FileMetadata'; + bucketName: Scalars['String']['output']; + objectKey: Scalars['String']['output']; +}; + +export type FileVisibility = + | 'PRIVATE' + | 'PUBLIC'; + export type ForgotPasswordData = { newPassword: Scalars['String']['input']; otpToken: Scalars['String']['input']; @@ -1059,6 +1089,12 @@ export type Group = { updatedAt: Scalars['DateTime']['output']; }; +export type Hash = { + __typename?: 'Hash'; + algorithm: Scalars['String']['output']; + value: Scalars['String']['output']; +}; + export type HoursHistory = { __typename?: 'HoursHistory'; date: Scalars['Date']['output']; @@ -2182,7 +2218,7 @@ export type Post = { comments?: Maybe>>; createdAt: Scalars['DateTime']['output']; creator?: Maybe; - imageUrl?: Maybe; + file?: Maybe; likeCount?: Maybe; likedBy?: Maybe>>; organization: Organization; @@ -2190,7 +2226,6 @@ export type Post = { text: Scalars['String']['output']; title?: Maybe; updatedAt: Scalars['DateTime']['output']; - videoUrl?: Maybe; }; export type PostEdge = { @@ -2285,7 +2320,6 @@ export type Query = { customDataByOrganization: Array; customFieldsByOrganization?: Maybe>>; event?: Maybe; - eventVolunteersByEvent?: Maybe>>; eventsAttendedByUser?: Maybe>>; eventsByOrganization?: Maybe>>; eventsByOrganizationConnection: Array; @@ -2419,16 +2453,13 @@ export type QueryEventArgs = { id: Scalars['ID']['input']; }; -export type QueryEventVolunteersByEventArgs = { - id: Scalars['ID']['input']; -}; - export type QueryEventsAttendedByUserArgs = { id?: InputMaybe; orderBy?: InputMaybe; }; + export type QueryEventsByOrganizationArgs = { id?: InputMaybe; orderBy?: InputMaybe; @@ -2959,6 +2990,7 @@ export type User = { employmentStatus?: Maybe; eventAdmin?: Maybe>>; eventsAttended?: Maybe>>; + file?: Maybe; firstName: Scalars['String']['output']; gender?: Maybe; identifier: Scalars['Int']['output']; @@ -3489,6 +3521,9 @@ export type ResolversTypes = { Feedback: ResolverTypeWrapper; FeedbackInput: FeedbackInput; FieldError: ResolverTypeWrapper['FieldError']>; + File: ResolverTypeWrapper; + FileMetadata: ResolverTypeWrapper; + FileVisibility: FileVisibility; Float: ResolverTypeWrapper; ForgotPasswordData: ForgotPasswordData; Frequency: Frequency; @@ -3502,6 +3537,7 @@ export type ResolversTypes = { FundraisingCampaignPledge: ResolverTypeWrapper; Gender: Gender; Group: ResolverTypeWrapper; + Hash: ResolverTypeWrapper; HoursHistory: ResolverTypeWrapper; ID: ResolverTypeWrapper; Int: ResolverTypeWrapper; @@ -3708,6 +3744,8 @@ export type ResolversParentTypes = { Feedback: InterfaceFeedbackModel; FeedbackInput: FeedbackInput; FieldError: ResolversInterfaceTypes['FieldError']; + File: File; + FileMetadata: FileMetadata; Float: Scalars['Float']['output']; ForgotPasswordData: ForgotPasswordData; Fund: InterfaceFundModel; @@ -3718,6 +3756,7 @@ export type ResolversParentTypes = { FundraisingCampaign: InterfaceFundraisingCampaignModel; FundraisingCampaignPledge: InterfaceFundraisingCampaignPledgesModel; Group: InterfaceGroupModel; + Hash: Hash; HoursHistory: HoursHistory; ID: Scalars['ID']['output']; Int: Scalars['Int']['output']; @@ -4278,6 +4317,32 @@ export type FieldErrorResolvers, ParentType, ContextType>; }; +export type FileResolvers = { + _id?: Resolver; + archived?: Resolver; + archivedAt?: Resolver, ParentType, ContextType>; + backupStatus?: Resolver; + createdAt?: Resolver; + encryption?: Resolver; + fileName?: Resolver; + hash?: Resolver; + metadata?: Resolver; + mimeType?: Resolver; + referenceCount?: Resolver; + size?: Resolver; + status?: Resolver; + updatedAt?: Resolver; + uri?: Resolver; + visibility?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type FileMetadataResolvers = { + bucketName?: Resolver; + objectKey?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type FundResolvers = { _id?: Resolver; campaigns?: Resolver>>, ParentType, ContextType>; @@ -4330,6 +4395,12 @@ export type GroupResolvers; }; +export type HashResolvers = { + algorithm?: Resolver; + value?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type HoursHistoryResolvers = { date?: Resolver; hours?: Resolver; @@ -4661,7 +4732,7 @@ export type PostResolvers>>, ParentType, ContextType>; createdAt?: Resolver; creator?: Resolver, ParentType, ContextType>; - imageUrl?: Resolver, ParentType, ContextType>; + file?: Resolver, ParentType, ContextType>; likeCount?: Resolver, ParentType, ContextType>; likedBy?: Resolver>>, ParentType, ContextType>; organization?: Resolver; @@ -4669,7 +4740,6 @@ export type PostResolvers; title?: Resolver, ParentType, ContextType>; updatedAt?: Resolver; - videoUrl?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -4708,7 +4778,6 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; customFieldsByOrganization?: Resolver>>, ParentType, ContextType, RequireFields>; event?: Resolver, ParentType, ContextType, RequireFields>; - eventVolunteersByEvent?: Resolver>>, ParentType, ContextType, RequireFields>; eventsAttendedByUser?: Resolver>>, ParentType, ContextType, Partial>; eventsByOrganization?: Resolver>>, ParentType, ContextType, Partial>; eventsByOrganizationConnection?: Resolver, ParentType, ContextType, Partial>; @@ -4835,6 +4904,7 @@ export type UserResolvers, ParentType, ContextType>; eventAdmin?: Resolver>>, ParentType, ContextType>; eventsAttended?: Resolver>>, ParentType, ContextType>; + file?: Resolver, ParentType, ContextType>; firstName?: Resolver; gender?: Resolver, ParentType, ContextType>; identifier?: Resolver; @@ -5020,10 +5090,13 @@ export type Resolvers = { ExtendSession?: ExtendSessionResolvers; Feedback?: FeedbackResolvers; FieldError?: FieldErrorResolvers; + File?: FileResolvers; + FileMetadata?: FileMetadataResolvers; Fund?: FundResolvers; FundraisingCampaign?: FundraisingCampaignResolvers; FundraisingCampaignPledge?: FundraisingCampaignPledgeResolvers; Group?: GroupResolvers; + Hash?: HashResolvers; HoursHistory?: HoursHistoryResolvers; InvalidCursor?: InvalidCursorResolvers; JSON?: GraphQLScalarType; diff --git a/src/utilities/encodedImageStorage/deletePreviousFile.ts b/src/utilities/encodedImageStorage/deletePreviousFile.ts new file mode 100644 index 0000000000..defeff0e57 --- /dev/null +++ b/src/utilities/encodedImageStorage/deletePreviousFile.ts @@ -0,0 +1,38 @@ +import { File } from "../../models"; +import { deleteFile } from "../../REST/services/minio"; +import { BUCKET_NAME } from "../../config/minio"; + +/** + * Deletes a file from the storage and database if its reference count is 1. + * Otherwise, decrements the reference count in the database by 1. + * + * @param fileId - The ID of the file to be deleted or updated. + * @param objectKey - The object key in the storage bucket associated with the file. + * @returns A promise that resolves when the file is either deleted or its reference count is updated. + */ +export const deletePreviousFile = async ( + fileId: string, + objectKey: string, +): Promise => { + const file = await File.findOne({ + _id: fileId, + }); + + if (file?.referenceCount === 1) { + await deleteFile(BUCKET_NAME as string, objectKey); + await File.deleteOne({ + _id: fileId, + }); + } else { + await File.findOneAndUpdate( + { + _id: fileId, + }, + { + $inc: { + referenceCount: -1, + }, + }, + ); + } +}; diff --git a/src/utilities/isValidMimeType.ts b/src/utilities/isValidMimeType.ts new file mode 100644 index 0000000000..1a121b93f8 --- /dev/null +++ b/src/utilities/isValidMimeType.ts @@ -0,0 +1,17 @@ +import type { FileMimeType } from "../REST/types"; + +/** + * Checks if the provided mimetype is valid. + * @param mimetype - The mimetype to check. + * @returns True if the mimetype is valid, false otherwise. + */ +export const isValidMimeType = (mimetype: string): mimetype is FileMimeType => { + const allowedMimeTypes: FileMimeType[] = [ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "video/mp4", + ]; + return allowedMimeTypes.includes(mimetype as FileMimeType); +}; diff --git a/tests/helpers/minio.ts b/tests/helpers/minio.ts new file mode 100644 index 0000000000..e19f51b809 --- /dev/null +++ b/tests/helpers/minio.ts @@ -0,0 +1,49 @@ +import { S3Client, ListBucketsCommand } from "@aws-sdk/client-s3"; +import { ConnectionTimeoutError } from "redis"; + +/** + * Creates an S3 client configured for testing with MinIO + */ +const createTestS3Client = (): S3Client => { + return new S3Client({ + endpoint: process.env.MINIO_ENDPOINT || "http://localhost:9000", + credentials: { + accessKeyId: process.env.MINIO_ROOT_USER || "minioadmin", + secretAccessKey: process.env.MINIO_ROOT_PASSWORD || "minioadmin", + }, + region: "us-east-1", + forcePathStyle: true, + requestHandler: { + connectionTimeout: 3000, + socketTimeout: 3000, + }, + }); +}; + +/** + * Checks if the MinIO server is running and accessible + */ +export const isMinioRunning = async (): Promise => { + const s3Client = createTestS3Client(); + try { + await s3Client.send(new ListBucketsCommand({})); + return true; + } catch (error: unknown) { + if ( + (error instanceof ConnectionTimeoutError && + error.name === "ConnectTimeoutError") || + (error instanceof Error && error.name === "NetworkError") || + (error instanceof Error && + (error as { code?: string }).code === "ECONNREFUSED") + ) { + console.warn( + "\x1b[33m%s\x1b[0m", + "⚠️ MinIO server is not running. Some tests will be skipped.\n" + + "To run all tests, start the MinIO server first.", + ); + return false; + } + // If it's a different kind of error (e.g., authentication), we still consider the server as running + return true; + } +}; diff --git a/tests/helpers/posts.ts b/tests/helpers/posts.ts index c17a7c2752..38e97ca095 100644 --- a/tests/helpers/posts.ts +++ b/tests/helpers/posts.ts @@ -1,7 +1,7 @@ import type { Document } from "mongoose"; import { nanoid } from "nanoid"; import type { InterfaceComment, InterfacePost } from "../../src/models"; -import { Comment, Organization, Post } from "../../src/models"; +import { Comment, Organization, Post, File } from "../../src/models"; import type { TestOrganizationType, TestUserType } from "./userAndOrg"; import { createTestUserAndOrganization } from "./userAndOrg"; @@ -119,19 +119,33 @@ export const createSinglePostwithComment = async ( return [testPost, testComment]; }; -export const createTestSinglePost = async ( +export const createTestPostWithMedia = async ( userId: string, organizationId: string, pinned = false, ): Promise => { + const testFile = await File.create({ + fileName: `test-file-${nanoid()}.jpg`, + mimeType: "image/jpeg", + size: 1024, + hash: { + value: "66465102d50336a0610af4ae66d531cc", + algorithm: "sha256", + }, + uri: "https://example.com/test-file.jpg", + metadata: { + description: "Test file for post", + objectKey: "test-file-object-key", + }, + }); + const testPost = await Post.create({ text: `text${nanoid().toLowerCase()}`, title: `title${nanoid()}`, - imageUrl: `imageUrl${nanoid()}`, - videoUrl: `videoUrl${nanoid()}`, creatorId: userId, organization: organizationId, pinned, + file: testFile, }); return testPost; }; diff --git a/tests/middleware/fileUpload.spec.ts b/tests/middleware/fileUpload.spec.ts new file mode 100644 index 0000000000..5cdd0d7ed6 --- /dev/null +++ b/tests/middleware/fileUpload.spec.ts @@ -0,0 +1,215 @@ +import { describe, test, expect, vi, beforeEach } from "vitest"; +import type { Request, Response } from "express"; +import express from "express"; +import request from "supertest"; + +import { upload } from "../../src/config/multer"; +import { fileUpload } from "../../src/middleware"; +import { + CONTENT_TYPE_SHOULD_BE_MULTIPART_FORM_DATA, + FILE_SIZE_EXCEEDED, + IMAGE_SIZE_LIMIT, + INVALID_FILE_FIELD_NAME, + VIDEO_SIZE_LIMIT, +} from "../../src/constants"; + +vi.mock("../../src/libraries/requestContext", () => ({ + translate: (message: string): string => message, +})); + +describe("fileUpload Middleware", () => { + let app: express.Application; + + beforeEach(() => { + // Reset mocks + vi.clearAllMocks(); + + // Create new Express app for each test + app = express(); + + // Add json and urlencoded middleware + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + + // Add test route with the middleware + app.post("/upload", fileUpload("file"), (_req: Request, res: Response) => { + res.status(200).json({ message: "Upload successful" }); + }); + }); + + test("should reject requests without multipart/form-data content type", async () => { + const response = await request(app) + .post("/upload") + .set("Content-Type", "application/json") + .send({ data: "test" }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ + error: CONTENT_TYPE_SHOULD_BE_MULTIPART_FORM_DATA.MESSAGE, + }); + }); + + test("should reject request with no file", async () => { + const imageBuffer = Buffer.from("fake image content"); + const response = await request(app) + .post("/upload") + .set("Content-Type", "multipart/form-data") + .field("someField", "someValue") + .attach("different-file-field-name", imageBuffer, { + filename: "test.jpg", + contentType: "image/jpeg", + }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ + error: INVALID_FILE_FIELD_NAME.MESSAGE, + }); + }); + + test("should accept valid image upload", async () => { + // Create a small buffer to simulate an image file + const imageBuffer = Buffer.from("fake image content"); + + const response = await request(app) + .post("/upload") + .attach("file", imageBuffer, { + filename: "test.jpg", + contentType: "image/jpeg", + }); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + message: "Upload successful", + }); + }); + + test("should accept valid video upload", async () => { + // Create a small buffer to simulate a video file + const videoBuffer = Buffer.from("fake video content"); + + const response = await request(app) + .post("/upload") + .attach("file", videoBuffer, { + filename: "test.mp4", + contentType: "video/mp4", + }); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + message: "Upload successful", + }); + }); + + test("should reject image exceeding size limit", async () => { + const largeImageBuffer = Buffer.alloc(IMAGE_SIZE_LIMIT + 1); + + const response = await request(app) + .post("/upload") + .attach("file", largeImageBuffer, { + filename: "large.jpg", + contentType: "image/jpeg", + }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ + error: FILE_SIZE_EXCEEDED.MESSAGE, + description: "Image size exceeds the limit of 5MB", + }); + }); + + test("should reject video exceeding size limit", async () => { + const largeVideoBuffer = Buffer.alloc(VIDEO_SIZE_LIMIT + 1); + + const response = await request(app) + .post("/upload") + .attach("file", largeVideoBuffer, { + filename: "large.mp4", + contentType: "video/mp4", + }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ + error: FILE_SIZE_EXCEEDED.MESSAGE, + description: "Video size exceeds the limit of 50MB", + }); + }); + + test("should throw error on exceeding the multer max file size", async () => { + const largeVideoBuffer = Buffer.alloc(VIDEO_SIZE_LIMIT + 3); + + const response = await request(app) + .post("/upload") + .attach("file", largeVideoBuffer, { + filename: "large.mp4", + contentType: "video/mp4", + }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ + error: "File too large", + }); + }); + + test("should handle multiple files correctly", async () => { + const imageBuffer = Buffer.from("fake image content"); + + const response = await request(app) + .post("/upload") + .attach("file", imageBuffer, "test1.jpg") + .attach("anotherFile", imageBuffer, "test2.jpg"); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ + error: INVALID_FILE_FIELD_NAME.MESSAGE, + }); + }); + + test("should accept request with no file when content-type is correct", async () => { + const response = await request(app) + .post("/upload") + .set("Content-Type", "multipart/form-data") + .field("someField", "someValue"); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + message: "Upload successful", + }); + }); + + test("should reject files with wrong field name", async () => { + const imageBuffer = Buffer.from("fake image content"); + + const response = await request(app) + .post("/upload") + .attach("wrongField", imageBuffer, "test.jpg"); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ + error: INVALID_FILE_FIELD_NAME.MESSAGE, + }); + }); + + test("should handle generic upload errors", async () => { + const multerMock = vi.spyOn(upload, "single").mockImplementation(() => { + return (req, res, next): void => { + next(new Error("Generic upload error")); + }; + }); + + const imageBuffer = Buffer.from("fake image content"); + + const response = await request(app) + .post("/upload") + .attach("file", imageBuffer, { + filename: "test.jpg", + contentType: "image/jpeg", + }); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ + error: "File upload failed", + }); + + multerMock.mockRestore(); + }); +}); diff --git a/tests/middleware/isAuth.spec.ts b/tests/middleware/isAuth.spec.ts index 78f685ca43..90a73c88ca 100644 --- a/tests/middleware/isAuth.spec.ts +++ b/tests/middleware/isAuth.spec.ts @@ -1,10 +1,18 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Request } from "express"; -import { isAuth } from "../../src/middleware/isAuth"; +import type { NextFunction, Request, Response } from "express"; +import type { InterfaceAuthenticatedRequest } from "../../src/middleware/isAuth"; +import { isAuth, isAuthMiddleware } from "../../src/middleware/isAuth"; import { beforeEach, afterEach, describe, expect, it, vi } from "vitest"; import jwt from "jsonwebtoken"; import { logger } from "../../src/libraries/logger"; -import { ACCESS_TOKEN_SECRET } from "../../src/constants"; +import { + ACCESS_TOKEN_SECRET, + UNAUTHENTICATED_ERROR, +} from "../../src/constants"; + +vi.mock("../../src/libraries/requestContext", () => ({ + translate: (message: string): string => message, +})); interface TestInterfaceAuthData { isAuth: boolean; @@ -197,3 +205,69 @@ describe("middleware -> isAuth", () => { expect(authData).toEqual(testAuthData); }); }); + +describe("isAuthMiddleware", () => { + let mockRequest: Partial; + let mockResponse: Partial; + let nextFunction: NextFunction; + + beforeEach(() => { + mockRequest = { + headers: {}, + }; + mockResponse = { + status: vi.fn().mockReturnThis(), + json: vi.fn(), + } as unknown as Response; + nextFunction = vi.fn(); + + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should call next() when user is authenticated", () => { + // Mock successful token verification + vi.spyOn(jwt, "verify").mockImplementationOnce((...args: any) => { + const decoded = { + userId: "ValidUserId", + }; + const callBackFn = args[2]; + return callBackFn(null, decoded); + }); + + mockRequest.headers = { + authorization: "Bearer validToken", + }; + + isAuthMiddleware( + mockRequest as Request, + mockResponse as Response, + nextFunction, + ); + + expect(nextFunction).toHaveBeenCalled(); + expect(mockRequest.isAuth).toBe(true); + expect(mockRequest.userId).toBe("ValidUserId"); + expect(mockRequest.tokenExpired).toBeUndefined(); + expect(mockResponse.status).not.toHaveBeenCalled(); + expect(mockResponse.json).not.toHaveBeenCalled(); + }); + + it("should return 401 when token is not present", () => { + isAuthMiddleware( + mockRequest as Request, + mockResponse as Response, + nextFunction, + ); + + expect(nextFunction).not.toHaveBeenCalled(); + expect(mockResponse.status).toHaveBeenCalledWith(401); + expect(mockResponse.json).toHaveBeenCalledWith({ + message: UNAUTHENTICATED_ERROR.MESSAGE, + expired: undefined, + }); + }); +}); diff --git a/tests/resolvers/Mutation/createPost.spec.ts b/tests/resolvers/Mutation/createPost.spec.ts index 6d7e154e6e..8a03f389e6 100644 --- a/tests/resolvers/Mutation/createPost.spec.ts +++ b/tests/resolvers/Mutation/createPost.spec.ts @@ -1,7 +1,7 @@ import "dotenv/config"; import type mongoose from "mongoose"; import { Types } from "mongoose"; -import type { MutationCreatePostArgs } from "../../../src/types/generatedGraphQLTypes"; +import type { Response } from "express"; import { connect, disconnect } from "../../helpers/db"; import { @@ -14,18 +14,17 @@ import { vi, } from "vitest"; import { - BASE_URL, + INTERNAL_SERVER_ERROR, + INVALID_FILE_TYPE, LENGTH_VALIDATION_ERROR, ORGANIZATION_NOT_FOUND_ERROR, + PLEASE_PROVIDE_TITLE, USER_NOT_AUTHORIZED_ERROR, USER_NOT_AUTHORIZED_TO_PIN, USER_NOT_FOUND_ERROR, USER_NOT_MEMBER_FOR_ORGANIZATION, } from "../../../src/constants"; import { AppUserProfile, Organization, Post } from "../../../src/models"; -import { createPost as createPostResolverImage } from "../../../src/resolvers/Mutation/createPost"; -import * as uploadEncodedImage from "../../../src/utilities/encodedImageStorage/uploadEncodedImage"; -import * as uploadEncodedVideo from "../../../src/utilities/encodedVideoStorage/uploadEncodedVideo"; import type { TestOrganizationType, TestUserType, @@ -34,14 +33,31 @@ import { createTestUser, createTestUserAndOrganization, } from "../../helpers/userAndOrg"; +import type { InterfaceAuthenticatedRequest } from "../../../src/middleware"; +import { createPost } from "../../../src/REST/controllers/mutation"; +import * as uploadFileService from "../../../src/REST/services/file"; +import type { InterfaceUploadedFileResponse } from "../../../src/REST/services/file/uploadFile"; + +vi.mock("../../../src/libraries/requestContext", () => ({ + translate: (message: string): string => `Translated ${message}`, +})); let testUser: TestUserType; let testOrganization: TestOrganizationType; let MONGOOSE_INSTANCE: typeof mongoose; -vi.mock("../../utilities/uploadEncodedImage", () => ({ - uploadEncodedImage: vi.fn(), -})); +interface InterfaceMockResponse extends Omit { + status(code: number): InterfaceMockResponse; + json(data: unknown): InterfaceMockResponse; +} + +// Mock response object with proper return type +const mockResponse = (): InterfaceMockResponse => { + const res = {} as InterfaceMockResponse; + res.status = vi.fn().mockReturnValue(res); + res.json = vi.fn().mockReturnValue(res); + return res; +}; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); @@ -54,559 +70,346 @@ afterAll(async () => { await disconnect(MONGOOSE_INSTANCE); }); -describe("resolvers -> Mutation -> createPost", () => { +describe("controllers -> post -> createPost", () => { afterEach(() => { - vi.doUnmock("../../../src/constants"); - vi.resetModules(); - vi.resetAllMocks(); + vi.clearAllMocks(); }); - it(`throws NotFoundError if no user exists with _id === context.userId`, async () => { - const { requestContext } = await import("../../../src/libraries"); - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { - const args: MutationCreatePostArgs = { - data: { - organizationId: "", - text: "", - videoUrl: "", - title: "", - }, - }; - - const context = { - userId: new Types.ObjectId().toString(), - }; - - const { createPost: createPostResolver } = await import( - "../../../src/resolvers/Mutation/createPost" - ); - - await createPostResolver?.({}, args, context); - expect.fail(); - } catch (error: unknown) { - expect(spy).toBeCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); - expect((error as Error).message).toEqual( - `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}`, - ); - } + it("should throw NotFoundError if no user exists with _id === userId", async () => { + const req = { + userId: new Types.ObjectId().toString(), + body: { + organizationId: testOrganization?._id, + text: "Test post", + }, + } as InterfaceAuthenticatedRequest; + + const res = mockResponse(); + await createPost(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}`, + }); }); - it(`throws NotFoundError if no organization exists with _id === args.data.organizationId`, async () => { - const { requestContext } = await import("../../../src/libraries"); - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { - const args: MutationCreatePostArgs = { - data: { - organizationId: new Types.ObjectId().toString(), - text: "", - videoUrl: "", - title: "", - imageUrl: null, - }, - }; - - const context = { - userId: testUser?.id, - }; - - const { createPost: createPostResolver } = await import( - "../../../src/resolvers/Mutation/createPost" - ); - - await createPostResolver?.({}, args, context); - expect.fail(); - } catch (error) { - if (error instanceof Error) { - expect(spy).toBeCalledWith(ORGANIZATION_NOT_FOUND_ERROR.MESSAGE); - expect(error.message).toEqual( - `Translated ${ORGANIZATION_NOT_FOUND_ERROR.MESSAGE}`, - ); - } - } + it("should throw NotFoundError if no organization exists with _id === organizationId", async () => { + const req = { + userId: testUser?.id, + body: { + organizationId: new Types.ObjectId().toString(), + text: "Test post", + }, + } as InterfaceAuthenticatedRequest; + + const res = mockResponse(); + await createPost(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${ORGANIZATION_NOT_FOUND_ERROR.MESSAGE}`, + }); }); - it("throws error if AppUserProfile is not found", async () => { + it("should throw UnauthorizedError if AppUserProfile is not found", async () => { const userWithoutProfileId = await createTestUser(); await AppUserProfile.findByIdAndDelete( userWithoutProfileId?.appUserProfileId, ); - const { requestContext } = await import("../../../src/libraries"); - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { - const args: MutationCreatePostArgs = { - data: { - organizationId: "", - text: "", - videoUrl: "", - title: "", - }, - }; - - const context = { - userId: userWithoutProfileId?.id, - }; - - const { createPost: createPostResolver } = await import( - "../../../src/resolvers/Mutation/createPost" - ); - - await createPostResolver?.({}, args, context); - expect.fail(); - } catch (error: unknown) { - expect(spy).toBeCalledWith(USER_NOT_AUTHORIZED_ERROR.MESSAGE); - expect((error as Error).message).toEqual( - `Translated ${USER_NOT_AUTHORIZED_ERROR.MESSAGE}`, - ); - } - }); - it(`throws USER_NOT_AUTHORIZED_TO_PIN ERROR if the user is not authorized to pin the post`, async () => { - const organizationWithNoAdmin = await createTestUserAndOrganization( - true, - false, - ); - const { requestContext } = await import("../../../src/libraries"); - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { - const args: MutationCreatePostArgs = { - data: { - organizationId: organizationWithNoAdmin[1]?._id, - text: "New Post Text", - videoUrl: "http://dummyURL.com/", - title: "New Post Title", - imageUrl: "http://dummyURL.com/image/", - pinned: true, - }, - }; - - const context = { - userId: organizationWithNoAdmin[0]?.id, - }; - - expect(args.data.pinned).toBe(true); - const { createPost: createPostResolver } = await import( - "../../../src/resolvers/Mutation/createPost" - ); - - await createPostResolver?.({}, args, context); - expect.fail(); - } catch (error) { - if (error instanceof Error) { - expect(spy).toBeCalledWith(USER_NOT_AUTHORIZED_TO_PIN.MESSAGE); - expect(error.message).toEqual( - `Translated ${USER_NOT_AUTHORIZED_TO_PIN.MESSAGE}`, - ); - } - } + const req = { + userId: userWithoutProfileId?.id, + body: { + organizationId: testOrganization?._id, + text: "Test post", + }, + } as InterfaceAuthenticatedRequest; + + const res = mockResponse(); + await createPost(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${USER_NOT_AUTHORIZED_ERROR.MESSAGE}`, + }); }); - it("throws error if user is not member of the organization and is not superadmin", async () => { - const organizationWithNoMember = await createTestUserAndOrganization(false); - const { requestContext } = await import("../../../src/libraries"); - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { - const args: MutationCreatePostArgs = { - data: { - organizationId: organizationWithNoMember[1]?._id, - text: "New Post Text", - videoUrl: "http://dummyURL.com/", - title: "New Post Title", - imageUrl: "http://dummyURL.com/image/", - pinned: true, - }, - }; - - const context = { - userId: organizationWithNoMember[0]?.id, - }; - - expect(args.data.pinned).toBe(true); - const { createPost: createPostResolver } = await import( - "../../../src/resolvers/Mutation/createPost" - ); - - await createPostResolver?.({}, args, context); - expect.fail(); - } catch (error) { - if (error instanceof Error) { - expect(spy).toBeCalledWith(USER_NOT_MEMBER_FOR_ORGANIZATION.MESSAGE); - expect(error.message).toEqual( - `Translated ${USER_NOT_MEMBER_FOR_ORGANIZATION.MESSAGE}`, - ); - } - } + it("should throw error if user is not member of the organization and is not superadmin", async () => { + const [nonMemberUser] = await createTestUserAndOrganization(false); + + const req = { + userId: nonMemberUser?.id, + body: { + organizationId: testOrganization?._id, + text: "Test post", + }, + } as InterfaceAuthenticatedRequest; + + const res = mockResponse(); + await createPost(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${USER_NOT_MEMBER_FOR_ORGANIZATION.MESSAGE}`, + }); }); - it(`pinned post should be successfully added to the organization`, async () => { - const { requestContext } = await import("../../../src/libraries"); - vi.spyOn(requestContext, "translate").mockImplementationOnce( - (message) => `Translated ${message}`, + it("should throw error if trying to pin post without proper authorization", async () => { + const [nonAdminUser, nonAdminOrg] = await createTestUserAndOrganization( + true, + false, ); - const args: MutationCreatePostArgs = { - data: { - organizationId: testOrganization?.id, - text: "New Post Text", - videoUrl: "http://dummyURL.com/", - title: "New Post Title", - imageUrl: "http://dummyURL.com/image/", + const req = { + userId: nonAdminUser?.id, + body: { + organizationId: nonAdminOrg?._id, + text: "Test post", + title: "Test title", pinned: true, }, - }; - const context = { - userId: testUser?.id, - }; + } as InterfaceAuthenticatedRequest; - expect(args.data.pinned).toBe(true); + const res = mockResponse(); + await createPost(req, res); - const { createPost: createPostResolver } = await import( - "../../../src/resolvers/Mutation/createPost" - ); - const createdPost = await createPostResolver?.({}, args, context); - expect(createdPost).toEqual( - expect.objectContaining({ - text: "New Post Text", - videoUrl: null, // Update the expected value to match the received value - title: "New Post Title", + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${USER_NOT_AUTHORIZED_TO_PIN.MESSAGE}`, + }); + }); + + it("should throw error if pinned post has no title", async () => { + const req = { + userId: testUser?.id, + body: { + organizationId: testOrganization?._id, + text: "Test post", pinned: true, - }), - ); + }, + } as InterfaceAuthenticatedRequest; - const updatedTestOrg = await Organization.findOne({ - _id: testOrganization?.id, - }).lean(); + const res = mockResponse(); + await createPost(req, res); - expect( - updatedTestOrg?.pinnedPosts - .map((id) => id.toString()) - .includes(createdPost?._id.toString()), - ).toBeTruthy(); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${PLEASE_PROVIDE_TITLE.MESSAGE}`, + }); }); - it(`creates the post and returns it when image or video is not provided`, async () => { - const args: MutationCreatePostArgs = { - data: { - organizationId: testOrganization?.id, - text: "text", - videoUrl: "videoUrl", - title: "title", + it("should throw error if title exceeds maximum length", async () => { + const req = { + userId: testUser?.id, + body: { + organizationId: testOrganization?._id, + title: "a".repeat(257), + text: "Test post", pinned: true, }, - }; + } as InterfaceAuthenticatedRequest; + + const res = mockResponse(); + await createPost(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${LENGTH_VALIDATION_ERROR.MESSAGE} 256 characters in title`, + }); + }); - const context = { + it("should throw error if text exceeds maximum length", async () => { + const req = { userId: testUser?.id, - }; + body: { + organizationId: testOrganization?._id, + title: "Test title", + text: "a".repeat(501), + pinned: true, + }, + } as InterfaceAuthenticatedRequest; - expect(args.data.pinned).toBe(true); + const res = mockResponse(); + await createPost(req, res); - const { createPost: createPostResolver } = await import( - "../../../src/resolvers/Mutation/createPost" - ); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${LENGTH_VALIDATION_ERROR.MESSAGE} 500 characters in information`, + }); + }); + + it("should successfully create a regular post", async () => { + const req = { + userId: testUser?.id, + body: { + organizationId: testOrganization?._id, + text: "Test post", + }, + } as InterfaceAuthenticatedRequest; - const createPostPayload = await createPostResolver?.({}, args, context); - expect(createPostPayload?.pinned).toBe(true); + const res = mockResponse(); + await createPost(req, res); - expect(createPostPayload).toEqual( + expect(res.status).toHaveBeenCalledWith(201); + expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ - title: "title", - videoUrl: null, // Update the expected value to match the received value - creatorId: testUser?._id, - organization: testOrganization?._id, - imageUrl: null, + post: expect.objectContaining({ + text: "Test post", + pinned: false, + }), }), ); }); - it(`creates the post and and returns it when an image is provided`, async () => { - const args: MutationCreatePostArgs = { - data: { - organizationId: testOrganization?.id, - text: "text", - title: "title", + it("should successfully create a pinned post", async () => { + const req = { + userId: testUser?.id, + body: { + organizationId: testOrganization?._id, + title: "Test pinned post", + text: "Test post content", pinned: true, }, - file: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAZSURBVBhXYzxz5sx/BiBgefLkCQMbGxsDAEdkBicg9wbaAAAAAElFTkSuQmCC", // Provide a supported file type - }; + } as InterfaceAuthenticatedRequest; - vi.spyOn(uploadEncodedImage, "uploadEncodedImage").mockImplementation( - async (encodedImageURL: string) => encodedImageURL, - ); - - const context = { - userId: testUser?.id, - apiRootUrl: BASE_URL, - }; + const res = mockResponse(); + await createPost(req, res); - expect(args.data.pinned).toBe(true); - - const createPostPayload = await createPostResolverImage?.( - {}, - args, - context, + expect(res.status).toHaveBeenCalledWith(201); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + post: expect.objectContaining({ + title: "Test pinned post", + text: "Test post content", + pinned: true, + }), + }), ); - expect(createPostPayload?.pinned).toBe(true); - - const testCreatePostPayload = await Post.findOne({ - _id: createPostPayload?._id, - imageUrl: { $ne: null }, - }).lean(); - //Ensures that the post is created and imageUrl is not null - expect(testCreatePostPayload).not.toBeNull(); + // Verify organization was updated with pinned post + const updatedOrg = await Organization.findById(testOrganization?._id); + const createdPost = await Post.findOne({ title: "Test pinned post" }); + expect(updatedOrg?.pinnedPosts).toContainEqual(createdPost?._id); }); - it(`creates the post and and returns it when a video is provided`, async () => { - const args: MutationCreatePostArgs = { - data: { - organizationId: testOrganization?.id, - text: "text", - title: "title", - pinned: true, - }, - file: "data:video/mp4;base64,VIDEO_BASE64_DATA_HERE", // Provide a supported file type + it("should successfully create a post with file upload", async () => { + const mockFileId = new Types.ObjectId(); + const mockFileUploadResponse: InterfaceUploadedFileResponse = { + _id: mockFileId, + uri: "test/file/path", + visibility: "PUBLIC", + objectKey: "test-object-key", }; - vi.spyOn(uploadEncodedVideo, "uploadEncodedVideo").mockImplementation( - async (uploadEncodedVideo: string) => uploadEncodedVideo, + vi.spyOn(uploadFileService, "uploadFile").mockResolvedValueOnce( + mockFileUploadResponse, ); - const context = { + const req = { userId: testUser?.id, - apiRootUrl: BASE_URL, - }; + body: { + organizationId: testOrganization?._id, + text: "Test post with file", + }, + file: { + filename: "test.jpg", + mimetype: "image/jpeg", + buffer: Buffer.from("test"), + originalname: "test.jpg", + size: 1024, + }, + } as InterfaceAuthenticatedRequest; - expect(args.data.pinned).toBe(true); + const res = mockResponse(); + await createPost(req, res); - const createPostPayload = await createPostResolverImage?.( - {}, - args, - context, + expect(uploadFileService.uploadFile).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(201); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + post: expect.objectContaining({ + text: "Test post with file", + file: mockFileId, + }), + }), ); - expect(createPostPayload?.pinned).toBe(true); - - const testCreatePostPayload = await Post.findOne({ - _id: createPostPayload?._id, - videoUrl: { $ne: null }, - }).lean(); - - //Ensures that the post is created and videoUrl is not null - expect(testCreatePostPayload).not.toBeNull(); }); - it(`creates the post and throws an error for unsupported file type`, async () => { - const args: MutationCreatePostArgs = { - data: { - organizationId: testOrganization?.id, - text: "text", - videoUrl: "videoUrl", - title: "title", - pinned: true, - }, - file: "unsupportedFile.txt", // Provide an unsupported file type - }; + it("should handle file upload error gracefully", async () => { + vi.spyOn(uploadFileService, "uploadFile").mockRejectedValueOnce( + new Error("Upload failed"), + ); - const context = { + const req = { userId: testUser?.id, - apiRootUrl: BASE_URL, - }; + body: { + organizationId: testOrganization?._id, + text: "Test post with file", + }, + file: { + filename: "test.jpg", + mimetype: "image/jpeg", + buffer: Buffer.from("test"), + originalname: "test.jpg", + size: 1024, + }, + } as InterfaceAuthenticatedRequest; - expect(args.data.pinned).toBe(true); + const res = mockResponse(); + await createPost(req, res); - // Mock the uploadEncodedImage function to throw an error for unsupported file types - vi.spyOn(uploadEncodedImage, "uploadEncodedImage").mockImplementation( - () => { - throw new Error("Unsupported file type."); + expect(uploadFileService.uploadFile).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: "Upload failed", + }); + }); + + it("should handle invalid file type", async () => { + const req = { + userId: testUser?.id, + body: { + organizationId: testOrganization?._id, + text: "Test post with file", }, - ); + file: { + filename: "test.exe", + mimetype: "application/x-msdownload", + buffer: Buffer.from("test"), + originalname: "test.exe", + size: 1024, + }, + } as InterfaceAuthenticatedRequest; - // Ensure that an error is thrown when createPostResolverImage is called - await expect( - createPostResolverImage?.({}, args, context), - ).rejects.toThrowError("Unsupported file type."); - }); + const res = mockResponse(); + await createPost(req, res); - it(`throws String Length Validation error if title is greater than 256 characters`, async () => { - const { requestContext } = await import("../../../src/libraries"); - vi.spyOn(requestContext, "translate").mockImplementationOnce( - (message) => message, - ); - try { - const args: MutationCreatePostArgs = { - data: { - organizationId: testOrganization?._id, - text: "random", - videoUrl: "", - title: - "AfGtN9o7IJXH9Xr5P4CcKTWMVWKOOHTldleLrWfZcThgoX5scPE5o0jARvtVA8VhneyxXquyhWb5nluW2jtP0Ry1zIOUFYfJ6BUXvpo4vCw4GVleGBnoKwkFLp5oW9L8OsEIrjVtYBwaOtXZrkTEBySZ1prr0vFcmrSoCqrCTaChNOxL3tDoHK6h44ChFvgmoVYMSq3IzJohKtbBn68D9NfEVMEtoimkGarUnVBAOsGkKv0mIBJaCl2pnR8Xwq1cG1", - imageUrl: null, - pinned: true, - }, - }; - - const context = { - userId: testUser?.id, - }; - expect(args.data.pinned).toBe(true); - const { createPost: createPostResolver } = await import( - "../../../src/resolvers/Mutation/createPost" - ); - - const createdPost = await createPostResolver?.({}, args, context); - expect(createdPost?.pinned).toBe(true); - } catch (error) { - if (error instanceof Error) { - expect(error.message).toEqual( - `${LENGTH_VALIDATION_ERROR.MESSAGE} 256 characters in title`, - ); - } - } - }); - it(`throws String Length Validation error if text is greater than 500 characters`, async () => { - const { requestContext } = await import("../../../src/libraries"); - vi.spyOn(requestContext, "translate").mockImplementationOnce( - (message) => message, - ); - try { - const args: MutationCreatePostArgs = { - data: { - organizationId: testOrganization?._id, - text: "JWQPfpdkGGGKyryb86K4YN85nDj4m4F7gEAMBbMXLax73pn2okV6kpWY0EYO0XSlUc0fAlp45UCgg3s6mqsRYF9FOlzNIDFLZ1rd03Z17cdJRuvBcAmbC0imyqGdXHGDUQmVyOjDkaOLAvjhB5uDeuEqajcAPTcKpZ6LMpigXuqRAd0xGdPNXyITC03FEeKZAjjJL35cSIUeMv5eWmiFlmmm70FU1Bp6575zzBtEdyWPLflcA2GpGmmf4zvT7nfgN3NIkwQIhk9OwP8dn75YYczcYuUzLpxBu1Lyog77YlAj5DNdTIveXu9zHeC6V4EEUcPQtf1622mhdU3jZNMIAyxcAG4ErtztYYRqFs0ApUxXiQI38rmiaLcicYQgcOxpmFvqRGiSduiCprCYm90CHWbQFq4w2uhr8HhR3r9HYMIYtrRyO6C3rPXaQ7otpjuNgE0AKI57AZ4nGG1lvNwptFCY60JEndSLX9Za6XP1zkVRLaMZArQNl", - videoUrl: "", - title: "random", - imageUrl: null, - pinned: true, - }, - }; - - const context = { - userId: testUser?.id, - }; - - expect(args.data.pinned).toBe(true); - - const { createPost: createPostResolver } = await import( - "../../../src/resolvers/Mutation/createPost" - ); - - const createdPost = await createPostResolver?.({}, args, context); - expect(createdPost?.pinned).toBe(true); - } catch (error) { - if (error instanceof Error) { - expect(error.message).toEqual( - `${LENGTH_VALIDATION_ERROR.MESSAGE} 500 characters in information`, - ); - } - } + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${INVALID_FILE_TYPE.MESSAGE}`, + }); }); - it("throws an error if the user tries to create a post but post is not pinned", async () => { - const { requestContext } = await import("../../../src/libraries"); - vi.spyOn(requestContext, "translate").mockImplementationOnce( - (message) => message, - ); - try { - const args: MutationCreatePostArgs = { - data: { - organizationId: testOrganization?._id, - text: "text", - pinned: false, - }, - }; - - const context = { - userId: testUser?.id, - }; - - expect(args.data.pinned).toBe(false); - const { createPost: createPostResolver } = await import( - "../../../src/resolvers/Mutation/createPost" - ); - const createdPost = await createPostResolver?.({}, args, context); - expect(createdPost?.pinned).toBe(false); - } catch (error) { - if (error instanceof Error) { - expect(error.message).toEqual( - `Cannot create post when pinned is false`, - ); - } - } - }); + it("should handle non-Error type errors with internal server error message", async () => { + // Mock User.findOne to throw a non-Error type error + vi.spyOn(Post, "create").mockImplementationOnce(() => { + throw "Some unknown error"; + }); - it("throws error if title is provided and post is not pinned", async () => { - const { requestContext } = await import("../../../src/libraries"); - vi.spyOn(requestContext, "translate").mockImplementationOnce( - (message) => message, - ); - try { - const args: MutationCreatePostArgs = { - data: { - organizationId: testOrganization?._id, - title: "Test title", - text: "Test text", - pinned: false, - }, - }; - - const context = { - userId: testUser?.id, - }; - - expect(args.data.pinned).toBe(false); - const { createPost: createPostResolver } = await import( - "../../../src/resolvers/Mutation/createPost" - ); - const createdPost = await createPostResolver?.({}, args, context); - expect(createdPost?.pinned).toBe(false); - } catch (error) { - if (error instanceof Error) { - expect(error.message).toEqual( - `Post needs to be pinned inorder to add a title`, - ); - } - } - }); + const req = { + userId: testUser?.id, + body: { + organizationId: testOrganization?._id, + text: "Test post", + }, + } as InterfaceAuthenticatedRequest; - it("throws error if title is not provided and post is pinned", async () => { - const { requestContext } = await import("../../../src/libraries"); - vi.spyOn(requestContext, "translate").mockImplementationOnce( - (message) => message, - ); - try { - const args: MutationCreatePostArgs = { - data: { - organizationId: testOrganization?._id, - title: "", - text: "Test text", - pinned: true, - }, - }; - - const context = { - userId: testUser?.id, - }; - expect(args.data.pinned).toBe(true); - - const { createPost: createPostResolver } = await import( - "../../../src/resolvers/Mutation/createPost" - ); - const createPost = await createPostResolver?.({}, args, context); - expect(createPost?.pinned).toBe(true); - } catch (error) { - if (error instanceof Error) { - expect(error.message).toEqual(`Please provide a title to pin post`); - } - } + const res = mockResponse(); + await createPost(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${INTERNAL_SERVER_ERROR.MESSAGE}`, + }); }); }); diff --git a/tests/resolvers/Mutation/removePost.spec.ts b/tests/resolvers/Mutation/removePost.spec.ts index 197e74b9c3..b16d32140f 100644 --- a/tests/resolvers/Mutation/removePost.spec.ts +++ b/tests/resolvers/Mutation/removePost.spec.ts @@ -18,7 +18,7 @@ import { USER_NOT_AUTHORIZED_ERROR, USER_NOT_FOUND_ERROR, } from "../../../src/constants"; -import { AppUserProfile, Post } from "../../../src/models"; +import { AppUserProfile, File, Post } from "../../../src/models"; import type { TestPostType } from "../../helpers/posts"; import { createTestPost } from "../../helpers/posts"; import type { TestUserType } from "../../helpers/userAndOrg"; @@ -149,27 +149,53 @@ describe("resolvers -> Mutation -> removePost", () => { expect(removePostPayload).toEqual(testPost?.toObject()); }); - it(`deletes the post with image with _id === args.id and returns it`, async () => { + it(`deletes the post with file and returns it when referenceCount is 1`, async () => { const { requestContext } = await import("../../../src/libraries"); + const { BUCKET_NAME } = await import("../../../src/config/minio"); + vi.spyOn(requestContext, "translate").mockImplementationOnce( (message) => `Translated ${message}`, ); - const deletePreviousImage = await import( - "../../../src/utilities/encodedImageStorage/deletePreviousImage" - ); - const deleteImageSpy = vi - .spyOn(deletePreviousImage, "deletePreviousImage") - .mockImplementation(() => { - return Promise.resolve(); - }); + + // Mock Minio deleteFile with proper return type + const minioService = await import("../../../src/REST/services/minio"); + const minioDeleteSpy = vi + .spyOn(minioService, "deleteFile") + .mockImplementation(() => + Promise.resolve({ + $metadata: { + httpStatusCode: 204, + requestId: "mock-request-id", + attempts: 1, + totalRetryDelay: 0, + }, + }), + ); const [newTestUser, , newTestPost] = await createTestPost(); + const testFile = await File.create({ + _id: new Types.ObjectId(), + fileName: "test-file.jpg", + mimeType: "image/jpeg", + size: 1024, + hash: { + value: "test-hash-value", + algorithm: "SHA-256", + }, + uri: "test-uri", + referenceCount: 1, + status: "ACTIVE", + metadata: { + objectKey: "test-object-key", + }, + }); + const updatedPost = await Post.findOneAndUpdate( { _id: newTestPost?.id }, { $set: { - imageUrl: "images/fakeImagePathimage.png", + file: testFile._id, }, }, { new: true }, @@ -189,30 +215,62 @@ describe("resolvers -> Mutation -> removePost", () => { const removePostPayload = await removePostResolver?.({}, args, context); expect(removePostPayload).toEqual(updatedPost); - expect(deleteImageSpy).toBeCalledWith("images/fakeImagePathimage.png"); + + expect(minioDeleteSpy).toHaveBeenCalledWith( + BUCKET_NAME, + testFile.metadata.objectKey, + ); + + // Verify file was deleted from database + const deletedFile = await File.findById(testFile._id); + expect(deletedFile).toBeNull(); }); - it(`deletes the post with video with _id === args.id and returns it`, async () => { + it(`decrements file referenceCount when greater than 1`, async () => { const { requestContext } = await import("../../../src/libraries"); vi.spyOn(requestContext, "translate").mockImplementationOnce( (message) => `Translated ${message}`, ); - const deletePreviousVideo = await import( - "../../../src/utilities/encodedVideoStorage/deletePreviousVideo" - ); - const deleteVideoSpy = vi - .spyOn(deletePreviousVideo, "deletePreviousVideo") - .mockImplementation(() => { - return Promise.resolve(); - }); + + // Mock Minio deleteFile with proper return type + const minioService = await import("../../../src/REST/services/minio"); + const minioDeleteSpy = vi + .spyOn(minioService, "deleteFile") + .mockImplementation(() => + Promise.resolve({ + $metadata: { + httpStatusCode: 204, + requestId: "mock-request-id", + attempts: 1, + totalRetryDelay: 0, + }, + }), + ); const [newTestUser, , newTestPost] = await createTestPost(); + const testFile = await File.create({ + _id: new Types.ObjectId(), + fileName: "test-file.jpg", + mimeType: "image/jpeg", + size: 1024, + hash: { + value: "test-hash-value", + algorithm: "SHA-256", + }, + uri: "test-uri", + referenceCount: 2, + status: "ACTIVE", + metadata: { + objectKey: "test-object-key", + }, + }); + const updatedPost = await Post.findOneAndUpdate( { _id: newTestPost?.id }, { $set: { - videoUrl: "videos/fakeVideoPathvideo.png", + file: testFile._id, }, }, { new: true }, @@ -232,9 +290,16 @@ describe("resolvers -> Mutation -> removePost", () => { const removePostPayload = await removePostResolver?.({}, args, context); expect(removePostPayload).toEqual(updatedPost); - expect(deleteVideoSpy).toBeCalledWith("videos/fakeVideoPathvideo.png"); + + // Verify minioDeleteSpy was NOT called + expect(minioDeleteSpy).not.toHaveBeenCalled(); + + // Verify referenceCount was decremented + const updatedFile = await File.findById(testFile._id); + expect(updatedFile?.referenceCount).toBe(1); }); - it("throws an error if the user does not have appUserProfile", async () => { + + it("throws an error if the user does not have appUserProfile", async () => { const { requestContext } = await import("../../../src/libraries"); vi.spyOn(requestContext, "translate").mockImplementationOnce( (message) => `Translated ${message}`, diff --git a/tests/resolvers/Mutation/updatePost.spec.ts b/tests/resolvers/Mutation/updatePost.spec.ts index c57152dc7f..d5cd9f8589 100644 --- a/tests/resolvers/Mutation/updatePost.spec.ts +++ b/tests/resolvers/Mutation/updatePost.spec.ts @@ -1,342 +1,444 @@ import "dotenv/config"; +import type mongoose from "mongoose"; import { Types } from "mongoose"; -import { AppUserProfile, Post } from "../../../src/models"; -import type { MutationUpdatePostArgs } from "../../../src/types/generatedGraphQLTypes"; -import { connect, disconnect } from "../../../src/db"; -import { updatePost as updatePostResolver } from "../../../src/resolvers/Mutation/updatePost"; +import type { Response } from "express"; +import { connect, disconnect } from "../../helpers/db"; + +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + it, + vi, +} from "vitest"; import { + INTERNAL_SERVER_ERROR, LENGTH_VALIDATION_ERROR, + PLEASE_PROVIDE_TITLE, + POST_NEEDS_TO_BE_PINNED, POST_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, USER_NOT_FOUND_ERROR, } from "../../../src/constants"; -import { beforeEach, afterEach, describe, it, expect, vi } from "vitest"; +import { AppUserProfile, Post } from "../../../src/models"; +import type { + TestOrganizationType, + TestUserType, +} from "../../helpers/userAndOrg"; import { - createTestOrganizationWithAdmin, createTestUser, - type TestOrganizationType, - type TestUserType, + createTestUserAndOrganization, } from "../../helpers/userAndOrg"; - +import type { InterfaceAuthenticatedRequest } from "../../../src/middleware"; +import { updatePost } from "../../../src/REST/controllers/mutation"; +import * as fileServices from "../../../src/REST/services/file"; +import type { InterfaceUploadedFileResponse } from "../../../src/REST/services/file/uploadFile"; +import { createTestPostWithMedia } from "../../helpers/posts"; import type { TestPostType } from "../../helpers/posts"; -import { createTestPost, createTestSinglePost } from "../../helpers/posts"; + +vi.mock("../../../src/libraries/requestContext", () => ({ + translate: (message: string): string => `Translated ${message}`, +})); + +/** + * module - PostUpdateControllerTests + * description - Tests for the Post Update controller functionality in a social media/blog platform + * @packageDocumentation + * + * @remarks + * Test environment uses Vitest with MongoDB for integration testing. + * File includes mock implementations for file services and translations. + * + * Key test scenarios: + * - Media attachment management (upload/delete) + * - Post content updates (text/title/pinned status) + * - Input validation (text: 500 chars, title: 256 chars) + * - Error handling (auth, post existence, pinned post rules) + * + * @see {@link updatePost} - The controller being tested + * @see {@link InterfaceAuthenticatedRequest} - Request interface + * + * @example + * ```typescript + * npm run test updatePost.test + * ``` + */ let testUser: TestUserType; -let testPost: TestPostType; let testOrganization: TestOrganizationType; -let testPost2: TestPostType; - -beforeEach(async () => { - await connect(); - const temp = await createTestPost(true); +let testPost: TestPostType; +let MONGOOSE_INSTANCE: typeof mongoose; + +interface InterfaceMockResponse extends Omit { + status(code: number): InterfaceMockResponse; + json(data: unknown): InterfaceMockResponse; +} + +const mockResponse = (): InterfaceMockResponse => { + const res = {} as InterfaceMockResponse; + res.status = vi.fn().mockReturnValue(res); + res.json = vi.fn().mockReturnValue(res); + return res; +}; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const temp = await createTestUserAndOrganization(); testUser = temp[0]; testOrganization = temp[1]; - testPost = temp[2]; - testPost2 = await createTestSinglePost(testUser?.id, testOrganization?.id); - const { requestContext } = await import("../../../src/libraries"); - vi.spyOn(requestContext, "translate").mockImplementation( - (message) => message, - ); + // Create a test post + testPost = await createTestPostWithMedia(testUser?.id, testOrganization?._id); }); -afterEach(async () => { - await disconnect(); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); }); -describe("resolvers -> Mutation -> updatePost", () => { - it(`throws NotFoundError if no user exists with _id === context.userId`, async () => { - const { requestContext } = await import("../../../src/libraries"); - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { - const args: MutationUpdatePostArgs = { - id: testPost?._id.toString() || "", - data: { - title: "newTitle", - }, - }; - - const context = { - userId: new Types.ObjectId().toString(), - }; - - await updatePostResolver?.({}, args, context); - } catch (error: unknown) { - expect(spy).toBeCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); - expect((error as Error).message).toEqual( - `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}`, - ); - } +describe("controllers -> post -> updatePost", () => { + afterEach(() => { + vi.clearAllMocks(); }); - it(`throws NotFoundError if no post exists with _id === args.id`, async () => { - try { - const args: MutationUpdatePostArgs = { - id: new Types.ObjectId().toString(), - }; + it("should successfully update post with file", async () => { + const mockFileId = new Types.ObjectId(); + const mockFileUploadResponse: InterfaceUploadedFileResponse = { + _id: mockFileId, + uri: "test/file/path", + visibility: "PUBLIC", + objectKey: "new-test-key", + }; - const context = { - userId: testUser?._id, - }; + vi.spyOn(fileServices, "uploadFile").mockResolvedValueOnce( + mockFileUploadResponse, + ); + vi.spyOn(fileServices, "deleteFile").mockResolvedValueOnce({ + success: true, + message: "File deleted successfully.", + }); + + const req = { + userId: testUser?.id, + params: { id: testPost?._id.toString() }, + body: { + text: "Updated text with new file", + }, + file: { + filename: "test.jpg", + mimetype: "image/jpeg", + buffer: Buffer.from("test"), + originalname: "test.jpg", + size: 1024, + }, + } as unknown as InterfaceAuthenticatedRequest; - await updatePostResolver?.({}, args, context); - } catch (error: unknown) { - expect((error as Error).message).toEqual(POST_NOT_FOUND_ERROR.MESSAGE); - } - }); + const res = mockResponse(); + await updatePost(req, res); - it(`throws UnauthorizedError as current user with _id === context.userId is - not an creator of post with _id === args.id`, async () => { - const testUser1 = await createTestUser(); - const testOrg1 = await createTestOrganizationWithAdmin( - testUser1?._id, - false, - false, + expect(fileServices.uploadFile).toHaveBeenCalled(); + expect(fileServices.deleteFile).toHaveBeenCalledWith( + "test-file-object-key", + testPost?.file._id.toString(), + ); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + post: expect.objectContaining({ + text: "Updated text with new file", + file: mockFileId, + }), + }), ); - const testPost1 = await createTestSinglePost(testUser1?._id, testOrg1?._id); - - try { - const args: MutationUpdatePostArgs = { - id: testPost1?._id.toString() ?? "", - data: { - title: "newTitle", - }, - }; - - const context = { - userId: testUser1?._id, - }; - - await Post.updateOne( - { _id: testPost1?._id }, - { $set: { creatorId: new Types.ObjectId().toString() } }, - ); - - await updatePostResolver?.({}, args, context); - } catch (error: unknown) { - expect((error as Error).message).toEqual( - USER_NOT_AUTHORIZED_ERROR.MESSAGE, - ); - } }); - it(`updates the post with _id === args.id and returns the updated post`, async () => { - const args: MutationUpdatePostArgs = { - id: testPost?._id.toString() || "", - data: { - title: "newTitle", - text: "nextText", + it("should successfully update the title of a pinned post", async () => { + const pinnedPost = await createTestPostWithMedia( + testUser?.id, + testOrganization?.id, + true, + ); + + const req = { + userId: testUser?.id, + params: { id: pinnedPost?._id.toString() }, + body: { + title: "Updated Title", + pinned: true, }, - }; + } as unknown as InterfaceAuthenticatedRequest; + + const res = mockResponse(); + await updatePost(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + post: expect.objectContaining({ + title: "Updated Title", + }), + }), + ); + }); - const context = { - userId: testUser?._id, - }; + it("should successfully update the pinned status of a post", async () => { + const req = { + userId: testUser?.id, + params: { id: testPost?._id.toString() }, + body: { + pinned: false, + }, + } as unknown as InterfaceAuthenticatedRequest; + + const res = mockResponse(); + await updatePost(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + post: expect.objectContaining({ + pinned: false, + }), + }), + ); + }); - const updatePostPayload = await updatePostResolver?.({}, args, context); + it("should throw NotFoundError if no user exists with _id === userId", async () => { + const req = { + userId: new Types.ObjectId().toString(), + params: { id: testPost?._id.toString() }, + body: { + text: "Updated text", + }, + } as unknown as InterfaceAuthenticatedRequest; - const testUpdatePostPayload = await Post.findOne({ - _id: testPost?._id, - }).lean(); + const res = mockResponse(); + await updatePost(req, res); - expect(updatePostPayload).toEqual(testUpdatePostPayload); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}`, + }); }); - it(`updates the post with imageUrl and returns the updated post`, async () => { - const args: MutationUpdatePostArgs = { - id: testPost?._id.toString() || "", - data: { - title: "newTitle", - text: "nextText", - imageUrl: - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAZSURBVBhXYzxz5sx/BiBgefLkCQMbGxsDAEdkBicg9wbaAAAAAElFTkSuQmCC", + + it("should throw NotFoundError if post does not exist", async () => { + const req = { + userId: testUser?.id, + params: { id: new Types.ObjectId().toString() }, + body: { + text: "Updated text", }, - }; + } as unknown as InterfaceAuthenticatedRequest; - const context = { - userId: testUser?._id, - }; + const res = mockResponse(); + await updatePost(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${POST_NOT_FOUND_ERROR.MESSAGE}`, + }); + }); + + it("should throw UnauthorizedError if AppUserProfile is not found", async () => { + const userWithoutProfileId = await createTestUser(); + await AppUserProfile.findByIdAndDelete( + userWithoutProfileId?.appUserProfileId, + ); - const updatePostPayload = await updatePostResolver?.({}, args, context); + const req = { + userId: userWithoutProfileId?.id, + params: { id: testPost?._id.toString() }, + body: { + text: "Updated text", + }, + } as unknown as InterfaceAuthenticatedRequest; - const testUpdatePostPayload = await Post.findOne({ - _id: testPost?._id, - }).lean(); + const res = mockResponse(); + await updatePost(req, res); - expect(updatePostPayload).toEqual(testUpdatePostPayload); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${USER_NOT_AUTHORIZED_ERROR.MESSAGE}`, + }); }); - it(`updates the post with videoUrl and returns the updated post`, async () => { - const args: MutationUpdatePostArgs = { - id: testPost?._id.toString() || "", - data: { - title: "newTitle", - text: "nextText", - videoUrl: "data:video/mp4;base64,VIDEO_BASE64_DATA_HERE", + + it("should throw UnauthorizedError if user is not authorized to update post", async () => { + const [unauthorizedUser] = await createTestUserAndOrganization(); + + const req = { + userId: unauthorizedUser?.id, + params: { id: testPost?._id.toString() }, + body: { + text: "Unauthorized update", }, - }; + } as unknown as InterfaceAuthenticatedRequest; - const context = { - userId: testUser?._id, - }; + const res = mockResponse(); + await updatePost(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${USER_NOT_AUTHORIZED_ERROR.MESSAGE}`, + }); + }); - const updatePostPayload = await updatePostResolver?.({}, args, context); + it("should throw error if trying to add title to unpinned post", async () => { + const req = { + userId: testUser?.id, + params: { id: testPost?._id.toString() }, + body: { + title: "New Title", + }, + } as unknown as InterfaceAuthenticatedRequest; - const testUpdatePostPayload = await Post.findOne({ - _id: testPost?._id, - }).lean(); + const res = mockResponse(); + await updatePost(req, res); - expect(updatePostPayload).toEqual(testUpdatePostPayload); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${POST_NEEDS_TO_BE_PINNED.MESSAGE}`, + }); }); - it(`throws String Length Validation error if title is greater than 256 characters`, async () => { - const { requestContext } = await import("../../../src/libraries"); - vi.spyOn(requestContext, "translate").mockImplementationOnce( - (message) => message, + + it("should throw error if removing title from pinned post", async () => { + // First create a pinned post with title + const pinnedPost = await createTestPostWithMedia( + testUser?.id, + testOrganization?.id, + true, ); - try { - const args: MutationUpdatePostArgs = { - id: testPost?._id.toString() || "", - data: { - text: "random", - videoUrl: "", - title: - "AfGtN9o7IJXH9Xr5P4CcKTWMVWKOOHTldleLrWfZcThgoX5scPE5o0jARvtVA8VhneyxXquyhWb5nluW2jtP0Ry1zIOUFYfJ6BUXvpo4vCw4GVleGBnoKwkFLp5oW9L8OsEIrjVtYBwaOtXZrkTEBySZ1prr0vFcmrSoCqrCTaChNOxL3tDoHK6h44ChFvgmoVYMSq3IzJohKtbBn68D9NfEVMEtoimkGarUnVBAOsGkKv0mIBJaCl2pnR8Xwq1cG1", - imageUrl: null, - }, - }; - - const context = { - userId: testUser?.id, - }; - - const { updatePost: updatePostResolver } = await import( - "../../../src/resolvers/Mutation/updatePost" - ); - - await updatePostResolver?.({}, args, context); - } catch (error: unknown) { - expect((error as Error).message).toEqual( - `${LENGTH_VALIDATION_ERROR.MESSAGE} 256 characters in title`, - ); - } + const req = { + userId: testUser?.id, + params: { id: pinnedPost?.id.toString() }, + body: { + title: "", + pinned: true, + }, + } as unknown as InterfaceAuthenticatedRequest; + + const res = mockResponse(); + await updatePost(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${PLEASE_PROVIDE_TITLE.MESSAGE}`, + }); }); - it(`throws String Length Validation error if text is greater than 500 characters`, async () => { - const { requestContext } = await import("../../../src/libraries"); - vi.spyOn(requestContext, "translate").mockImplementationOnce( - (message) => message, + + it("should throw error if title exceeds maximum length", async () => { + const testPost = await createTestPostWithMedia( + testUser?.id, + testOrganization?._id, + true, ); - try { - const args: MutationUpdatePostArgs = { - id: testPost?._id.toString() || "", - data: { - text: "JWQPfpdkGGGKyryb86K4YN85nDj4m4F7gEAMBbMXLax73pn2okV6kpWY0EYO0XSlUc0fAlp45UCgg3s6mqsRYF9FOlzNIDFLZ1rd03Z17cdJRuvBcAmbC0imyqGdXHGDUQmVyOjDkaOLAvjhB5uDeuEqajcAPTcKpZ6LMpigXuqRAd0xGdPNXyITC03FEeKZAjjJL35cSIUeMv5eWmiFlmmm70FU1Bp6575zzBtEdyWPLflcA2GpGmmf4zvT7nfgN3NIkwQIhk9OwP8dn75YYczcYuUzLpxBu1Lyog77YlAj5DNdTIveXu9zHeC6V4EEUcPQtf1622mhdU3jZNMIAyxcAG4ErtztYYRqFs0ApUxXiQI38rmiaLcicYQgcOxpmFvqRGiSduiCprCYm90CHWbQFq4w2uhr8HhR3r9HYMIYtrRyO6C3rPXaQ7otpjuNgE0AKI57AZ4nGG1lvNwptFCY60JEndSLX9Za6XP1zkVRLaMZArQNl", - videoUrl: "", - title: "random", - imageUrl: null, - }, - }; - - const context = { - userId: testUser?.id, - }; - - const { updatePost: updatePostResolver } = await import( - "../../../src/resolvers/Mutation/updatePost" - ); - - await updatePostResolver?.({}, args, context); - } catch (error: unknown) { - expect((error as Error).message).toEqual( - `${LENGTH_VALIDATION_ERROR.MESSAGE} 500 characters in information`, - ); - } + const req = { + userId: testUser?.id, + params: { id: testPost?._id.toString() }, + body: { + title: "a".repeat(257), + pinned: true, + }, + } as unknown as InterfaceAuthenticatedRequest; + + const res = mockResponse(); + await updatePost(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${LENGTH_VALIDATION_ERROR.MESSAGE} 256 characters in title`, + }); }); - it("throws error if title is provided and post is not pinned", async () => { - const { requestContext } = await import("../../../src/libraries"); - vi.spyOn(requestContext, "translate").mockImplementationOnce( - (message) => message, - ); - try { - const args: MutationUpdatePostArgs = { - id: testPost2?._id.toString() || "", - data: { - title: "Test title", - text: "Test text", - }, - }; - - const context = { - userId: testUser?.id, - }; - - const { updatePost: updatePostResolver } = await import( - "../../../src/resolvers/Mutation/updatePost" - ); - - await updatePostResolver?.({}, args, context); - } catch (error: unknown) { - expect((error as Error).message).toEqual( - `Post needs to be pinned inorder to add a title`, - ); - } + it("should throw error if text exceeds maximum length", async () => { + const req = { + userId: testUser?.id, + params: { id: testPost?._id.toString() }, + body: { + text: "a".repeat(501), + }, + } as unknown as InterfaceAuthenticatedRequest; + + const res = mockResponse(); + await updatePost(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${LENGTH_VALIDATION_ERROR.MESSAGE} 500 characters in information`, + }); }); - it(`throws error if title is not provided and post is pinned`, async () => { - const { requestContext } = await import("../../../src/libraries"); - vi.spyOn(requestContext, "translate").mockImplementationOnce( - (message) => message, + it("should successfully update post text", async () => { + const req = { + userId: testUser?.id, + params: { id: testPost?._id.toString() }, + body: { + text: "Updated post text", + }, + } as unknown as InterfaceAuthenticatedRequest; + + const res = mockResponse(); + await updatePost(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + post: expect.objectContaining({ + text: "Updated post text", + }), + }), ); - try { - const args: MutationUpdatePostArgs = { - id: testPost?._id.toString() || "", - data: { - text: "Testing text", - }, - }; - - const context = { - userId: testUser?.id, - }; - - const { updatePost: updatePostResolver } = await import( - "../../../src/resolvers/Mutation/updatePost" - ); - - await updatePostResolver?.({}, args, context); - } catch (error: unknown) { - expect((error as Error).message).toEqual( - `Please provide a title to pin post`, - ); - } }); - it("throws error if AppUserProfile is not found", async () => { - const userWithoutProfileId = await createTestUser(); - await AppUserProfile.findByIdAndDelete( - userWithoutProfileId?.appUserProfileId, + it("should handle file upload error gracefully", async () => { + vi.spyOn(fileServices, "uploadFile").mockRejectedValueOnce( + new Error("Upload failed"), ); - const { requestContext } = await import("../../../src/libraries"); - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { - const args: MutationUpdatePostArgs = { - id: testPost?._id.toString() || "", - data: { - title: "newTitle", - }, - }; - - const context = { - userId: userWithoutProfileId?._id, - }; - - await updatePostResolver?.({}, args, context); - } catch (error: unknown) { - expect(spy).toBeCalledWith(USER_NOT_AUTHORIZED_ERROR.MESSAGE); - expect((error as Error).message).toEqual( - `Translated ${USER_NOT_AUTHORIZED_ERROR.MESSAGE}`, - ); - } + + const req = { + userId: testUser?.id, + params: { id: testPost?._id.toString() }, + body: { + text: "Updated text with file", + }, + file: { + filename: "test.jpg", + mimetype: "image/jpeg", + buffer: Buffer.from("test"), + originalname: "test.jpg", + size: 1024, + }, + } as unknown as InterfaceAuthenticatedRequest; + + const res = mockResponse(); + await updatePost(req, res); + + expect(fileServices.uploadFile).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: "Upload failed", + }); + }); + + it("should handle non-Error type errors with internal server error message", async () => { + vi.spyOn(Post, "findOneAndUpdate").mockImplementationOnce(() => { + throw "Some unknown error"; + }); + + const req = { + userId: testUser?.id, + params: { id: testPost?._id.toString() }, + body: { + text: "Updated text", + }, + } as unknown as InterfaceAuthenticatedRequest; + + const res = mockResponse(); + await updatePost(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: `Translated ${INTERNAL_SERVER_ERROR.MESSAGE}`, + }); }); }); diff --git a/tests/resolvers/Mutation/updateUserProfile.spec.ts b/tests/resolvers/Mutation/updateUserProfile.spec.ts index 5cfe090aa1..e3657c28b5 100644 --- a/tests/resolvers/Mutation/updateUserProfile.spec.ts +++ b/tests/resolvers/Mutation/updateUserProfile.spec.ts @@ -6,6 +6,7 @@ import type { InterfaceUser } from "../../../src/models"; import { User } from "../../../src/models"; import type { MutationUpdateUserProfileArgs } from "../../../src/types/generatedGraphQLTypes"; import { connect, disconnect } from "../../helpers/db"; +import * as userCache from "../../../src/services/UserCache/deleteUserFromCache"; import { nanoid } from "nanoid"; import { @@ -23,7 +24,6 @@ import { USER_NOT_FOUND_ERROR, } from "../../../src/constants"; import { updateUserProfile as updateUserProfileResolver } from "../../../src/resolvers/Mutation/updateUserProfile"; -import { deleteUserFromCache } from "../../../src/services/UserCache/deleteUserFromCache"; import * as uploadEncodedImage from "../../../src/utilities/encodedImageStorage/uploadEncodedImage"; let MONGOOSE_INSTANCE: typeof mongoose; @@ -302,75 +302,6 @@ describe("resolvers -> Mutation -> updateUserProfile", () => { }); }); - it("When Image is give updates the current user's object with the uploaded image and returns it", async () => { - const args: MutationUpdateUserProfileArgs = { - data: {}, - file: "newImageFile.png", - }; - - vi.spyOn(uploadEncodedImage, "uploadEncodedImage").mockImplementation( - async (encodedImageURL: string) => encodedImageURL, - ); - - const context = { - userId: testUser._id, - apiRootUrl: BASE_URL, - }; - await deleteUserFromCache(testUser._id.toString() || ""); - - const updateUserProfilePayload = await updateUserProfileResolver?.( - {}, - args, - context, - ); - - expect(updateUserProfilePayload).toEqual({ - ...testUser.toObject(), - email: updateUserProfilePayload?.email, - firstName: "newFirstName", - lastName: "newLastName", - image: BASE_URL + "newImageFile.png", - updatedAt: expect.anything(), - createdAt: expect.anything(), - }); - }); - - it("When Image is give updates the current user's object with the uploaded image and returns it", async () => { - const args: MutationUpdateUserProfileArgs = { - data: { - email: `email${nanoid().toLowerCase()}@gmail.com`, - firstName: "newFirstName", - lastName: "newLastName", - }, - file: "newImageFile.png", - }; - - vi.spyOn(uploadEncodedImage, "uploadEncodedImage").mockImplementation( - async (encodedImageURL: string) => encodedImageURL, - ); - - const context = { - userId: testUser._id, - apiRootUrl: BASE_URL, - }; - - const updateUserProfilePayload = await updateUserProfileResolver?.( - {}, - args, - context, - ); - - expect(updateUserProfilePayload).toEqual({ - ...testUser.toObject(), - email: args.data?.email, - firstName: "newFirstName", - lastName: "newLastName", - image: BASE_URL + args.file, - updatedAt: expect.anything(), - createdAt: expect.anything(), - }); - }); - it(`updates current user's user object when any single argument(birthdate) is given w/0 changing other fields `, async () => { const args: MutationUpdateUserProfileArgs = { data: { @@ -396,7 +327,7 @@ describe("resolvers -> Mutation -> updateUserProfile", () => { email: testUserobj?.email, firstName: testUserobj?.firstName, lastName: testUserobj?.lastName, - image: BASE_URL + "newImageFile.png", + image: null, birthDate: args.data?.birthDate, updatedAt: expect.anything(), createdAt: expect.anything(), @@ -428,7 +359,7 @@ describe("resolvers -> Mutation -> updateUserProfile", () => { email: testUserobj?.email, firstName: testUserobj?.firstName, lastName: testUserobj?.lastName, - image: BASE_URL + "newImageFile.png", + image: null, birthDate: date, educationGrade: args.data?.educationGrade, updatedAt: expect.anything(), @@ -461,7 +392,7 @@ describe("resolvers -> Mutation -> updateUserProfile", () => { email: testUserobj?.email, firstName: testUserobj?.firstName, lastName: testUserobj?.lastName, - image: BASE_URL + "newImageFile.png", + image: null, birthDate: date, educationGrade: "GRADUATE", employmentStatus: args.data?.employmentStatus, @@ -495,7 +426,7 @@ describe("resolvers -> Mutation -> updateUserProfile", () => { email: testUserobj?.email, firstName: testUserobj?.firstName, lastName: testUserobj?.lastName, - image: BASE_URL + "newImageFile.png", + image: null, birthDate: date, educationGrade: "GRADUATE", employmentStatus: "FULL_TIME", @@ -530,7 +461,7 @@ describe("resolvers -> Mutation -> updateUserProfile", () => { email: testUserobj?.email, firstName: testUserobj?.firstName, lastName: testUserobj?.lastName, - image: BASE_URL + "newImageFile.png", + image: null, birthDate: date, educationGrade: "GRADUATE", employmentStatus: "FULL_TIME", @@ -570,7 +501,7 @@ describe("resolvers -> Mutation -> updateUserProfile", () => { email: testUserobj?.email, firstName: testUserobj?.firstName, lastName: testUserobj?.lastName, - image: BASE_URL + "newImageFile.png", + image: null, birthDate: date, educationGrade: "GRADUATE", employmentStatus: "FULL_TIME", @@ -616,7 +547,7 @@ describe("resolvers -> Mutation -> updateUserProfile", () => { email: testUserobj?.email, firstName: testUserobj?.firstName, lastName: testUserobj?.lastName, - image: BASE_URL + "newImageFile.png", + image: null, birthDate: date, educationGrade: "GRADUATE", employmentStatus: "FULL_TIME", @@ -668,6 +599,10 @@ describe("resolvers -> Mutation -> updateUserProfile", () => { async (encodedImageURL: string) => encodedImageURL, ); + const deleteFromCacheSpy = vi + .spyOn(userCache, "deleteUserFromCache") + .mockImplementation(async () => Promise.resolve()); + const context = { userId: testUser._id, apiRootUrl: BASE_URL, @@ -679,12 +614,15 @@ describe("resolvers -> Mutation -> updateUserProfile", () => { context, ); + expect(deleteFromCacheSpy).toHaveBeenCalledWith( + updateUserProfilePayload?._id.toString(), + ); expect(updateUserProfilePayload).toEqual({ ...testUser.toObject(), email: args.data?.email, firstName: args.data?.firstName, lastName: args.data?.lastName, - image: BASE_URL + args.file, + image: args.file, birthDate: date, educationGrade: args.data?.educationGrade, employmentStatus: args.data?.employmentStatus, diff --git a/tests/resolvers/Organization/image.spec.ts b/tests/resolvers/Organization/image.spec.ts index 81124c80dc..f94db31e7b 100644 --- a/tests/resolvers/Organization/image.spec.ts +++ b/tests/resolvers/Organization/image.spec.ts @@ -66,7 +66,7 @@ describe("resolvers -> Organization -> image", () => { const org = await Organization.findOne({ _id: parent._id, }); - expect(creatorPayload).toEqual("http://testdomain.com" + org?.image); + expect(creatorPayload).toEqual(org?.image); } }); it(`returns null if the image is null in the organization`, async () => { diff --git a/tests/resolvers/Organization/posts.spec.ts b/tests/resolvers/Organization/posts.spec.ts index 74bb6a8d69..a71ec1d322 100644 --- a/tests/resolvers/Organization/posts.spec.ts +++ b/tests/resolvers/Organization/posts.spec.ts @@ -70,54 +70,23 @@ describe("resolvers -> Organization -> post", () => { creatorId: testUser?._id, }).countDocuments(); - const context = { apiRootUrl: "http://example.com" }; - - const formattedPost2 = { - ...testPost2?.toObject(), - imageUrl: testPost2?.imageUrl - ? new URL(testPost2.imageUrl, context.apiRootUrl).toString() - : null, - videoUrl: testPost2?.videoUrl - ? new URL(testPost2.videoUrl, context.apiRootUrl).toString() - : null, - }; - - const formattedPost = { - ...testPost?.toObject(), - imageUrl: testPost?.imageUrl - ? new URL(testPost.imageUrl, context.apiRootUrl).toString() - : null, - videoUrl: testPost?.videoUrl - ? new URL(testPost.videoUrl, context.apiRootUrl).toString() - : null, - }; + const formattedPost2 = testPost2?.toObject(); + const formattedPost = testPost?.toObject(); expect(connection).toEqual({ edges: [ { - cursor: formattedPost2._id?.toString(), + cursor: formattedPost2?._id?.toString(), node: { ...formattedPost2, - _id: formattedPost2._id?.toString(), - imageUrl: testPost?.imageUrl - ? new URL(testPost.imageUrl, context.apiRootUrl).toString() - : null, - videoUrl: formattedPost2?.videoUrl - ? new URL(formattedPost2.videoUrl, context.apiRootUrl).toString() - : null, + _id: formattedPost2?._id?.toString(), }, }, { - cursor: formattedPost._id?.toString(), + cursor: formattedPost?._id?.toString(), node: { ...formattedPost, _id: formattedPost?._id?.toString(), - imageUrl: formattedPost?.imageUrl - ? new URL(formattedPost.imageUrl, context.apiRootUrl).toString() - : null, - videoUrl: formattedPost?.videoUrl - ? new URL(formattedPost.videoUrl, context.apiRootUrl).toString() - : null, }, }, ], @@ -165,4 +134,48 @@ describe("parseCursor function", () => { expect(result.parsedCursor).toEqual(testPost?._id.toString()); } }); + it("throws GraphQLError when an invalid cursor is provided", async () => { + const parent = { + _id: testOrganization?._id, + } as InterfaceOrganization; + + try { + await postResolver?.( + parent, + { + first: 2, + after: new Types.ObjectId().toString(), // Invalid cursor + }, + {}, + ); + // If we reach here, the test should fail because an error should have been thrown + expect(true).toBe(false); + } catch (error) { + expect(error).toBeInstanceOf(GraphQLError); + if (error instanceof GraphQLError) { + expect(error.extensions.code).toEqual("INVALID_ARGUMENTS"); + expect( + (error.extensions.errors as DefaultGraphQLArgumentError[]).length, + ).toBeGreaterThan(0); + } + } + }); + + it("successfully uses parseCursor with valid cursor", async () => { + const parent = { + _id: testOrganization?._id, + } as InterfaceOrganization; + + const connection = await postResolver?.( + parent, + { + first: 2, + after: testPost2?._id.toString(), // Valid cursor + }, + {}, + ); + + expect(connection).toBeDefined(); + expect(connection?.edges.length).toBe(1); + }); }); diff --git a/tests/resolvers/Query/checkAuth.spec.ts b/tests/resolvers/Query/checkAuth.spec.ts index a4539389b9..7024150f66 100644 --- a/tests/resolvers/Query/checkAuth.spec.ts +++ b/tests/resolvers/Query/checkAuth.spec.ts @@ -9,7 +9,7 @@ import { USER_NOT_FOUND_ERROR } from "../../../src/constants"; let MONGOOSE_INSTANCE: typeof mongoose; -import { AppUserProfile, User } from "../../../src/models"; +import { AppUserProfile } from "../../../src/models"; import { createTestUser } from "../../helpers/userAndOrg"; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); @@ -44,34 +44,6 @@ describe("resolvers -> Query -> checkAuth", () => { expect(user).toEqual({ ...testUser?.toObject(), image: null }); }); - it("returns user object with image ", async () => { - let testUser = await createTestUser(); - - await User.findOneAndUpdate( - { - _id: testUser?.id, - }, - { - image: `path`, - }, - ); - - testUser = await User.findOne({ - _id: testUser?.id, - }); - - const context = { - userId: testUser?._id, - apiRootUrl: `http://localhost:3000`, - }; - - const user = await checkAuthResolver?.({}, {}, context); - - expect(user).toEqual({ - ...testUser?.toObject(), - image: `${context.apiRootUrl}${testUser?.image}`, - }); - }); it("throws error if user does not have appUserProfile", async () => { try { const testUser = await createTestUser(); diff --git a/tests/resolvers/Query/getFile.spec.ts b/tests/resolvers/Query/getFile.spec.ts new file mode 100644 index 0000000000..a6ff41d4f4 --- /dev/null +++ b/tests/resolvers/Query/getFile.spec.ts @@ -0,0 +1,138 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { GetObjectCommandOutput } from "@aws-sdk/client-s3"; +import { GetObjectCommand } from "@aws-sdk/client-s3"; +import type { Request, Response } from "express"; +import type { Readable } from "stream"; +import { s3Client } from "../../../src/config/minio"; +import { getFile } from "../../../src/REST/controllers/query"; +import type { SdkStreamMixin } from "@aws-sdk/types"; + +// Mock the s3Client +vi.mock("../../../src/config/minio", () => ({ + s3Client: { + send: vi.fn(), + }, + BUCKET_NAME: "test-bucket", +})); + +describe("getFile", () => { + let mockRequest: Request; + let mockResponse: Response; + + beforeEach(() => { + vi.clearAllMocks(); + + mockResponse = { + setHeader: vi.fn(), + status: vi.fn().mockReturnThis(), + send: vi.fn(), + pipe: vi.fn(), + } as unknown as Response; + + mockRequest = { + params: { + "0": "test-file.txt", + }, + } as unknown as Request; + }); + + const createMockStream = (): Readable & SdkStreamMixin => { + // Create a minimal mock that implements only what we need + const mockStream = { + pipe: vi.fn().mockReturnThis(), + on: vi.fn().mockReturnThis(), + once: vi.fn().mockReturnThis(), + emit: vi.fn().mockReturnThis(), + transformToByteArray: (): Promise => + Promise.resolve(new Uint8Array()), + transformToWebStream: (): ReadableStream => new ReadableStream(), + }; + + return mockStream as unknown as Readable & SdkStreamMixin; + }; + + it("should successfully retrieve and stream a file", async () => { + const mockStream = createMockStream(); + + const send = vi.fn().mockImplementation(() => + Promise.resolve({ + Body: mockStream, + ContentType: "text/plain", + $metadata: {}, + } as GetObjectCommandOutput), + ); + + vi.mocked(s3Client.send).mockImplementation(send); + + await getFile(mockRequest, mockResponse); + + expect(s3Client.send).toHaveBeenCalledWith(expect.any(GetObjectCommand)); + + const commandCall = vi.mocked(s3Client.send).mock + .calls[0][0] as GetObjectCommand; + expect(commandCall.input).toEqual({ + Bucket: "test-bucket", + Key: "test-file.txt", + }); + + expect(mockResponse.setHeader).toHaveBeenCalledWith( + "Content-Type", + "text/plain", + ); + expect(mockResponse.setHeader).toHaveBeenCalledWith( + "Cross-Origin-Resource-Policy", + "same-site", + ); + + expect(mockStream.pipe).toHaveBeenCalledWith(mockResponse); + }); + + it("should handle errors when retrieving file fails", async () => { + const mockError = new Error("Failed to retrieve file"); + const send = vi.fn().mockImplementation(() => Promise.reject(mockError)); + vi.mocked(s3Client.send).mockImplementation(send); + + const consoleErrorSpy = vi.spyOn(console, "error"); + + await getFile(mockRequest, mockResponse); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Error fetching file:", + mockError, + ); + + expect(mockResponse.status).toHaveBeenCalledWith(500); + expect(mockResponse.send).toHaveBeenCalledWith( + "Error occurred while fetching file", + ); + }); + + it("should handle file paths with subdirectories", async () => { + const mockRequestWithPath = { + params: { + "0": "subfolder/nested/test-file.txt", + }, + } as unknown as Request; + + const mockStream = createMockStream(); + + const send = vi.fn().mockImplementation(() => + Promise.resolve({ + Body: mockStream, + ContentType: "text/plain", + $metadata: {}, + } as GetObjectCommandOutput), + ); + + vi.mocked(s3Client.send).mockImplementation(send); + + await getFile(mockRequestWithPath, mockResponse); + + const commandCall = vi.mocked(s3Client.send).mock + .calls[0][0] as GetObjectCommand; + expect(commandCall.input).toEqual({ + Bucket: "test-bucket", + Key: "subfolder/nested/test-file.txt", + }); + }); +}); diff --git a/tests/resolvers/Query/organizationsMemberConnection.spec.ts b/tests/resolvers/Query/organizationsMemberConnection.spec.ts index 27147bb126..9265454acf 100644 --- a/tests/resolvers/Query/organizationsMemberConnection.spec.ts +++ b/tests/resolvers/Query/organizationsMemberConnection.spec.ts @@ -1094,7 +1094,7 @@ describe("resolvers -> Query -> organizationsMemberConnection", () => { {}, { $set: { - image: `image/image.png`, + image: BASE_URL + `image/image.png`, }, }, ); @@ -1141,7 +1141,7 @@ describe("resolvers -> Query -> organizationsMemberConnection", () => { employmentStatus: user.employmentStatus, firstName: user.firstName, gender: user.gender, - image: `${BASE_URL}${user.image}`, + image: user.image, joinedOrganizations: user.joinedOrganizations, lastName: user.lastName, maritalStatus: user.maritalStatus, diff --git a/tests/resolvers/Query/post.spec.ts b/tests/resolvers/Query/post.spec.ts index 7d6c1afe9e..4f89104ec3 100644 --- a/tests/resolvers/Query/post.spec.ts +++ b/tests/resolvers/Query/post.spec.ts @@ -51,14 +51,9 @@ describe("resolvers -> Query -> post", () => { const post = await Post.findOne({ _id: testPost?._id }) .populate("organization") - .populate("likedBy") + .populate(["likedBy", "file"]) .lean(); - if (post) { - post.imageUrl = `${context.apiRootUrl}${post.imageUrl}`; - post.videoUrl = `${context.apiRootUrl}${post.videoUrl}`; - } - expect(postPayload).toEqual(post); }); it(`returns post object with null image and video links`, async () => { @@ -74,14 +69,9 @@ describe("resolvers -> Query -> post", () => { const post = await Post.findOne({ _id: testPost?._id }) .populate("organization") - .populate("likedBy") + .populate(["likedBy", "file"]) .lean(); - if (post) { - post.imageUrl = null; - post.videoUrl = null; - } - expect(postPayload).toEqual(post); }); }); diff --git a/tests/resolvers/Query/user.spec.ts b/tests/resolvers/Query/user.spec.ts index 3d00aebbc6..23f2c090cc 100644 --- a/tests/resolvers/Query/user.spec.ts +++ b/tests/resolvers/Query/user.spec.ts @@ -1,7 +1,7 @@ import "dotenv/config"; import type mongoose from "mongoose"; import { Types } from "mongoose"; -import { BASE_URL, USER_NOT_FOUND_ERROR } from "../../../src/constants"; +import { USER_NOT_FOUND_ERROR } from "../../../src/constants"; import { User } from "../../../src/models"; import { user as userResolver } from "../../../src/resolvers/Query/user"; import { connect, disconnect } from "../../helpers/db"; @@ -64,37 +64,4 @@ describe("resolvers -> Query -> user", () => { image: null, }); }); - it(`returns user object with image`, async () => { - await User.findOneAndUpdate( - { - _id: testUser?.id, - }, - { - $set: { - image: `images/newImage.png`, - }, - }, - ); - - const args: QueryUserArgs = { - id: testUser?.id, - }; - - const context = { - userId: testUser?.id, - apiRootUrl: BASE_URL, - }; - - const userPayload = await userResolver?.({}, args, context); - - const user = await User.findOne({ - _id: testUser?._id, - }).lean(); - - expect(userPayload?.user).toEqual({ - ...user, - organizationsBlockedBy: [], - image: user?.image ? `${BASE_URL}${user.image}` : null, - }); - }); }); diff --git a/tests/services/createFile.spec.ts b/tests/services/createFile.spec.ts new file mode 100644 index 0000000000..7e096f966d --- /dev/null +++ b/tests/services/createFile.spec.ts @@ -0,0 +1,110 @@ +/** + * module - createFile + * + * This module contains unit tests for the `createFile` function in the file service. + * The tests utilize Vitest for mocking and asserting behavior. + * + * ### Tests + * + * - **Test Case 1**: `should create a new file document when no existing file is found` + * - Verifies that when a file with a specific hash does not exist, a new file document is created in the database. + * - It checks that `File.findOne` is called with the correct hash and that `File.create` is invoked with the expected file details. + * + * - **Test Case 2**: `should increment reference count if an existing file is found` + * - Confirms that if a file with the same hash already exists, the reference count for that file is incremented. + * - It ensures that the `save` method of the existing file is called and that `File.create` is not invoked. + * + * @example + * import \{ createFile \} from "./path/to/createFile"; + * + * const result = await createFile(uploadResult, originalname, mimetype, size); + * + * @see {@link ../../src/REST/services/file#createFile} + * @see {@link ../../src/models#File} + */ + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { File } from "../../src/models"; +import { createFile } from "../../src/REST/services/file"; + +vi.mock("../../src/models", () => ({ + File: { + findOne: vi.fn(), + create: vi.fn(), + }, +})); + +describe("createFile", () => { + const uploadResult = { + hash: "testHash123", + objectKey: "test/file/path", + hashAlgorithm: "sha256", + exists: false, + }; + + const originalname = "image.png"; + const mimetype = "image/png"; + const size = 2048; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should create a new file document when no existing file is found", async () => { + vi.mocked(File.findOne).mockResolvedValueOnce(null); + + const createdFile = File.create({ + fileName: originalname, + mimeType: mimetype, + size: size, + hash: { + value: uploadResult.hash, + algorithm: uploadResult.hashAlgorithm, + }, + uri: `http://localhost:3000/api/file/${uploadResult.objectKey}`, + metadata: { + objectKey: uploadResult.objectKey, + bucketName: "test-bucket", + }, + }); + + const result = await createFile(uploadResult, originalname, mimetype, size); + + expect(result).toEqual(createdFile); + expect(File.findOne).toHaveBeenCalledWith({ + "hash.value": uploadResult.hash, + }); + expect(File.create).toHaveBeenCalledWith({ + fileName: originalname, + mimeType: mimetype, + size: size, + hash: { + value: uploadResult.hash, + algorithm: uploadResult.hashAlgorithm, + }, + uri: expect.any(String), + metadata: { + objectKey: uploadResult.objectKey, + bucketName: expect.any(String), + }, + }); + }); + + it("should increment reference count if an existing file is found", async () => { + const existingFile = { + referenceCount: 1, + save: vi.fn().mockResolvedValueOnce(undefined), + }; + vi.mocked(File.findOne).mockResolvedValueOnce(existingFile); + + const result = await createFile(uploadResult, originalname, mimetype, size); + + expect(result).toEqual(existingFile); + expect(File.findOne).toHaveBeenCalledWith({ + "hash.value": uploadResult.hash, + }); + expect(existingFile.referenceCount).toBe(2); + expect(existingFile.save).toHaveBeenCalled(); + expect(File.create).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/services/deleteFile.spec.ts b/tests/services/deleteFile.spec.ts new file mode 100644 index 0000000000..0acc40f036 --- /dev/null +++ b/tests/services/deleteFile.spec.ts @@ -0,0 +1,123 @@ +import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest"; +import { File } from "../../src/models"; +import { deleteFile } from "../../src/REST/services/file"; +import * as minioServices from "../../src/REST/services/minio"; +import { BUCKET_NAME } from "../../src/config/minio"; +import { isMinioRunning } from "../helpers/minio"; + +vi.mock("../../src/models", () => ({ + File: { + findOne: vi.fn(), + deleteOne: vi.fn(), + prototype: { + save: vi.fn(), + }, + }, +})); + +vi.mock("../../src/REST/controllers/minio", () => ({ + deleteFile: vi.fn(), +})); + +describe("src -> REST -> services -> file -> deleteFile", () => { + const objectKey = "test/file/path"; + const fileId = "12345"; + let serverRunning = false; + + // Wait for server status check before running tests + beforeAll(async () => { + try { + serverRunning = await isMinioRunning(); + } catch (error) { + console.error("Error checking MinIO server status:", error); + serverRunning = false; + } + }); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should return success:false and message if file is not found", async () => { + vi.mocked(File.findOne).mockResolvedValueOnce(null); + + const result = await deleteFile(objectKey, fileId); + + expect(result).toEqual({ success: false, message: "File not found." }); + expect(File.findOne).toHaveBeenCalledWith({ + _id: fileId, + "metadata.objectKey": objectKey, + }); + }); + + it("should decrement reference count if file exists and reference count is greater than 1", async () => { + const mockFile = { + referenceCount: 2, + save: vi.fn().mockResolvedValueOnce(undefined), + }; + vi.mocked(File.findOne).mockResolvedValueOnce(mockFile); + + const result = await deleteFile(objectKey, fileId); + + expect(result).toEqual({ + success: true, + message: "File reference count decreased successfully", + }); + expect(mockFile.referenceCount).toBe(1); + expect(mockFile.save).toHaveBeenCalled(); + }); + + // Use describe block with a dynamic condition + describe("MinIO server integration tests", () => { + beforeEach(async () => { + serverRunning = await isMinioRunning(); + }); + + it("should delete the file from the database and storage if reference count is 1", async () => { + // Skip this test if server is not running + if (!serverRunning) { + console.log("Skipping MinIO test - server not running"); + return; + } + + const mockFile = { + referenceCount: 1, + _id: fileId, + id: fileId, + }; + vi.mocked(File.findOne).mockResolvedValueOnce(mockFile); + + const deleteFileFromBucketSpy = vi.spyOn(minioServices, "deleteFile"); + + const result = await deleteFile(objectKey, fileId); + + expect(result).toEqual({ + success: true, + message: "File deleted successfully", + }); + expect(File.deleteOne).toHaveBeenCalledWith({ _id: fileId }); + expect(deleteFileFromBucketSpy).toHaveBeenCalledWith( + BUCKET_NAME, + objectKey, + ); + }); + }); + + it("should handle errors and return an error message", async () => { + const mockError = new Error("Deletion failed"); + vi.mocked(File.findOne).mockRejectedValueOnce(mockError); + + const consoleErrorSpy = vi.spyOn(console, "error"); + + const result = await deleteFile(objectKey, fileId); + + expect(result).toEqual({ + success: false, + message: "Error occurred while deleting file", + }); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Error deleting file:", + mockError, + ); + }); +}); diff --git a/tests/services/uploadFile.spec.ts b/tests/services/uploadFile.spec.ts new file mode 100644 index 0000000000..6c175fad99 --- /dev/null +++ b/tests/services/uploadFile.spec.ts @@ -0,0 +1,162 @@ +/** + * Tests for the file upload functionality. + * + * This test suite validates the uploadFile service which handles file uploads in the application. + * The service processes incoming files, validates their types, uploads them to storage, and creates + * corresponding database records. + * + * Test Coverage: + * - Input validation for missing files + * - MIME type validation for uploaded files + * - Successful file upload flow including storage and database operations + * - Error handling for upload failures + * + * Mock Strategy: + * - External services (uploadMedia, createFile) are mocked to isolate the upload logic + * - File validation utilities are mocked to control validation responses + * - Express Request/Response objects are mocked with necessary properties + * + * package - REST/services/file + */ + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { Request, Response } from "express"; + +import { isValidMimeType } from "../../src/utilities/isValidMimeType"; +import { errors, requestContext } from "../../src/libraries"; +import { FILE_NOT_FOUND } from "../../src/constants"; +import { BUCKET_NAME } from "../../src/config/minio"; +import { createFile, uploadFile } from "../../src/REST/services/file"; +import { uploadMedia } from "../../src/REST/services/minio"; +import { Types } from "mongoose"; + +vi.mock("../../src/REST/services/minio", () => ({ + uploadMedia: vi.fn(), +})); + +vi.mock("../../src/REST/services/file/createFile", () => ({ + createFile: vi.fn(), +})); + +vi.mock("../../src/utilities/isValidMimeType", () => ({ + isValidMimeType: vi.fn(), +})); + +vi.mock("../../src/libraries/requestContext", () => ({ + translate: (message: string): string => `Translated ${message}`, +})); + +describe("uploadFile", () => { + const mockFile = { + buffer: Buffer.from("mock file content"), + mimetype: "image/png", + originalname: "test-image.png", + size: 1024, + }; + + const mockRequest = { + file: mockFile, + } as unknown as Request; + + const mockResponse = { + status: vi.fn().mockReturnThis(), + json: vi.fn(), + } as unknown as Response; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should return error if no file is uploaded", async () => { + const reqWithoutFile = {} as Request; + + await expect(uploadFile(reqWithoutFile, mockResponse)).rejects.toThrow( + errors.InputValidationError, + ); + + expect(mockResponse.status).toHaveBeenCalledWith(400); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: requestContext.translate(FILE_NOT_FOUND.MESSAGE), + }); + }); + + it("should return error if file type is invalid", async () => { + vi.mocked(isValidMimeType).mockReturnValueOnce(false); + + await expect(uploadFile(mockRequest, mockResponse)).rejects.toThrow( + errors.InputValidationError, + ); + + expect(isValidMimeType).toHaveBeenCalledWith(mockFile.mimetype); + }); + + it("should upload file and return file document data", async () => { + const mockUploadResult = { + hash: "mockhash", + objectKey: "mock-object-key", + hashAlgorithm: "SHA-256", + exists: false, + }; + + const mockFileDoc = { + _id: new Types.ObjectId(), + fileName: "test-image.png", + mimeType: "image/png", + size: 1024, + hash: { + value: "mockhash", + algorithm: "SHA-256", + }, + uri: "http://localhost/api/file/mock-object-key", + referenceCount: 1, + metadata: { + objectKey: "mock-object-key", + }, + encryption: false, + archived: false, + visibility: "PUBLIC" as const, + backupStatus: "", + status: "ACTIVE" as const, + createdAt: new Date(), + updatedAt: new Date(), + }; + + vi.mocked(isValidMimeType).mockReturnValueOnce(true); + vi.mocked(uploadMedia).mockResolvedValueOnce(mockUploadResult); + vi.mocked(createFile).mockResolvedValueOnce(mockFileDoc); + + const result = await uploadFile(mockRequest, mockResponse); + + expect(uploadMedia).toHaveBeenCalledWith( + BUCKET_NAME, + mockFile.buffer, + mockFile.originalname, + { ContentType: mockFile.mimetype }, + ); + expect(createFile).toHaveBeenCalledWith( + mockUploadResult, + mockFile.originalname, + mockFile.mimetype, + mockFile.size, + ); + expect(result).toEqual({ + uri: mockFileDoc.uri, + _id: mockFileDoc._id, + visibility: mockFileDoc.visibility, + objectKey: mockFileDoc.metadata.objectKey, + }); + }); + + it("should return an internal server error if upload fails", async () => { + const mockError = new Error("Failed to upload"); + + vi.mocked(isValidMimeType).mockReturnValueOnce(true); + vi.mocked(uploadMedia).mockRejectedValueOnce(mockError); + + await expect(uploadFile(mockRequest, mockResponse)).rejects.toThrow( + errors.InternalServerError, + ); + + expect(mockResponse.status).not.toHaveBeenCalledWith(400); + }); +}); diff --git a/tests/utilities/createSampleOrganizationUtil.spec.ts b/tests/utilities/createSampleOrganizationUtil.spec.ts index 6e33feab9c..d2c3ccff07 100644 --- a/tests/utilities/createSampleOrganizationUtil.spec.ts +++ b/tests/utilities/createSampleOrganizationUtil.spec.ts @@ -114,7 +114,6 @@ describe("generatePostData function", () => { expect(post.title).toEqual(expect.any(String)); expect(post.creatorId).toEqual(expect.any(Object)); expect(post.organization).toEqual(expect.any(Object)); - expect(post.imageUrl).toEqual(expect.any(String)); expect(post.createdAt).toEqual(expect.any(Date)); }); diff --git a/tests/utilities/deletePreviousFile.spec.ts b/tests/utilities/deletePreviousFile.spec.ts new file mode 100644 index 0000000000..d48d2e8859 --- /dev/null +++ b/tests/utilities/deletePreviousFile.spec.ts @@ -0,0 +1,94 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import type { InterfaceFile } from "../../src/models"; +import { File } from "../../src/models"; +import { deleteFile } from "../../src/REST/services/minio"; +import { BUCKET_NAME } from "../../src/config/minio"; +import { deletePreviousFile } from "../../src/utilities/encodedImageStorage/deletePreviousFile"; +import type { DeleteObjectCommandOutput } from "@aws-sdk/client-s3"; +import type { Document } from "mongoose"; + +type FileDocument = InterfaceFile & Document; + +vi.mock("../../src/models", () => ({ + File: { + findOne: vi.fn(), + deleteOne: vi.fn(), + findOneAndUpdate: vi.fn(), + }, +})); + +vi.mock("../../src/REST/services/minio", () => ({ + deleteFile: vi.fn(), +})); + +describe("deletePreviousFile", () => { + const mockFileId = "test-file-id"; + const mockObjectKey = "test-object-key"; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + it("should delete file and record when referenceCount is 1", async () => { + vi.mocked(File.findOne).mockResolvedValueOnce({ + referenceCount: 1, + } as FileDocument); + + vi.mocked(File.deleteOne).mockResolvedValueOnce({ + acknowledged: true, + deletedCount: 1, + }); + vi.mocked(deleteFile).mockResolvedValueOnce({ + $metadata: {}, + } as DeleteObjectCommandOutput); + + await deletePreviousFile(mockFileId, mockObjectKey); + + expect(File.findOne).toHaveBeenCalledWith({ + _id: mockFileId, + }); + + expect(deleteFile).toHaveBeenCalledWith(BUCKET_NAME, mockObjectKey); + + expect(File.deleteOne).toHaveBeenCalledWith({ + _id: mockFileId, + }); + + expect(File.findOneAndUpdate).not.toHaveBeenCalled(); + }); + + it("should decrement referenceCount when greater than 1", async () => { + vi.mocked(File.findOne).mockResolvedValueOnce({ + referenceCount: 2, + } as FileDocument); + + vi.mocked(File.findOneAndUpdate).mockResolvedValueOnce({ + referenceCount: 1, + } as FileDocument); + + await deletePreviousFile(mockFileId, mockObjectKey); + + expect(File.findOne).toHaveBeenCalledWith({ + _id: mockFileId, + }); + + expect(deleteFile).not.toHaveBeenCalled(); + + expect(File.deleteOne).not.toHaveBeenCalled(); + + expect(File.findOneAndUpdate).toHaveBeenCalledWith( + { + _id: mockFileId, + }, + { + $inc: { + referenceCount: -1, + }, + }, + ); + }); +}); diff --git a/tests/utilities/isValidMimeType.spec.ts b/tests/utilities/isValidMimeType.spec.ts new file mode 100644 index 0000000000..f93a10aee9 --- /dev/null +++ b/tests/utilities/isValidMimeType.spec.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from "vitest"; +import { isValidMimeType } from "../../src/utilities/isValidMimeType"; + +describe("isValidMimeType", () => { + it("should return true for valid image mime types", () => { + expect(isValidMimeType("image/jpeg")).toBe(true); + expect(isValidMimeType("image/png")).toBe(true); + expect(isValidMimeType("image/gif")).toBe(true); + expect(isValidMimeType("image/webp")).toBe(true); + }); + + it("should return true for valid video mime types", () => { + expect(isValidMimeType("video/mp4")).toBe(true); + }); + + it("should return false for invalid mime types", () => { + expect(isValidMimeType("image/bmp")).toBe(false); + expect(isValidMimeType("video/webm")).toBe(false); + expect(isValidMimeType("application/pdf")).toBe(false); + expect(isValidMimeType("")).toBe(false); + expect(isValidMimeType("invalid-mime-type")).toBe(false); + }); +});