Skip to content

Commit

Permalink
implement tagsAssignedWith connection on the User schema
Browse files Browse the repository at this point in the history
  • Loading branch information
meetulr committed Oct 28, 2024
1 parent 2c372d9 commit f467fd8
Show file tree
Hide file tree
Showing 3 changed files with 285 additions and 2 deletions.
4 changes: 2 additions & 2 deletions src/resolvers/User/index.ts
Original file line number Diff line number Diff line change
@@ -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,
};
160 changes: 160 additions & 0 deletions src/resolvers/User/tagsAssignedWith.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
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: (args) =>
parseCursor({
...args,
userId: 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([
TagUser.find({
...filter,
userId: parent._id,
})
.sort(sort)
.limit(parsedArgs.limit)
.populate("tagId")
.lean()
.exec(),

TagUser.find({
userId: parent._id,
})
.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 tagId - The ID of the user to which the tags are assigned.
* @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<ParsedCursor> => {
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,
};
};
123 changes: 123 additions & 0 deletions tests/resolvers/User/tagsAssignedWith.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@

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 { 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;

beforeAll(async () => {
MONGOOSE_INSTANCE = await connect();
[testUser, , [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,
},
{},
);

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());
}
});
});

0 comments on commit f467fd8

Please sign in to comment.