diff --git a/src/libraries/validators/validatePaginationArgs.ts b/src/libraries/validators/validatePaginationArgs.ts deleted file mode 100644 index 9b431fd6bd2..00000000000 --- a/src/libraries/validators/validatePaginationArgs.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MAXIMUM_FETCH_LIMIT } from "../../constants"; -import type { - ConnectionError, - CursorPaginationInput, -} from "../../types/generatedGraphQLTypes"; - -export const validatePaginationArgs = ( - args: CursorPaginationInput, -): ConnectionError[] => { - const connectionErrors: ConnectionError[] = []; - - // Ensure that limit is less than the maximum allowed fetch limit - if (args.limit > MAXIMUM_FETCH_LIMIT) { - connectionErrors.push({ - __typename: "MaximumValueError", - message: - "More items than the allowed number of items were requested to be fetched.", - limit: MAXIMUM_FETCH_LIMIT, - path: ["input", "limit"], - }); - } - - return connectionErrors; -}; diff --git a/src/utilities/graphqlConnectionFactory.ts b/src/utilities/graphqlConnectionFactory.ts deleted file mode 100644 index e78c9db7a01..00000000000 --- a/src/utilities/graphqlConnectionFactory.ts +++ /dev/null @@ -1,161 +0,0 @@ -import type { - ConnectionPageInfo, - ConnectionError, - CursorPaginationInput, -} from "../types/generatedGraphQLTypes"; -import type { Types } from "mongoose"; - -interface InterfaceConnectionEdge { - cursor: string; - node: T; -} - -interface InterfaceConnection { - edges: InterfaceConnectionEdge[]; - pageInfo: ConnectionPageInfo; -} - -interface InterfaceConnectionResult { - data: InterfaceConnection | null; - errors: ConnectionError[]; -} - -type GetNodeFromResultFnType = { - (result: T2): T1; -}; - -/* -This is a factory function to quickly create a graphql connection object. The function accepts a generic type -'T' which is used to reference the type of node that this connection and it's edges will reference. A node is -a business object which can be uniquely identified in graphql. For example `User`, `Organization`, `Event`, `Post` etc. -All of these objects are viable candiates for a node and can be paginated using graphql connections. The default object returned by this function represents a connection which has no data at all, i.e., the table/collection for that node(along with ther constraints like filters if any) is completely empty in database. -This object will need to be transformed according to different logic inside resolvers. -*/ -export function graphqlConnectionFactory(): InterfaceConnection { - return { - edges: [], - pageInfo: { - startCursor: null, - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - }, - }; -} -// Generates the limit that can should be passed in the .limit() method -export const getLimit = (limit: number): number => { - // We always fetch 1 object more than args.limit - // so that we can use that to get the information about hasNextPage / hasPreviousPage - return limit + 1; -}; - -// Generates the sortingObject that can be passed in the .sort() method -export const getSortingObject = ( - direction: "FORWARD" | "BACKWARD", - sortingObject: Record, -): Record => { - // We assume that the resolver would always be written with respect to the sorting that needs to be implemented for forward pagination - if (direction === "FORWARD") return sortingObject; - - // If we are paginating backwards, then we must reverse the order of all fields that are being sorted by. - for (const [key, value] of Object.entries(sortingObject)) { - sortingObject[key] = value * -1; - } - - return sortingObject; -}; - -type FilterObjectType = { - _id: { - [key: string]: string; - }; -}; - -// Generates the sorting arguments for filterQuery that can be passed into the .find() method -export function getFilterObject( - args: CursorPaginationInput, -): FilterObjectType | null { - if (args.cursor) { - if (args.direction === "FORWARD") return { _id: { $gt: args.cursor } }; - else return { _id: { $lt: args.cursor } }; - } - - return null; -} - -/* -This is a function which generates a GraphQL cursor pagination object based on the requirements of the client. - -The function takes the following arguments: - -A. TYPE PARAMETERS - -1. T1: Refers the type of the node that the connection and it's edges would refer. -Example values include `Interface_User`, `Interface_Organization`, `Interface_Event`, `Interface_Post`. -2. T2: Regers to the type of interface that is implemented by the model that you want to query. -For example, if you want to query the TagUser Model, then you would send Interface_UserTag for this type parameter - -B. DATA PARAMETERS - -1. args: These are the tranformed arguments that were orginally passed in the request. -2. allFetchedObjects: Refers to objects that were fetched from the database thorugh a query. -3. getNodeFromResult: Describes a transformation function that given an object of type T2, would convert it to the desired object of type T1. This would mostly include mapping to some specific field of the fetched object. - -The function returns a promise which would resolve to the desired connection object (of the type InterfaceConnection). -*/ -export function generateConnectionObject< - T1 extends { _id: Types.ObjectId }, - T2 extends { _id: Types.ObjectId }, ->( - args: CursorPaginationInput, - allFetchedObjects: T2[] | null, - getNodeFromResult: GetNodeFromResultFnType, -): InterfaceConnectionResult { - // Initialize the connection object - const connectionObject = graphqlConnectionFactory(); - - // Return the default object if the recieved list is empty - if (!allFetchedObjects || allFetchedObjects.length === 0) - return { - data: connectionObject, - errors: [], - }; - - // Handling the case when the cursor is provided - if (args.cursor) { - // Populate the relevant pageInfo fields - if (args.direction === "FORWARD") - connectionObject.pageInfo.hasPreviousPage = true; - else connectionObject.pageInfo.hasNextPage = true; - } - - // Populate the page pointer variable - if (allFetchedObjects?.length === args.limit + 1) { - if (args.direction === "FORWARD") - connectionObject.pageInfo.hasNextPage = true; - else connectionObject.pageInfo.hasPreviousPage = true; - allFetchedObjects?.pop(); - } - - // Reverse the order of the fetched objects in backward pagination, - // as according to the Relay Specification, the order of - // returned objects must always be ascending on the basis of the cursor used - if (args.direction === "BACKWARD") - allFetchedObjects = allFetchedObjects?.reverse(); - - // Create edges from the fetched objects - connectionObject.edges = allFetchedObjects?.map((object: T2) => ({ - node: getNodeFromResult(object), - cursor: object._id.toString(), - })); - - // Set the start and end cursor - connectionObject.pageInfo.startCursor = connectionObject.edges[0]?.cursor; - connectionObject.pageInfo.endCursor = - connectionObject.edges[connectionObject.edges.length - 1]?.cursor; - - return { - data: connectionObject, - errors: [], - }; -} diff --git a/tests/utilities/graphqlConnectionFactory.spec.ts b/tests/utilities/graphqlConnectionFactory.spec.ts deleted file mode 100644 index d272ca6eba3..00000000000 --- a/tests/utilities/graphqlConnectionFactory.spec.ts +++ /dev/null @@ -1,414 +0,0 @@ -import { beforeAll, describe, expect, it } from "vitest"; -import { - graphqlConnectionFactory, - getLimit, - getFilterObject, - getSortingObject, - generateConnectionObject, -} from "../../src/utilities/graphqlConnectionFactory"; -import { type CursorPaginationInput } from "../../src/types/generatedGraphQLTypes"; -import { Types } from "mongoose"; -import { nanoid } from "nanoid"; -import { MAXIMUM_FETCH_LIMIT } from "../../src/constants"; - -describe("utilities -> graphqlConnectionFactory -> graphqlConnectionFactory", () => { - it(`Returns a connection object with default/pre-defined fields which -represents a connection that has no data at all and cannot be paginated.`, () => { - const connection = graphqlConnectionFactory(); - - expect(connection).toEqual({ - edges: [], - pageInfo: { - startCursor: null, - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - }, - }); - }); -}); - -describe("utilities -> graphqlConnectionFactory -> getLimit", () => { - it(`Should return 1 + limit `, () => { - expect(getLimit(MAXIMUM_FETCH_LIMIT)).toBe(MAXIMUM_FETCH_LIMIT + 1); - }); -}); - -describe("utilities -> graphqlConnectionFactory -> getSortingObject", () => { - it(`Should return the default sorting object when the direction is forward`, () => { - const sortingObject = { - a: 1, - b: 1, - }; - - const payload = getSortingObject("FORWARD", sortingObject); - - expect(payload).toEqual({ - a: 1, - b: 1, - }); - }); - - it(`Should return the supplied sorting object negated and in the same order when the direction is backward`, () => { - const sortingObject = { - a: 1, - b: 1, - }; - - const payload = getSortingObject("BACKWARD", sortingObject); - - expect(payload).toEqual({ - a: -1, - b: -1, - }); - }); -}); - -describe("utilities -> graphqlConnectionFactory -> getFilterObject", () => { - it(`Should return null if no cursor is supplied`, () => { - const args: CursorPaginationInput = { - limit: 10, - direction: "FORWARD", - }; - - const payload = getFilterObject(args); - - expect(payload).toEqual(null); - }); - - it(`Should return gte filter object if cursor is supplied and direction is forward`, () => { - const args: CursorPaginationInput = { - cursor: "12345", - limit: 10, - direction: "FORWARD", - }; - - const payload = getFilterObject(args); - - expect(payload).toEqual({ - _id: { $gt: "12345" }, - }); - }); - - it(`Should return lte filter object if cursor is supplied and direction is backward`, () => { - const args: CursorPaginationInput = { - cursor: "12345", - limit: 10, - direction: "BACKWARD", - }; - - const payload = getFilterObject(args); - - expect(payload).toEqual({ - _id: { $lt: "12345" }, - }); - }); -}); - -// To test generateConnectionObject function, we will create an abstract type to mock the results of our database query -type MongoModelBase = { - _id: Types.ObjectId; - a: string; -}; - -describe("utilities -> graphqlConnectionFactory -> generateConnectionObject -> General checks", () => { - it(`returns blank connection result if there are no fetched objects`, () => { - const args: CursorPaginationInput = { - direction: "FORWARD", - limit: 10, - }; - - const payload = generateConnectionObject(args, [], (x) => x); - - expect(payload.errors.length).toBe(0); - expect(payload.data).toMatchObject({ - edges: [], - pageInfo: { - endCursor: null, - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - }, - }); - }); -}); - -describe("utilities -> graphqlConnectionFactory -> generateConnectionObject -> Forward Pagination", () => { - let fetchedObjects: MongoModelBase[]; - let fetchedObjectIds: string[]; - let allEdges: { - cursor: string; - node: { _id: Types.ObjectId }; - }[]; - - beforeAll(() => { - fetchedObjects = Array.from({ length: 5 }, () => ({ - _id: new Types.ObjectId(), - a: nanoid(), - })); - - fetchedObjectIds = fetchedObjects.map((obj) => obj._id.toString()); - - allEdges = fetchedObjects.map((obj) => ({ - cursor: obj._id.toString(), - node: { - _id: obj._id, - }, - })); - }); - - it(`testing FORWARD pagination WITHOUT cursor and such that there IS NO next page`, () => { - const args: CursorPaginationInput = { - direction: "FORWARD", - limit: 10, - }; - - const payload = generateConnectionObject( - args, - fetchedObjects.slice(0, getLimit(args.limit)), - (x) => ({ - _id: x._id, - }), - ); - - expect(payload).toMatchObject({ - errors: [], - data: { - pageInfo: { - startCursor: fetchedObjectIds[0], - endCursor: fetchedObjectIds[4], - hasNextPage: false, - hasPreviousPage: false, - }, - edges: allEdges, - }, - }); - }); - - it(`testing FORWARD pagination WITHOUT cursor and such that there IS A next page`, () => { - const args: CursorPaginationInput = { - direction: "FORWARD", - limit: 3, - }; - - const payload = generateConnectionObject( - args, - fetchedObjects.slice(0, getLimit(args.limit)), - (x) => ({ - _id: x._id, - }), - ); - - expect(payload).toMatchObject({ - errors: [], - data: { - pageInfo: { - startCursor: fetchedObjectIds[0], - endCursor: fetchedObjectIds[2], - hasNextPage: true, - hasPreviousPage: false, - }, - edges: allEdges.slice(0, 3), - }, - }); - }); - - it(`testing FORWARD pagination WITH cursor and such that there IS NO next page`, () => { - const args: CursorPaginationInput = { - cursor: fetchedObjectIds[0], - direction: "FORWARD", - limit: 10, - }; - - const payload = generateConnectionObject( - args, - fetchedObjects.slice(1, getLimit(args.limit)), - (x) => ({ - _id: x._id, - }), - ); - - expect(payload).toMatchObject({ - errors: [], - data: { - pageInfo: { - startCursor: fetchedObjectIds[1], - endCursor: fetchedObjectIds[4], - hasNextPage: false, - hasPreviousPage: true, - }, - edges: allEdges.slice(1), - }, - }); - }); - - it(`testing FORWARD pagination WITH cursor and such that there IS A next page`, () => { - const args: CursorPaginationInput = { - cursor: fetchedObjectIds[0], - direction: "FORWARD", - limit: 3, - }; - - const payload = generateConnectionObject( - args, - fetchedObjects.slice(1, 1 + getLimit(args.limit)), - (x) => ({ - _id: x._id, - }), - ); - - expect(payload).toMatchObject({ - errors: [], - data: { - pageInfo: { - startCursor: fetchedObjectIds[1], - endCursor: fetchedObjectIds[3], - hasNextPage: true, - hasPreviousPage: true, - }, - edges: allEdges.slice(1, 4), - }, - }); - }); -}); - -describe("utilities -> graphqlConnectionFactory -> generateConnectionObject -> Backward pagination", () => { - let fetchedObjects: MongoModelBase[]; - let reversedFetchedObjects: MongoModelBase[]; - let fetchedObjectIds: string[]; - let allEdges: { - cursor: string; - node: { _id: Types.ObjectId }; - }[]; - - beforeAll(() => { - fetchedObjects = Array.from({ length: 5 }, () => ({ - _id: new Types.ObjectId(), - a: nanoid(), - })); - reversedFetchedObjects = Array.from(fetchedObjects).reverse(); - - fetchedObjectIds = fetchedObjects.map((obj) => obj._id.toString()); - - allEdges = fetchedObjects.map((obj) => ({ - cursor: obj._id.toString(), - node: { - _id: obj._id, - }, - })); - }); - - it(`testing BACKWARD pagination WITHOUT cursor and such that there IS NO previous page`, () => { - const args: CursorPaginationInput = { - direction: "BACKWARD", - limit: 10, - }; - - const payload = generateConnectionObject( - args, - reversedFetchedObjects.slice(0, getLimit(args.limit)), - (x) => ({ - _id: x._id, - }), - ); - - expect(payload).toMatchObject({ - errors: [], - data: { - pageInfo: { - startCursor: fetchedObjectIds[0], - endCursor: fetchedObjectIds[4], - hasNextPage: false, - hasPreviousPage: false, - }, - edges: allEdges, - }, - }); - }); - - it(`testing BACKWARD pagination WITHOUT cursor and such that there IS A previous page`, () => { - const args: CursorPaginationInput = { - direction: "BACKWARD", - limit: 3, - }; - - const payload = generateConnectionObject( - args, - reversedFetchedObjects.slice(0, getLimit(args.limit)), - (x) => ({ - _id: x._id, - }), - ); - - expect(payload).toMatchObject({ - errors: [], - data: { - pageInfo: { - startCursor: fetchedObjectIds[2], - endCursor: fetchedObjectIds[4], - hasNextPage: false, - hasPreviousPage: true, - }, - edges: allEdges.slice(2), - }, - }); - }); - - it(`testing BACKWARD pagination WITH cursor and such that there IS NO previous page`, () => { - const args: CursorPaginationInput = { - cursor: fetchedObjectIds[4], - direction: "BACKWARD", - limit: 10, - }; - - const payload = generateConnectionObject( - args, - reversedFetchedObjects.slice(1, getLimit(args.limit)), - (x) => ({ - _id: x._id, - }), - ); - - expect(payload).toMatchObject({ - errors: [], - data: { - pageInfo: { - startCursor: fetchedObjectIds[0], - endCursor: fetchedObjectIds[3], - hasNextPage: true, - hasPreviousPage: false, - }, - edges: allEdges.slice(0, 4), - }, - }); - }); - - it(`testing BACKWARD pagination WITH cursor and such that there IS A previous page`, () => { - const args: CursorPaginationInput = { - cursor: fetchedObjectIds[4], - direction: "BACKWARD", - limit: 3, - }; - - const payload = generateConnectionObject( - args, - reversedFetchedObjects.slice(1, 1 + getLimit(args.limit)), - (x) => ({ - _id: x._id, - }), - ); - - expect(payload).toMatchObject({ - errors: [], - data: { - pageInfo: { - startCursor: fetchedObjectIds[1], - endCursor: fetchedObjectIds[3], - hasNextPage: true, - hasPreviousPage: true, - }, - edges: allEdges.slice(1, 4), - }, - }); - }); -});