From f0a0f805bef516acd9413f173641735a15d9e783 Mon Sep 17 00:00:00 2001 From: meetul Date: Sat, 13 Jul 2024 21:25:00 +0530 Subject: [PATCH 1/5] add organization userTags connection --- schema.graphql | 3 +- src/resolvers/Organization/index.ts | 3 +- src/resolvers/Organization/userTags.ts | 120 +++++++++++++++++++++++++ src/typeDefs/inputs.ts | 2 +- src/typeDefs/types.ts | 1 + src/types/generatedGraphQLTypes.ts | 4 +- 6 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 src/resolvers/Organization/userTags.ts diff --git a/schema.graphql b/schema.graphql index 0c5d00f364..561bf2ea9c 100644 --- a/schema.graphql +++ b/schema.graphql @@ -336,7 +336,7 @@ input CreateUserTagInput { name: String! organizationId: ID! parentTagId: ID - tagColor: String! + tagColor: String } enum Currency { @@ -1913,6 +1913,7 @@ type UserTag { type UserTagsConnection { edges: [UserTagsConnectionEdge!]! pageInfo: DefaultConnectionPageInfo! + totalCount: PositiveInt } """A default connection edge on the UserTag type for UserTagsConnection.""" diff --git a/src/resolvers/Organization/index.ts b/src/resolvers/Organization/index.ts index de297f5a3b..50bc85c30e 100644 --- a/src/resolvers/Organization/index.ts +++ b/src/resolvers/Organization/index.ts @@ -11,6 +11,7 @@ import { membershipRequests } from "./membershipRequests"; import { pinnedPosts } from "./pinnedPosts"; import { posts } from "./posts"; import { advertisements } from "./advertisements"; +import { userTags } from "./userTags"; import { venues } from "./venues"; // import { userTags } from "./userTags"; @@ -27,7 +28,7 @@ export const Organization: OrganizationResolvers = { membershipRequests, pinnedPosts, funds, + userTags, posts, venues, - // userTags, }; diff --git a/src/resolvers/Organization/userTags.ts b/src/resolvers/Organization/userTags.ts new file mode 100644 index 0000000000..e76b50220a --- /dev/null +++ b/src/resolvers/Organization/userTags.ts @@ -0,0 +1,120 @@ +import type { OrganizationResolvers } from "../../types/generatedGraphQLTypes"; +import type { InterfaceOrganizationTagUser } from "../../models"; +import { OrganizationTagUser } from "../../models"; +import { + getCommonGraphQLConnectionFilter, + getCommonGraphQLConnectionSort, + parseGraphQLConnectionArguments, + transformToDefaultGraphQLConnection, + type DefaultGraphQLArgumentError, + type ParseGraphQLConnectionCursorArguments, + type ParseGraphQLConnectionCursorResult, +} from "../../utilities/graphQLConnection"; +import { GraphQLError } from "graphql"; +import { MAXIMUM_FETCH_LIMIT } from "../../constants"; +import type { Types } from "mongoose"; + +export const userTags: OrganizationResolvers["userTags"] = async ( + parent, + args, +) => { + const parseGraphQLConnectionArgumentsResult = + await parseGraphQLConnectionArguments({ + args, + parseCursor: (args) => + parseCursor({ + ...args, + organizationId: parent._id, + }), + 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([ + OrganizationTagUser.find({ + ...filter, + organizationId: parent._id, + }) + .sort(sort) + .limit(parsedArgs.limit) + .lean() + .exec(), + OrganizationTagUser.find({ + organizationId: parent._id, + }) + .countDocuments() + .exec(), + ]); + + return transformToDefaultGraphQLConnection< + ParsedCursor, + InterfaceOrganizationTagUser, + InterfaceOrganizationTagUser + >({ + objectList, + parsedArgs, + totalCount, + }); +}; + +/* +This is typescript type of the parsed cursor for this connection resolver. +*/ +type ParsedCursor = string; + +/* +This function is used to validate and transform the cursor passed to this connnection +resolver. +*/ +export const parseCursor = async ({ + cursorValue, + cursorName, + cursorPath, + organizationId, +}: ParseGraphQLConnectionCursorArguments & { + organizationId: string | Types.ObjectId; +}): ParseGraphQLConnectionCursorResult => { + const errors: DefaultGraphQLArgumentError[] = []; + const tag = await OrganizationTagUser.findOne({ + _id: cursorValue, + organizationId, + }); + + if (!tag) { + 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/typeDefs/inputs.ts b/src/typeDefs/inputs.ts index 4c819ce973..c15731a238 100644 --- a/src/typeDefs/inputs.ts +++ b/src/typeDefs/inputs.ts @@ -34,7 +34,7 @@ export const inputs = gql` input CreateUserTagInput { name: String! - tagColor: String! + tagColor: String parentTagId: ID organizationId: ID! } diff --git a/src/typeDefs/types.ts b/src/typeDefs/types.ts index fb24e24a54..b275a935e6 100644 --- a/src/typeDefs/types.ts +++ b/src/typeDefs/types.ts @@ -738,6 +738,7 @@ export const types = gql` type UserTagsConnection { edges: [UserTagsConnectionEdge!]! pageInfo: DefaultConnectionPageInfo! + totalCount: PositiveInt } """ diff --git a/src/types/generatedGraphQLTypes.ts b/src/types/generatedGraphQLTypes.ts index 9fd44eaccb..f12b1b3643 100644 --- a/src/types/generatedGraphQLTypes.ts +++ b/src/types/generatedGraphQLTypes.ts @@ -414,7 +414,7 @@ export type CreateUserTagInput = { name: Scalars['String']['input']; organizationId: Scalars['ID']['input']; parentTagId?: InputMaybe; - tagColor: Scalars['String']['input']; + tagColor?: InputMaybe; }; export type Currency = @@ -3020,6 +3020,7 @@ export type UserTagsConnection = { __typename?: 'UserTagsConnection'; edges: Array; pageInfo: DefaultConnectionPageInfo; + totalCount?: Maybe; }; /** A default connection edge on the UserTag type for UserTagsConnection. */ @@ -4706,6 +4707,7 @@ export type UserTagResolvers = { edges?: Resolver, ParentType, ContextType>; pageInfo?: Resolver; + totalCount?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; From 754b21dd1f684bd1585aa404a70a85d2fefb5dbb Mon Sep 17 00:00:00 2001 From: meetul Date: Sat, 13 Jul 2024 22:04:21 +0530 Subject: [PATCH 2/5] add tests --- tests/resolvers/Organization/userTags.spec.ts | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 tests/resolvers/Organization/userTags.spec.ts diff --git a/tests/resolvers/Organization/userTags.spec.ts b/tests/resolvers/Organization/userTags.spec.ts new file mode 100644 index 0000000000..55b7e81856 --- /dev/null +++ b/tests/resolvers/Organization/userTags.spec.ts @@ -0,0 +1,121 @@ +import "dotenv/config"; +import { GraphQLError } from "graphql"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import type { + InterfaceOrganization} from "../../../src/models"; +import { + OrganizationTagUser, +} from "../../../src/models"; +import { + parseCursor, + userTags as userTagsResolver, +} from "../../../src/resolvers/Organization/userTags"; +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 type { TestOrganizationType } from "../../helpers/userAndOrg"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testUserTag1: TestUserTagType, testUserTag2: TestUserTagType; +let testOrganization: TestOrganizationType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [, testOrganization, [testUserTag1, testUserTag2]] = + await createRootTagsWithOrg(2); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("userTags resolver", () => { + const parent = testOrganization?.toObject() as InterfaceOrganization; + it(`throws GraphQLError if invalid arguments are provided to the resolver`, async () => { + try { + await userTagsResolver?.(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 = testOrganization?.toObject() as InterfaceOrganization; + + const connection = await userTagsResolver?.( + parent, + { + first: 2, + }, + {}, + ); + + const totalCount = await OrganizationTagUser.find({ + organizationId: testOrganization?._id, + }).countDocuments(); + + expect(connection).toEqual({ + edges: [ + { + cursor: testUserTag2?._id.toString(), + node: { + ...testUserTag2, + _id: testUserTag2?._id.toString(), + }, + }, + { + cursor: testUserTag1?._id.toString(), + node: { + ...testUserTag1, + _id: testUserTag1?._id.toString(), + }, + }, + ], + pageInfo: { + endCursor: testUserTag1?._id.toString(), + hasNextPage: false, + hasPreviousPage: false, + startCursor: testUserTag2?._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(), + organizationId: testOrganization?._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 result = await parseCursor({ + cursorName: "after", + cursorPath: ["after"], + cursorValue: testUserTag1?._id.toString() as string, + organizationId: testOrganization?._id.toString() as string, + }); + + expect(result.isSuccessful).toEqual(true); + if (result.isSuccessful === true) { + expect(result.parsedCursor).toEqual(testUserTag1?._id.toString()); + } + }); +}); From 63efbc0819e0c2fd4b91b46b6cd4fe06fe344eb9 Mon Sep 17 00:00:00 2001 From: meetul Date: Sat, 13 Jul 2024 22:21:51 +0530 Subject: [PATCH 3/5] fix formatting issue --- tests/resolvers/Organization/userTags.spec.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/resolvers/Organization/userTags.spec.ts b/tests/resolvers/Organization/userTags.spec.ts index 55b7e81856..d3d3e9b948 100644 --- a/tests/resolvers/Organization/userTags.spec.ts +++ b/tests/resolvers/Organization/userTags.spec.ts @@ -3,11 +3,8 @@ import { GraphQLError } from "graphql"; import type mongoose from "mongoose"; import { Types } from "mongoose"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import type { - InterfaceOrganization} from "../../../src/models"; -import { - OrganizationTagUser, -} from "../../../src/models"; +import type { InterfaceOrganization } from "../../../src/models"; +import { OrganizationTagUser } from "../../../src/models"; import { parseCursor, userTags as userTagsResolver, From c1ea23c4dc555f08c51fd251eba603c298b714b4 Mon Sep 17 00:00:00 2001 From: meetul Date: Thu, 15 Aug 2024 17:42:54 +0530 Subject: [PATCH 4/5] adjustments and queries for tag screens --- src/resolvers/Mutation/assignUserTag.ts | 30 +++++++-- src/resolvers/Mutation/unassignUserTag.ts | 31 ++++++++- src/resolvers/Mutation/updateUserTag.ts | 4 +- src/resolvers/Organization/userTags.ts | 1 + src/resolvers/Query/getUserTag.ts | 21 ++++++ src/resolvers/Query/getUserTagAncestors.ts | 32 ++++++++++ src/resolvers/Query/index.ts | 4 ++ src/typeDefs/inputs.ts | 4 +- src/typeDefs/queries.ts | 4 ++ src/typeDefs/types.ts | 3 +- src/types/generatedGraphQLTypes.ts | 24 +++++-- .../resolvers/Mutation/assignUserTag.spec.ts | 51 +++++++++++++-- .../Mutation/unassignUserTag.spec.ts | 57 ++++++++++++++++- .../resolvers/Mutation/updateUserTag.spec.ts | 12 ++-- tests/resolvers/Query/getUserTag.spec.ts | 64 +++++++++++++++++++ .../Query/getUserTagAncestors.spec.ts | 61 ++++++++++++++++++ 16 files changed, 375 insertions(+), 28 deletions(-) create mode 100644 src/resolvers/Query/getUserTag.ts create mode 100644 src/resolvers/Query/getUserTagAncestors.ts create mode 100644 tests/resolvers/Query/getUserTag.spec.ts create mode 100644 tests/resolvers/Query/getUserTagAncestors.spec.ts diff --git a/src/resolvers/Mutation/assignUserTag.ts b/src/resolvers/Mutation/assignUserTag.ts index 37d4628d3c..ee2186642d 100644 --- a/src/resolvers/Mutation/assignUserTag.ts +++ b/src/resolvers/Mutation/assignUserTag.ts @@ -152,10 +152,32 @@ export const assignUserTag: MutationResolvers["assignUserTag"] = async ( ); } - // Assign the tag - await TagUser.create({ - ...args.input, - }); + // assign all the ancestor tags + const allAncestorTags = [tag._id]; + let currentTag = tag; + while (currentTag?.parentTagId) { + const currentParentTag = await OrganizationTagUser.findOne({ + _id: currentTag.parentTagId, + }).lean(); + + if (currentParentTag) { + allAncestorTags.push(currentParentTag?._id); + currentTag = currentParentTag; + } + } + + const assigneeId = args.input.userId; + + const tagUserDocs = allAncestorTags.map((tagId) => ({ + updateOne: { + filter: { userId: assigneeId, tagId }, + update: { $setOnInsert: { userId: assigneeId, tagId } }, + upsert: true, + setDefaultsOnInsert: true, + }, + })); + + await TagUser.bulkWrite(tagUserDocs); return requestUser; }; diff --git a/src/resolvers/Mutation/unassignUserTag.ts b/src/resolvers/Mutation/unassignUserTag.ts index 120da46f15..cd19f6d891 100644 --- a/src/resolvers/Mutation/unassignUserTag.ts +++ b/src/resolvers/Mutation/unassignUserTag.ts @@ -145,9 +145,36 @@ export const unassignUserTag: MutationResolvers["unassignUserTag"] = async ( ); } + // Get all the child tags of the current tag (including itself) + // on the OrganizationTagUser model + // The following implementation makes number of queries = max depth of nesting in the tag provided + let allTagIds: string[] = []; + let currentParents = [tag._id.toString()]; + + while (currentParents.length) { + allTagIds = allTagIds.concat(currentParents); + const foundTags = await OrganizationTagUser.find( + { + organizationId: tag.organizationId, + parentTagId: { + $in: currentParents, + }, + }, + { + _id: 1, + }, + ); + currentParents = foundTags + .map((tag) => tag._id.toString()) + .filter((id: string | null) => id); + } + // Unassign the tag - await TagUser.deleteOne({ - ...args.input, + await TagUser.deleteMany({ + tagId: { + $in: allTagIds, + }, + userId: args.input.userId, }); return requestUser; diff --git a/src/resolvers/Mutation/updateUserTag.ts b/src/resolvers/Mutation/updateUserTag.ts index 05513c0716..794ca3fb15 100644 --- a/src/resolvers/Mutation/updateUserTag.ts +++ b/src/resolvers/Mutation/updateUserTag.ts @@ -74,7 +74,7 @@ export const updateUserTag: MutationResolvers["updateUserTag"] = async ( // Get the tag object const existingTag = await OrganizationTagUser.findOne({ - _id: args.input._id, + _id: args.input.tagId, }).lean(); if (!existingTag) { @@ -130,7 +130,7 @@ export const updateUserTag: MutationResolvers["updateUserTag"] = async ( // Update the title of the tag and return it return await OrganizationTagUser.findOneAndUpdate( { - _id: args.input._id, + _id: args.input.tagId, }, { name: args.input.name, diff --git a/src/resolvers/Organization/userTags.ts b/src/resolvers/Organization/userTags.ts index 6ccdf71d5b..2b4160cf36 100644 --- a/src/resolvers/Organization/userTags.ts +++ b/src/resolvers/Organization/userTags.ts @@ -72,6 +72,7 @@ export const userTags: OrganizationResolvers["userTags"] = async ( OrganizationTagUser.find({ ...filter, organizationId: parent._id, + parentTagId: null, }) .sort(sort) .limit(parsedArgs.limit) diff --git a/src/resolvers/Query/getUserTag.ts b/src/resolvers/Query/getUserTag.ts new file mode 100644 index 0000000000..cb6e5cfa1c --- /dev/null +++ b/src/resolvers/Query/getUserTag.ts @@ -0,0 +1,21 @@ +import { OrganizationTagUser } from "../../models"; +import { errors, requestContext } from "../../libraries"; +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import { TAG_NOT_FOUND } from "../../constants"; + +export const getUserTag: QueryResolvers["getUserTag"] = async ( + _parent, + args, +) => { + const userTag = await OrganizationTagUser.findById(args.id).lean(); + + if (!userTag) { + throw new errors.NotFoundError( + requestContext.translate(TAG_NOT_FOUND.MESSAGE), + TAG_NOT_FOUND.CODE, + TAG_NOT_FOUND.PARAM, + ); + } + + return userTag; +}; diff --git a/src/resolvers/Query/getUserTagAncestors.ts b/src/resolvers/Query/getUserTagAncestors.ts new file mode 100644 index 0000000000..eb4b3fcb27 --- /dev/null +++ b/src/resolvers/Query/getUserTagAncestors.ts @@ -0,0 +1,32 @@ +import { OrganizationTagUser } from "../../models"; +import { errors, requestContext } from "../../libraries"; +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import { TAG_NOT_FOUND } from "../../constants"; + +export const getUserTagAncestors: QueryResolvers["getUserTagAncestors"] = + async (_parent, args) => { + let currentTag = await OrganizationTagUser.findById(args.id).lean(); + + const tagAncestors = [currentTag]; + + while (currentTag?.parentTagId) { + const currentParent = await OrganizationTagUser.findById( + currentTag.parentTagId, + ).lean(); + + if (currentParent) { + tagAncestors.push(currentParent); + currentTag = currentParent; + } + } + + if (!currentTag) { + throw new errors.NotFoundError( + requestContext.translate(TAG_NOT_FOUND.MESSAGE), + TAG_NOT_FOUND.CODE, + TAG_NOT_FOUND.PARAM, + ); + } + + return tagAncestors.reverse(); + }; diff --git a/src/resolvers/Query/index.ts b/src/resolvers/Query/index.ts index 8b1410ad35..fc878303d8 100644 --- a/src/resolvers/Query/index.ts +++ b/src/resolvers/Query/index.ts @@ -34,6 +34,8 @@ import { getFundraisingCampaigns } from "./getFundraisingCampaigns"; 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"; @@ -84,6 +86,8 @@ export const Query: QueryResolvers = { getNoteById, getlanguage, getPlugins, + getUserTag, + getUserTagAncestors, isSampleOrganization, me, myLanguage, diff --git a/src/typeDefs/inputs.ts b/src/typeDefs/inputs.ts index ac5a99320c..043eaacfa3 100644 --- a/src/typeDefs/inputs.ts +++ b/src/typeDefs/inputs.ts @@ -490,8 +490,8 @@ export const inputs = gql` } input UpdateUserTagInput { - _id: ID! - tagColor: String! + tagId: ID! + tagColor: String name: String! } diff --git a/src/typeDefs/queries.ts b/src/typeDefs/queries.ts index bea4e3a501..f1a51bca0a 100644 --- a/src/typeDefs/queries.ts +++ b/src/typeDefs/queries.ts @@ -125,6 +125,10 @@ export const queries = gql` getNoteById(id: ID!): Note! + 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 76796c96f0..16fc7783f8 100644 --- a/src/typeDefs/types.ts +++ b/src/typeDefs/types.ts @@ -742,7 +742,7 @@ export const types = gql` type UserTagsConnection { edges: [UserTagsConnectionEdge!]! pageInfo: DefaultConnectionPageInfo! - totalCount: PositiveInt + totalCount: Int } """ @@ -759,6 +759,7 @@ export const types = gql` type UsersConnection { edges: [UsersConnectionEdge!]! pageInfo: DefaultConnectionPageInfo! + totalCount: Int } """ diff --git a/src/types/generatedGraphQLTypes.ts b/src/types/generatedGraphQLTypes.ts index 33399964aa..bc7cb68800 100644 --- a/src/types/generatedGraphQLTypes.ts +++ b/src/types/generatedGraphQLTypes.ts @@ -2306,6 +2306,8 @@ export type Query = { getNoteById: Note; getPledgesByUserId?: Maybe>>; getPlugins?: Maybe>>; + getUserTag?: Maybe; + getUserTagAncestors?: Maybe>>; getVenueByOrgId?: Maybe>>; getlanguage?: Maybe>>; groupChatById?: Maybe; @@ -2521,6 +2523,16 @@ export type QueryGetPledgesByUserIdArgs = { }; +export type QueryGetUserTagArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryGetUserTagAncestorsArgs = { + id: Scalars['ID']['input']; +}; + + export type QueryGetVenueByOrgIdArgs = { first?: InputMaybe; orderBy?: InputMaybe; @@ -2908,9 +2920,9 @@ export type UpdateUserPasswordInput = { }; export type UpdateUserTagInput = { - _id: Scalars['ID']['input']; name: Scalars['String']['input']; - tagColor: Scalars['String']['input']; + tagColor?: InputMaybe; + tagId: Scalars['ID']['input']; }; export type User = { @@ -3084,7 +3096,7 @@ export type UserTagsConnection = { __typename?: 'UserTagsConnection'; edges: Array; pageInfo: DefaultConnectionPageInfo; - totalCount?: Maybe; + totalCount?: Maybe; }; /** A default connection edge on the UserTag type for UserTagsConnection. */ @@ -3133,6 +3145,7 @@ export type UsersConnection = { __typename?: 'UsersConnection'; edges: Array; pageInfo: DefaultConnectionPageInfo; + totalCount?: Maybe; }; /** A default connection edge on the User type for UsersConnection. */ @@ -4600,6 +4613,8 @@ export type QueryResolvers>; getPledgesByUserId?: Resolver>>, 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>; groupChatById?: Resolver, ParentType, ContextType, RequireFields>; @@ -4784,7 +4799,7 @@ export type UserTagResolvers = { edges?: Resolver, ParentType, ContextType>; pageInfo?: Resolver; - totalCount?: Resolver, ParentType, ContextType>; + totalCount?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -4797,6 +4812,7 @@ export type UserTagsConnectionEdgeResolvers = { edges?: Resolver, ParentType, ContextType>; pageInfo?: Resolver; + totalCount?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/tests/resolvers/Mutation/assignUserTag.spec.ts b/tests/resolvers/Mutation/assignUserTag.spec.ts index 2b5d428144..0bf9190468 100644 --- a/tests/resolvers/Mutation/assignUserTag.spec.ts +++ b/tests/resolvers/Mutation/assignUserTag.spec.ts @@ -22,19 +22,28 @@ import { } from "../../../src/constants"; import { AppUserProfile, TagUser } from "../../../src/models"; import type { TestUserTagType } from "../../helpers/tags"; -import { createRootTagWithOrg } from "../../helpers/tags"; +import { + createRootTagWithOrg, + createTwoLevelTagsWithOrg, +} from "../../helpers/tags"; import type { TestUserType } from "../../helpers/userAndOrg"; import { createTestUser } from "../../helpers/userAndOrg"; let MONGOOSE_INSTANCE: typeof mongoose; let adminUser: TestUserType; +let adminUser2: TestUserType; +let testTag2: TestUserTagType; let testTag: TestUserTagType; +let testSubTag1: TestUserTagType; +let testSubTag2: TestUserTagType; let randomUser: TestUserType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); - [adminUser, , testTag] = await createRootTagWithOrg(); + [adminUser, , [testTag, testSubTag1, testSubTag2]] = + await createTwoLevelTagsWithOrg(); + [adminUser2, , testTag2] = await createRootTagWithOrg(); randomUser = await createTestUser(); }); @@ -210,10 +219,36 @@ describe("resolvers -> Mutation -> assignUserTag", () => { }); it(`Tag assign should be successful and the user who has been assigned the tag is returned`, async () => { + const args: MutationAssignUserTagArgs = { + input: { + userId: adminUser2?._id, + tagId: testTag2?._id.toString() ?? "", + }, + }; + const context = { + userId: adminUser2?._id, + }; + + const { assignUserTag: assignUserTagResolver } = await import( + "../../../src/resolvers/Mutation/assignUserTag" + ); + + const payload = await assignUserTagResolver?.({}, args, context); + + expect(payload?._id.toString()).toEqual(adminUser2?._id.toString()); + + const tagAssigned = await TagUser.exists({ + ...args.input, + }); + + expect(tagAssigned).toBeTruthy(); + }); + + it(`Should assign all the ancestor tags and returns the user that is assigned`, async () => { const args: MutationAssignUserTagArgs = { input: { userId: adminUser?._id, - tagId: testTag?._id.toString() ?? "", + tagId: testSubTag2?._id.toString() ?? "", }, }; const context = { @@ -228,11 +263,17 @@ describe("resolvers -> Mutation -> assignUserTag", () => { expect(payload?._id.toString()).toEqual(adminUser?._id.toString()); - const tagAssigned = await TagUser.exists({ + const subTagAssigned = await TagUser.exists({ ...args.input, }); - expect(tagAssigned).toBeTruthy(); + const ancestorTagAssigned = await TagUser.exists({ + ...args.input, + tagId: testTag?._id.toString() ?? "", + }); + + expect(subTagAssigned).toBeTruthy(); + expect(ancestorTagAssigned).toBeTruthy(); }); it(`Throws USER_ALREADY_HAS_TAG error if tag with _id === args.input.tagId is already assigned to user with _id === args.input.userId`, async () => { diff --git a/tests/resolvers/Mutation/unassignUserTag.spec.ts b/tests/resolvers/Mutation/unassignUserTag.spec.ts index 5a6ea938d7..6b75cf76f1 100644 --- a/tests/resolvers/Mutation/unassignUserTag.spec.ts +++ b/tests/resolvers/Mutation/unassignUserTag.spec.ts @@ -21,7 +21,7 @@ import { } from "../../../src/constants"; import { AppUserProfile, TagUser } from "../../../src/models"; import type { TestUserTagType } from "../../helpers/tags"; -import { createRootTagWithOrg } from "../../helpers/tags"; +import { createTwoLevelTagsWithOrg } from "../../helpers/tags"; import type { TestUserType } from "../../helpers/userAndOrg"; import { createTestUser } from "../../helpers/userAndOrg"; @@ -29,11 +29,14 @@ let MONGOOSE_INSTANCE: typeof mongoose; let adminUser: TestUserType; let testTag: TestUserTagType; +let testSubTag1: TestUserTagType; +let testSubTag2: TestUserTagType; let randomUser: TestUserType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); - [adminUser, , testTag] = await createRootTagWithOrg(); + [adminUser, , [testTag, testSubTag1, testSubTag2]] = + await createTwoLevelTagsWithOrg(); randomUser = await createTestUser(); }); @@ -244,6 +247,56 @@ describe("resolvers -> Mutation -> unassignUserTag", () => { expect(tagAssigned).toBeFalsy(); }); + + it(`should unassign all the child tags and decendent tags of a parent tag and return the user`, async () => { + const { requestContext } = await import("../../../src/libraries"); + + vi.spyOn(requestContext, "translate").mockImplementationOnce( + (message) => `Translated ${message}`, + ); + + const args: MutationUnassignUserTagArgs = { + input: { + userId: adminUser?._id, + tagId: testTag ? testTag._id.toString() : "", + }, + }; + const context = { + userId: adminUser?._id, + }; + + // Assign the parent and sub tag to the user + await TagUser.create({ + ...args.input, + }); + + await TagUser.create({ + ...args.input, + tagId: testSubTag1 ? testSubTag1._id.toString() : "", + }); + + // Test the unassignUserTag resolver + const { unassignUserTag: unassignUserTagResolver } = await import( + "../../../src/resolvers/Mutation/unassignUserTag" + ); + + const payload = await unassignUserTagResolver?.({}, args, context); + + expect(payload?._id.toString()).toEqual(adminUser?._id.toString()); + + const tagAssigned = await TagUser.exists({ + ...args.input, + }); + + const subTagAssigned = await TagUser.exists({ + ...args.input, + tagId: testSubTag1 ? testSubTag1._id.toString() : "", + }); + + expect(tagAssigned).toBeFalsy(); + expect(subTagAssigned).toBeFalsy(); + }); + it("throws an error if the user does not have appUserProfile", async () => { const { requestContext } = await import("../../../src/libraries"); const spy = vi diff --git a/tests/resolvers/Mutation/updateUserTag.spec.ts b/tests/resolvers/Mutation/updateUserTag.spec.ts index 60f2099498..87280b4bc3 100644 --- a/tests/resolvers/Mutation/updateUserTag.spec.ts +++ b/tests/resolvers/Mutation/updateUserTag.spec.ts @@ -60,7 +60,7 @@ describe("resolvers -> Mutation -> updateUserTag", () => { const args: MutationUpdateUserTagArgs = { input: { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - _id: testTag!._id.toString(), + tagId: testTag!._id.toString(), name: "NewName", tagColor: "#000000", }, @@ -92,7 +92,7 @@ describe("resolvers -> Mutation -> updateUserTag", () => { try { const args: MutationUpdateUserTagArgs = { input: { - _id: new Types.ObjectId().toString(), + tagId: new Types.ObjectId().toString(), name: "NewName", tagColor: "#000000", }, @@ -125,7 +125,7 @@ describe("resolvers -> Mutation -> updateUserTag", () => { const args: MutationUpdateUserTagArgs = { input: { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - _id: testTag!._id.toString(), + tagId: testTag!._id.toString(), name: "NewName", tagColor: "#000000", }, @@ -162,7 +162,7 @@ describe("resolvers -> Mutation -> updateUserTag", () => { const args: MutationUpdateUserTagArgs = { input: { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - _id: testTag!._id.toString(), + tagId: testTag!._id.toString(), // eslint-disable-next-line @typescript-eslint/no-non-null-assertion name: testTag!.name, tagColor: "#000000", @@ -198,7 +198,7 @@ describe("resolvers -> Mutation -> updateUserTag", () => { const args: MutationUpdateUserTagArgs = { input: { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - _id: testTag!._id.toString(), + tagId: testTag!._id.toString(), // eslint-disable-next-line @typescript-eslint/no-non-null-assertion name: testTag2!.name, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -226,7 +226,7 @@ describe("resolvers -> Mutation -> updateUserTag", () => { const args: MutationUpdateUserTagArgs = { input: { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - _id: testTag!._id.toString(), + tagId: testTag!._id.toString(), name: "NewName", tagColor: "#000000", }, diff --git a/tests/resolvers/Query/getUserTag.spec.ts b/tests/resolvers/Query/getUserTag.spec.ts new file mode 100644 index 0000000000..20f0abc520 --- /dev/null +++ b/tests/resolvers/Query/getUserTag.spec.ts @@ -0,0 +1,64 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; +import { connect, disconnect } from "../../helpers/db"; + +import { getUserTag as getUserTagResolver } from "../../../src/resolvers/Query/getUserTag"; +import type { + QueryGetUserTagAncestorsArgs, + QueryGetUserTagArgs, +} from "../../../src/types/generatedGraphQLTypes"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import { TestUserType } from "../../helpers/userAndOrg"; +import { + createRootTagWithOrg, + createTwoLevelTagsWithOrg, + TestUserTagType, +} from "../../helpers/tags"; +import { TAG_NOT_FOUND } from "../../../src/constants"; + +let MONGOOSE_INSTANCE: typeof mongoose; + +let testTag: TestUserTagType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [, , testTag] = await createRootTagWithOrg(); +}); + +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: QueryGetUserTagArgs = { + id: new Types.ObjectId().toString(), + }; + + await getUserTagResolver?.({}, 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 tag with _id === args.id`, async () => { + const args: QueryGetUserTagArgs = { + id: testTag?._id.toString() ?? "", + }; + + const getUserTagAncestorsPayload = await getUserTagResolver?.({}, args, {}); + + expect(getUserTagAncestorsPayload).toEqual(testTag); + }); +}); diff --git a/tests/resolvers/Query/getUserTagAncestors.spec.ts b/tests/resolvers/Query/getUserTagAncestors.spec.ts new file mode 100644 index 0000000000..3eed95a84e --- /dev/null +++ b/tests/resolvers/Query/getUserTagAncestors.spec.ts @@ -0,0 +1,61 @@ +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, 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]); + }); +}); From b0e898bc89c6e797919866248028bf39fdbeabe9 Mon Sep 17 00:00:00 2001 From: meetul Date: Thu, 15 Aug 2024 18:40:22 +0530 Subject: [PATCH 5/5] fix linting --- schema.graphql | 9 ++++++--- tests/resolvers/Mutation/assignUserTag.spec.ts | 6 ++---- tests/resolvers/Mutation/unassignUserTag.spec.ts | 4 +--- tests/resolvers/Query/getUserTag.spec.ts | 12 ++---------- tests/resolvers/Query/getUserTagAncestors.spec.ts | 5 ++++- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/schema.graphql b/schema.graphql index c5a1c8d502..c55d48ef34 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1514,6 +1514,8 @@ type Query { getNoteById(id: ID!): Note! 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] groupChatById(id: ID!): GroupChat @@ -1799,9 +1801,9 @@ input UpdateUserPasswordInput { } input UpdateUserTagInput { - _id: ID! name: String! - tagColor: String! + tagColor: String + tagId: ID! } scalar Upload @@ -1939,7 +1941,7 @@ type UserTag { type UserTagsConnection { edges: [UserTagsConnectionEdge!]! pageInfo: DefaultConnectionPageInfo! - totalCount: PositiveInt + totalCount: Int } """A default connection edge on the UserTag type for UserTagsConnection.""" @@ -1987,6 +1989,7 @@ input UserWhereInput { type UsersConnection { edges: [UsersConnectionEdge!]! pageInfo: DefaultConnectionPageInfo! + totalCount: Int } """A default connection edge on the User type for UsersConnection.""" diff --git a/tests/resolvers/Mutation/assignUserTag.spec.ts b/tests/resolvers/Mutation/assignUserTag.spec.ts index 0bf9190468..92281363d2 100644 --- a/tests/resolvers/Mutation/assignUserTag.spec.ts +++ b/tests/resolvers/Mutation/assignUserTag.spec.ts @@ -36,13 +36,11 @@ let adminUser2: TestUserType; let testTag2: TestUserTagType; let testTag: TestUserTagType; let testSubTag1: TestUserTagType; -let testSubTag2: TestUserTagType; let randomUser: TestUserType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); - [adminUser, , [testTag, testSubTag1, testSubTag2]] = - await createTwoLevelTagsWithOrg(); + [adminUser, , [testTag, testSubTag1]] = await createTwoLevelTagsWithOrg(); [adminUser2, , testTag2] = await createRootTagWithOrg(); randomUser = await createTestUser(); }); @@ -248,7 +246,7 @@ describe("resolvers -> Mutation -> assignUserTag", () => { const args: MutationAssignUserTagArgs = { input: { userId: adminUser?._id, - tagId: testSubTag2?._id.toString() ?? "", + tagId: testSubTag1?._id.toString() ?? "", }, }; const context = { diff --git a/tests/resolvers/Mutation/unassignUserTag.spec.ts b/tests/resolvers/Mutation/unassignUserTag.spec.ts index 6b75cf76f1..16ce061c8f 100644 --- a/tests/resolvers/Mutation/unassignUserTag.spec.ts +++ b/tests/resolvers/Mutation/unassignUserTag.spec.ts @@ -30,13 +30,11 @@ let MONGOOSE_INSTANCE: typeof mongoose; let adminUser: TestUserType; let testTag: TestUserTagType; let testSubTag1: TestUserTagType; -let testSubTag2: TestUserTagType; let randomUser: TestUserType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); - [adminUser, , [testTag, testSubTag1, testSubTag2]] = - await createTwoLevelTagsWithOrg(); + [adminUser, , [testTag, testSubTag1]] = await createTwoLevelTagsWithOrg(); randomUser = await createTestUser(); }); diff --git a/tests/resolvers/Query/getUserTag.spec.ts b/tests/resolvers/Query/getUserTag.spec.ts index 20f0abc520..f81f51d960 100644 --- a/tests/resolvers/Query/getUserTag.spec.ts +++ b/tests/resolvers/Query/getUserTag.spec.ts @@ -4,17 +4,9 @@ import { Types } from "mongoose"; import { connect, disconnect } from "../../helpers/db"; import { getUserTag as getUserTagResolver } from "../../../src/resolvers/Query/getUserTag"; -import type { - QueryGetUserTagAncestorsArgs, - QueryGetUserTagArgs, -} from "../../../src/types/generatedGraphQLTypes"; +import type { QueryGetUserTagArgs } from "../../../src/types/generatedGraphQLTypes"; import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; -import { TestUserType } from "../../helpers/userAndOrg"; -import { - createRootTagWithOrg, - createTwoLevelTagsWithOrg, - TestUserTagType, -} from "../../helpers/tags"; +import { createRootTagWithOrg, type TestUserTagType } from "../../helpers/tags"; import { TAG_NOT_FOUND } from "../../../src/constants"; let MONGOOSE_INSTANCE: typeof mongoose; diff --git a/tests/resolvers/Query/getUserTagAncestors.spec.ts b/tests/resolvers/Query/getUserTagAncestors.spec.ts index 3eed95a84e..2a3ee00143 100644 --- a/tests/resolvers/Query/getUserTagAncestors.spec.ts +++ b/tests/resolvers/Query/getUserTagAncestors.spec.ts @@ -6,7 +6,10 @@ 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, TestUserTagType } from "../../helpers/tags"; +import { + createTwoLevelTagsWithOrg, + type TestUserTagType, +} from "../../helpers/tags"; import { TAG_NOT_FOUND } from "../../../src/constants"; let MONGOOSE_INSTANCE: typeof mongoose;