Skip to content

Commit 3a00c53

Browse files
authored
feat(nestjs-query): Add enableFetchAllWithNegative option to allow to return all the items with -1 (#87)
Fixes #63 Querying the todo items from the [example](https://tripss.github.io/nestjs-query/docs/introduction/example) ```graphql todoItems(paging: {first:-1}) { # ... } ``` returns the entire list.
2 parents bbe86c3 + ff0e245 commit 3a00c53

30 files changed

+692
-27
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Connection } from 'typeorm'
2+
3+
import { executeTruncate } from '../../helpers'
4+
import { TodoItemEntity } from '../src/todo-item/todo-item.entity'
5+
6+
const tables = ['todo_item']
7+
export const truncate = async (connection: Connection): Promise<void> => executeTruncate(connection, tables)
8+
9+
export let todoItems: TodoItemEntity[]
10+
11+
export const refresh = async (connection: Connection): Promise<void> => {
12+
await truncate(connection)
13+
14+
const todoRepo = connection.getRepository(TodoItemEntity)
15+
16+
todoItems = await todoRepo.save(
17+
Array.from({ length: 100 }, (_, index) => ({
18+
title: `todoTitle${index + 1}`,
19+
completed: index % 2 === 0
20+
}))
21+
)
22+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export const todoItemFields = `
2+
id
3+
title
4+
completed
5+
`
6+
7+
export const cursorPageInfoField = `
8+
pageInfo{
9+
hasNextPage
10+
hasPreviousPage
11+
startCursor
12+
endCursor
13+
}
14+
`
15+
16+
export const offsetPageInfoField = `
17+
pageInfo{
18+
hasNextPage
19+
hasPreviousPage
20+
}
21+
`
22+
23+
export const nodes = (fields: string): string => `
24+
nodes{
25+
${fields}
26+
}
27+
`
28+
29+
export const edgeNodes = (fields: string): string => `
30+
edges {
31+
node{
32+
${fields}
33+
}
34+
cursor
35+
}
36+
`
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { INestApplication, ValidationPipe } from '@nestjs/common'
2+
import { Test } from '@nestjs/testing'
3+
import { CursorConnectionType } from '@ptc-org/nestjs-query-graphql'
4+
import request from 'supertest'
5+
import { Connection } from 'typeorm'
6+
7+
import { AppModule } from '../src/app.module'
8+
import { TodoItemCursorFetchWithNegativeDisableDTO } from '../src/todo-item/dto/todo-item-cursor-fetch-all-disable.dto'
9+
import { refresh } from './fixtures'
10+
import { cursorPageInfoField, edgeNodes, todoItemFields } from './graphql-fragments'
11+
12+
describe('TodoItemResolver (cursor pagination - fetch all with negative disabled)', () => {
13+
let app: INestApplication
14+
15+
beforeAll(async () => {
16+
const moduleRef = await Test.createTestingModule({
17+
imports: [AppModule]
18+
}).compile()
19+
20+
app = moduleRef.createNestApplication()
21+
app.useGlobalPipes(
22+
new ValidationPipe({
23+
transform: true,
24+
whitelist: true,
25+
forbidNonWhitelisted: true,
26+
skipMissingProperties: false,
27+
forbidUnknownValues: true
28+
})
29+
)
30+
31+
await app.init()
32+
await refresh(app.get(Connection))
33+
})
34+
35+
afterAll(() => refresh(app.get(Connection)))
36+
37+
describe('query', () => {
38+
describe('paging', () => {
39+
it('should not allow to fetch all the nodes', () =>
40+
request(app.getHttpServer())
41+
.post('/graphql')
42+
.send({
43+
operationName: null,
44+
variables: {},
45+
query: `{
46+
todoItemCursorFetchWithNegativeDisables(paging: {first: -1, after: "YXJyYXljb25uZWN0aW9uOjE="}) {
47+
${cursorPageInfoField}
48+
${edgeNodes(todoItemFields)}
49+
}
50+
}`
51+
})
52+
.expect(200)
53+
.then(({ body }) => {
54+
const { edges, pageInfo }: CursorConnectionType<TodoItemCursorFetchWithNegativeDisableDTO> =
55+
body.data.todoItemCursorFetchWithNegativeDisables
56+
expect(pageInfo).toEqual({
57+
endCursor: null,
58+
hasNextPage: false,
59+
hasPreviousPage: false,
60+
startCursor: null
61+
})
62+
expect(edges).toHaveLength(0)
63+
}))
64+
})
65+
})
66+
67+
afterAll(async () => {
68+
await app.close()
69+
})
70+
})
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { INestApplication, ValidationPipe } from '@nestjs/common'
2+
import { Test } from '@nestjs/testing'
3+
import { CursorConnectionType } from '@ptc-org/nestjs-query-graphql'
4+
import request from 'supertest'
5+
import { Connection } from 'typeorm'
6+
7+
import { AppModule } from '../src/app.module'
8+
import { TodoItemCursorFetchWithNegativeEnableDTO } from '../src/todo-item/dto/todo-item-cursor-fetch-all-enable.dto'
9+
import { refresh, todoItems } from './fixtures'
10+
import { cursorPageInfoField, edgeNodes, todoItemFields } from './graphql-fragments'
11+
12+
describe('TodoItemResolver (cursor pagination - fetch all with negative enabled)', () => {
13+
let app: INestApplication
14+
15+
const describeIf = (condition: boolean) => (condition ? describe : describe.skip)
16+
17+
beforeAll(async () => {
18+
const moduleRef = await Test.createTestingModule({
19+
imports: [AppModule]
20+
}).compile()
21+
22+
app = moduleRef.createNestApplication()
23+
app.useGlobalPipes(
24+
new ValidationPipe({
25+
transform: true,
26+
whitelist: true,
27+
forbidNonWhitelisted: true,
28+
skipMissingProperties: false,
29+
forbidUnknownValues: true
30+
})
31+
)
32+
33+
await app.init()
34+
await refresh(app.get(Connection))
35+
})
36+
37+
afterAll(() => refresh(app.get(Connection)))
38+
39+
describe('query', () => {
40+
describe('paging', () => {
41+
it('should return all the nodes', () =>
42+
request(app.getHttpServer())
43+
.post('/graphql')
44+
.send({
45+
operationName: null,
46+
variables: {},
47+
query: `{
48+
todoItemCursorFetchWithNegativeEnables(paging: {first: -1}) {
49+
${cursorPageInfoField}
50+
${edgeNodes(todoItemFields)}
51+
}
52+
}`
53+
})
54+
.expect(200)
55+
.then(({ body }) => {
56+
const { edges, pageInfo }: CursorConnectionType<TodoItemCursorFetchWithNegativeEnableDTO> =
57+
body.data.todoItemCursorFetchWithNegativeEnables
58+
expect(pageInfo).toEqual({
59+
endCursor: 'YXJyYXljb25uZWN0aW9uOjk5',
60+
hasNextPage: false,
61+
hasPreviousPage: false,
62+
startCursor: 'YXJyYXljb25uZWN0aW9uOjA='
63+
})
64+
expect(edges).toHaveLength(100)
65+
66+
expect(edges.map((e) => e.node)).toEqual(todoItems)
67+
}))
68+
69+
describeIf(process.env.NESTJS_QUERY_DB_TYPE == 'postgres')('postgres', () => {
70+
it('should return all the nodes after the given cursor', () =>
71+
request(app.getHttpServer())
72+
.post('/graphql')
73+
.send({
74+
operationName: null,
75+
variables: {},
76+
query: `{
77+
todoItemCursorFetchWithNegativeEnables(paging: {first: -1, after: "YXJyYXljb25uZWN0aW9uOjE="}) {
78+
${cursorPageInfoField}
79+
${edgeNodes(todoItemFields)}
80+
}
81+
}`
82+
})
83+
.expect(200)
84+
.then(({ body }) => {
85+
const { edges, pageInfo }: CursorConnectionType<TodoItemCursorFetchWithNegativeEnableDTO> =
86+
body.data.todoItemCursorFetchWithNegativeEnables
87+
expect(pageInfo).toEqual({
88+
endCursor: 'YXJyYXljb25uZWN0aW9uOjk5',
89+
hasNextPage: false,
90+
hasPreviousPage: true,
91+
startCursor: 'YXJyYXljb25uZWN0aW9uOjI='
92+
})
93+
expect(edges).toHaveLength(98)
94+
95+
expect(edges.map((e) => e.node)).toEqual(todoItems.slice(2))
96+
}))
97+
})
98+
99+
describeIf(process.env.NESTJS_QUERY_DB_TYPE == 'mysql')('mysql', () => {
100+
it('should return an error when requesting all the nodes after the given cursor', () =>
101+
request(app.getHttpServer())
102+
.post('/graphql')
103+
.send({
104+
operationName: null,
105+
variables: {},
106+
query: `{
107+
todoItemCursorFetchWithNegativeEnables(paging: {first: -1, after: "YXJyYXljb25uZWN0aW9uOjE="}) {
108+
${cursorPageInfoField}
109+
${edgeNodes(todoItemFields)}
110+
}
111+
}`
112+
})
113+
.expect(200)
114+
.then(({ body }) => {
115+
expect(body.errors).toBeDefined()
116+
expect(body.errors).toHaveLength(1)
117+
expect(body.errors[0].message).toContain('RDBMS does not support OFFSET without LIMIT in SELECT statements')
118+
expect(body.data).toBeNull()
119+
}))
120+
})
121+
})
122+
it('should return an error when request all the nodes before the given cursor', () =>
123+
request(app.getHttpServer())
124+
.post('/graphql')
125+
.send({
126+
operationName: null,
127+
variables: {},
128+
query: `{
129+
todoItemCursorFetchWithNegativeEnables(paging: {last: -1, before: "YXJyYXljb25uZWN0aW9uOjk4"}) {
130+
${cursorPageInfoField}
131+
${edgeNodes(todoItemFields)}
132+
}
133+
}`
134+
})
135+
.expect(200)
136+
.then(({ body }) => {
137+
const { edges, pageInfo }: CursorConnectionType<TodoItemCursorFetchWithNegativeEnableDTO> =
138+
body.data.todoItemCursorFetchWithNegativeEnables
139+
expect(pageInfo).toEqual({
140+
endCursor: 'YXJyYXljb25uZWN0aW9uOjk3',
141+
hasNextPage: true,
142+
hasPreviousPage: false,
143+
startCursor: 'YXJyYXljb25uZWN0aW9uOjA='
144+
})
145+
expect(edges).toHaveLength(98)
146+
147+
expect(edges.map((e) => e.node)).toEqual(todoItems.slice(0, 98))
148+
}))
149+
})
150+
151+
afterAll(async () => {
152+
await app.close()
153+
})
154+
})
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { INestApplication, ValidationPipe } from '@nestjs/common'
2+
import { Test } from '@nestjs/testing'
3+
import { OffsetConnectionType } from '@ptc-org/nestjs-query-graphql'
4+
import request from 'supertest'
5+
import { Connection } from 'typeorm'
6+
7+
import { AppModule } from '../src/app.module'
8+
import { TodoItemOffsetFetchWithNegativeDisableDTO } from '../src/todo-item/dto/todo-item-offset-fetch-all-disable.dto'
9+
import { refresh } from './fixtures'
10+
import { nodes as graphqlNodes, offsetPageInfoField, todoItemFields } from './graphql-fragments'
11+
12+
describe('TodoItemResolver (offset pagination - fetch all with negative disabled)', () => {
13+
let app: INestApplication
14+
15+
beforeAll(async () => {
16+
const moduleRef = await Test.createTestingModule({
17+
imports: [AppModule]
18+
}).compile()
19+
20+
app = moduleRef.createNestApplication()
21+
app.useGlobalPipes(
22+
new ValidationPipe({
23+
transform: true,
24+
whitelist: true,
25+
forbidNonWhitelisted: true,
26+
skipMissingProperties: false,
27+
forbidUnknownValues: true
28+
})
29+
)
30+
31+
await app.init()
32+
await refresh(app.get(Connection))
33+
})
34+
35+
afterAll(() => refresh(app.get(Connection)))
36+
37+
describe('query', () => {
38+
describe('paging', () => {
39+
it('should not allow to fetch all the nodes', () =>
40+
request(app.getHttpServer())
41+
.post('/graphql')
42+
.send({
43+
operationName: null,
44+
variables: {},
45+
query: `{
46+
todoItemOffsetFetchWithNegativeDisables(paging: {limit: -1, offset: 2}) {
47+
${offsetPageInfoField}
48+
${graphqlNodes(todoItemFields)}
49+
}
50+
}`
51+
})
52+
.expect(200)
53+
.then(({ body }) => {
54+
const { nodes, pageInfo }: OffsetConnectionType<TodoItemOffsetFetchWithNegativeDisableDTO> =
55+
body.data.todoItemOffsetFetchWithNegativeDisables
56+
expect(pageInfo).toEqual({
57+
hasNextPage: false,
58+
hasPreviousPage: false
59+
})
60+
expect(nodes).toHaveLength(0)
61+
}))
62+
})
63+
})
64+
65+
afterAll(async () => {
66+
await app.close()
67+
})
68+
})

0 commit comments

Comments
 (0)