diff --git a/.changeset/quiet-cars-warn.md b/.changeset/quiet-cars-warn.md new file mode 100644 index 00000000..d26bfb93 --- /dev/null +++ b/.changeset/quiet-cars-warn.md @@ -0,0 +1,5 @@ +--- +'@graphprotocol/client-auto-pagination': minor +--- + +Support nested fields diff --git a/packages/auto-pagination/__tests__/auto-pagination.test.ts b/packages/auto-pagination/__tests__/auto-pagination.test.ts index f994f8f4..1af9b2b2 100644 --- a/packages/auto-pagination/__tests__/auto-pagination.test.ts +++ b/packages/auto-pagination/__tests__/auto-pagination.test.ts @@ -1,23 +1,46 @@ import { makeExecutableSchema } from '@graphql-tools/schema' import { wrapSchema } from '@graphql-tools/wrap' -import { execute, ExecutionResult, parse } from 'graphql' +import { execute, ExecutionResult, GraphQLFieldResolver, parse } from 'graphql' import AutoPaginationTransform from '../src/index.js' import PrefixTransform from '@graphql-mesh/transform-prefix' import LocalforageCache from '@graphql-mesh/cache-localforage' import { PubSub } from '@graphql-mesh/utils' describe('Auto Pagination', () => { + const FIRST_LIMIT = 10 + const SKIP_DEFAULT = 0 + const SKIP_LIMIT = 50 + const userResolver: GraphQLFieldResolver = ( + _, + { first = FIRST_LIMIT, skip = SKIP_DEFAULT, odd, where }, + ) => { + if (first > FIRST_LIMIT) { + throw new Error(`You cannot request more than ${FIRST_LIMIT} users; you requested ${first}`) + } + if (skip > SKIP_LIMIT) { + throw new Error(`You cannot skip more than ${SKIP_LIMIT} users; you requested ${skip}`) + } + let usersSlice = users + if (odd) { + usersSlice = usersOdd + } + if (where?.id_gte) { + usersSlice = users.slice(where.id_gte) + } + return usersSlice.slice(skip, skip + first) + } const users = new Array(20000).fill({}).map((_, i) => ({ id: (i + 1).toString(), name: `User ${i + 1}` })) const usersOdd = users.filter((_, i) => i % 2 === 1) const schema = makeExecutableSchema({ typeDefs: /* GraphQL */ ` type Query { _meta: Meta - users(first: Int = ${1000}, skip: Int = 0, odd: Boolean, where: WhereInput): [User!]! + users(first: Int = ${FIRST_LIMIT}, skip: Int = ${SKIP_DEFAULT}, odd: Boolean, where: WhereInput): [User!]! } type User { id: ID! name: String! + friends(first: Int = ${FIRST_LIMIT}, skip: Int = ${SKIP_DEFAULT}): [User!]! } type Meta { block: Block @@ -31,38 +54,33 @@ describe('Auto Pagination', () => { `, resolvers: { Query: { - users: (_, { first = 1000, skip = 0, odd, where }) => { - if (first > 1000) { - throw new Error(`You cannot request more than 1000 users; you requested ${first}`) - } - if (skip > 5000) { - throw new Error(`You cannot skip more than 5000 users; you requested ${skip}`) - } - let usersSlice = users - if (odd) { - usersSlice = usersOdd - } - if (where?.id_gte) { - usersSlice = users.slice(where.id_gte) - } - return usersSlice.slice(skip, skip + first) - }, + users: userResolver, _meta: () => ({ block: { number: Date.now(), }, }), }, + User: { + friends: userResolver, + }, }, }) const wrappedSchema = wrapSchema({ schema, - transforms: [new AutoPaginationTransform()], + transforms: [ + new AutoPaginationTransform({ + config: { + limitOfRecords: FIRST_LIMIT, + skipArgumentLimit: SKIP_LIMIT, + }, + }), + ], }) it('should give correct numbers of results if first arg are higher than given limit', async () => { const query = /* GraphQL */ ` query { - users(first: 2000) { + users(first: 20) { id name } @@ -72,13 +90,13 @@ describe('Auto Pagination', () => { schema: wrappedSchema, document: parse(query), }) - expect(result.data?.users).toHaveLength(2000) - expect(result.data?.users).toEqual(users.slice(0, 2000)) + expect(result.data?.users).toHaveLength(20) + expect(result.data?.users).toEqual(users.slice(0, 20)) }) it('should respect skip argument', async () => { const query = /* GraphQL */ ` query { - users(first: 2000, skip: 1) { + users(first: 20, skip: 1) { id name } @@ -88,8 +106,8 @@ describe('Auto Pagination', () => { schema: wrappedSchema, document: parse(query), }) - expect(result.data?.users).toHaveLength(2000) - expect(result.data?.users).toEqual(users.slice(1, 2001)) + expect(result.data?.users).toHaveLength(20) + expect(result.data?.users).toEqual(users.slice(1, 21)) }) it('should work with the values under the limit', async () => { const query = /* GraphQL */ ` @@ -115,7 +133,7 @@ describe('Auto Pagination', () => { number } } - users(first: 2000) { + users(first: 20) { id name } @@ -126,13 +144,13 @@ describe('Auto Pagination', () => { document: parse(query), }) expect(result.data?._meta?.block?.number).toBeDefined() - expect(result.data?.users).toHaveLength(2000) - expect(result.data?.users).toEqual(users.slice(0, 2000)) + expect(result.data?.users).toHaveLength(20) + expect(result.data?.users).toEqual(users.slice(0, 20)) }) it('should respect other arguments', async () => { const query = /* GraphQL */ ` query { - users(first: 2000, odd: true) { + users(first: 20, odd: true) { id name } @@ -142,13 +160,13 @@ describe('Auto Pagination', () => { schema: wrappedSchema, document: parse(query), }) - expect(result.data?.users).toHaveLength(2000) - expect(result.data?.users).toEqual(usersOdd.slice(0, 2000)) + expect(result.data?.users).toHaveLength(20) + expect(result.data?.users).toEqual(usersOdd.slice(0, 20)) }) it('should make queries serially if skip limit reaches the limit', async () => { const query = /* GraphQL */ ` query { - users(first: 15000) { + users(first: 150) { id name } @@ -158,8 +176,8 @@ describe('Auto Pagination', () => { schema: wrappedSchema, document: parse(query), }) - expect(result.data?.users).toHaveLength(15000) - expect(result.data?.users).toEqual(users.slice(0, 15000)) + expect(result.data?.users).toHaveLength(150) + expect(result.data?.users).toEqual(users.slice(0, 150)) }) it('should work with prefix transform properly', async () => { const wrappedSchema = wrapSchema({ @@ -176,12 +194,17 @@ describe('Auto Pagination', () => { }, importFn: (m) => import(m), }), - new AutoPaginationTransform(), + new AutoPaginationTransform({ + config: { + limitOfRecords: FIRST_LIMIT, + skipArgumentLimit: SKIP_LIMIT, + }, + }), ], }) const query = /* GraphQL */ ` query { - my_users(first: 15000) { + my_users(first: 150) { id name } @@ -191,7 +214,28 @@ describe('Auto Pagination', () => { schema: wrappedSchema, document: parse(query), }) - expect(result.data?.my_users).toHaveLength(15000) - expect(result.data?.my_users).toEqual(users.slice(0, 15000)) + expect(result.data?.my_users).toHaveLength(150) + expect(result.data?.my_users).toEqual(users.slice(0, 150)) + }) + it('should work with nested fields', async () => { + const query = /* GraphQL */ ` + query { + users(first: 20) { + id + name + friends(first: 25) { + id + name + } + } + } + ` + const result: any = await execute({ + schema: wrappedSchema, + document: parse(query), + }) + expect(result.data?.users).toHaveLength(20) + expect(result.data?.users[0].friends).toHaveLength(25) + expect(result.data?.users[0].friends).toEqual(users.slice(0, 25)) }) }) diff --git a/packages/auto-pagination/src/index.ts b/packages/auto-pagination/src/index.ts index 03421d58..14c31feb 100644 --- a/packages/auto-pagination/src/index.ts +++ b/packages/auto-pagination/src/index.ts @@ -12,7 +12,7 @@ import { SelectionNode, visit, } from 'graphql' -import { memoize1, memoize2 } from '@graphql-tools/utils' +import { memoize2 } from '@graphql-tools/utils' import _ from 'lodash' interface AutoPaginationTransformConfig { @@ -61,13 +61,14 @@ const validateSchema = memoize2(function validateSchema( } }) +/* const getQueryFieldNames = memoize1(function getQueryFields(schema: GraphQLSchema) { const queryType = schema.getQueryType() if (queryType == null) { throw new Error(`Make sure you have a query type in this source before applying Block Tracking`) } return Object.keys(queryType.getFields()) -}) +}) */ export default class AutoPaginationTransform implements MeshTransform { public config: Required @@ -91,7 +92,7 @@ export default class AutoPaginationTransform implements MeshTransform { const field = queryFields[fieldName] const existingResolver = field.resolve! field.resolve = async (root, args, context, info) => { - const totalRecords = args[this.config.firstArgumentName] || 1000 + const totalRecords = args[this.config.firstArgumentName] || this.config.limitOfRecords const initialSkipValue = args[this.config.skipArgumentName] || 0 if (totalRecords >= this.config.skipArgumentLimit * 2) { let remainingRecords = totalRecords @@ -144,7 +145,6 @@ export default class AutoPaginationTransform implements MeshTransform { if ( selectionNode.kind === Kind.FIELD && !selectionNode.name.value.startsWith('_') && - getQueryFieldNames(delegationContext.transformedSchema).includes(selectionNode.name.value) && !selectionNode.arguments?.some((argNode) => argNode.name.value === 'id') ) { const existingArgs: ArgumentNode[] = [] @@ -238,24 +238,34 @@ export default class AutoPaginationTransform implements MeshTransform { transformResult(originalResult: ExecutionResult): ExecutionResult { if (originalResult.data != null) { - const finalData = {} - for (const fullAliasName in originalResult.data) { - if (fullAliasName.startsWith('splitted_')) { - const [, , ...rest] = fullAliasName.split('_') - const aliasName = rest.join('_') - finalData[aliasName] = finalData[aliasName] || [] - for (const record of originalResult.data[fullAliasName]) { - finalData[aliasName].push(record) - } - } else { - finalData[fullAliasName] = originalResult.data[fullAliasName] - } - } return { ...originalResult, - data: finalData, + data: mergeSplittedResults(originalResult.data), } } return originalResult } } + +function mergeSplittedResults(originalData: any): any { + if (originalData != null && typeof originalData === 'object') { + if (Array.isArray(originalData)) { + return originalData.map((record) => mergeSplittedResults(record)) + } + const finalData: any = {} + for (const fullAliasName in originalData) { + if (fullAliasName.startsWith('splitted_')) { + const [, , ...rest] = fullAliasName.split('_') + const aliasName = rest.join('_') + finalData[aliasName] = finalData[aliasName] || [] + for (const record of originalData[fullAliasName]) { + finalData[aliasName].push(mergeSplittedResults(record)) + } + } else { + finalData[fullAliasName] = mergeSplittedResults(originalData[fullAliasName]) + } + } + return finalData + } + return originalData +}