Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply auto pagination logic to the nested fields #163

Merged
merged 3 commits into from
Jul 22, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-cars-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphprotocol/client-auto-pagination': minor
---

Support nested fields
120 changes: 82 additions & 38 deletions packages/auto-pagination/__tests__/auto-pagination.test.ts
Original file line number Diff line number Diff line change
@@ -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<any, any> = (
_,
{ 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
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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 */ `
Expand All @@ -115,7 +133,7 @@ describe('Auto Pagination', () => {
number
}
}
users(first: 2000) {
users(first: 20) {
id
name
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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({
Expand All @@ -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
}
Expand All @@ -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({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably add some type safety to tests with codegen

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))
})
})
47 changes: 29 additions & 18 deletions packages/auto-pagination/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<AutoPaginationTransformConfig>
Expand All @@ -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
Expand Down Expand Up @@ -144,7 +145,7 @@ export default class AutoPaginationTransform implements MeshTransform {
if (
selectionNode.kind === Kind.FIELD &&
!selectionNode.name.value.startsWith('_') &&
getQueryFieldNames(delegationContext.transformedSchema).includes(selectionNode.name.value) &&
// getQueryFieldNames(delegationContext.transformedSchema).includes(selectionNode.name.value) &&
ardatan marked this conversation as resolved.
Show resolved Hide resolved
!selectionNode.arguments?.some((argNode) => argNode.name.value === 'id')
) {
const existingArgs: ArgumentNode[] = []
Expand Down Expand Up @@ -238,24 +239,34 @@ export default class AutoPaginationTransform implements MeshTransform {

transformResult(originalResult: ExecutionResult<any>): 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
}