Skip to content

Commit 6bfc92f

Browse files
feat(query-graphql): Adds enableFetchAllWithNegative option
1 parent 51781f2 commit 6bfc92f

File tree

12 files changed

+68
-25
lines changed

12 files changed

+68
-25
lines changed

packages/query-graphql/src/types/connection/cursor/pager/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@ import { KeysetPagerStrategy, LimitOffsetPagerStrategy } from './strategies'
88

99
export type PagerOpts = {
1010
disableKeySetPagination?: boolean
11+
enableFetchAllWithNegative?: boolean
1112
}
1213

1314
// default pager factory to plug in addition paging strategies later on.
1415
export const createPager = <DTO>(DTOClass: Class<DTO>, opts: PagerOpts): Pager<DTO, CursorPagerResult<DTO>> => {
1516
const keySet = opts.disableKeySetPagination ? undefined : getKeySet(DTOClass)
1617
if (keySet && keySet.length) {
17-
return new CursorPager<DTO>(new KeysetPagerStrategy(DTOClass, keySet))
18+
return new CursorPager<DTO>(
19+
new KeysetPagerStrategy(DTOClass, keySet, opts.enableFetchAllWithNegative),
20+
opts.enableFetchAllWithNegative
21+
)
1822
}
19-
return new CursorPager<DTO>(new LimitOffsetPagerStrategy())
23+
return new CursorPager<DTO>(new LimitOffsetPagerStrategy(opts.enableFetchAllWithNegative), opts.enableFetchAllWithNegative)
2024
}

packages/query-graphql/src/types/connection/cursor/pager/pager.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const DEFAULT_PAGING_META = <DTO>(query: Query<DTO>): PagingMeta<DTO, OffsetPagi
1717
})
1818

1919
export class CursorPager<DTO> implements Pager<DTO, CursorPagerResult<DTO>> {
20-
constructor(readonly strategy: PagerStrategy<DTO>) {}
20+
constructor(readonly strategy: PagerStrategy<DTO>, private readonly enableFetchAllWithNegative?: boolean) {}
2121

2222
async page<Q extends CursorQueryArgsType<DTO>>(
2323
queryMany: QueryMany<DTO, Q>,
@@ -36,10 +36,12 @@ export class CursorPager<DTO> implements Pager<DTO, CursorPagerResult<DTO>> {
3636
}
3737

3838
private isValidPaging(pagingMeta: PagingMeta<DTO, CursorPagingOpts<DTO>>): boolean {
39+
const minimumLimit = this.enableFetchAllWithNegative ? -1 : 1
40+
const isValidLimit = pagingMeta.opts.limit >= minimumLimit
3941
if ('offset' in pagingMeta.opts) {
40-
return pagingMeta.opts.offset > 0 || pagingMeta.opts.limit > 0
42+
return pagingMeta.opts.offset > 0 && isValidLimit
4143
}
42-
return pagingMeta.opts.limit > 0
44+
return isValidLimit
4345
}
4446

4547
private async runQuery<Q extends Query<DTO>>(

packages/query-graphql/src/types/connection/cursor/pager/strategies/keyset.pager-strategy.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import { decodeBase64, encodeBase64, hasBeforeCursor, isBackwardPaging, isForwar
77
import { KeySetCursorPayload, KeySetPagingOpts, PagerStrategy } from './pager-strategy'
88

99
export class KeysetPagerStrategy<DTO> implements PagerStrategy<DTO> {
10-
constructor(readonly DTOClass: Class<DTO>, readonly pageFields: (keyof DTO)[]) {}
10+
constructor(
11+
readonly DTOClass: Class<DTO>,
12+
readonly pageFields: (keyof DTO)[],
13+
private readonly enableFetchAllWithNegative?: boolean
14+
) {}
1115

1216
fromCursorArgs(cursor: CursorPagingType): KeySetPagingOpts<DTO> {
1317
const { defaultSort } = this
@@ -38,19 +42,21 @@ export class KeysetPagerStrategy<DTO> implements PagerStrategy<DTO> {
3842

3943
createQuery<Q extends Query<DTO>>(query: Q, opts: KeySetPagingOpts<DTO>, includeExtraNode: boolean): Q {
4044
const paging = { limit: opts.limit }
41-
if (includeExtraNode) {
45+
if (includeExtraNode && (!this.enableFetchAllWithNegative || opts.limit !== -1)) {
4246
// Add 1 to the limit so we will fetch an additional node
4347
paging.limit += 1
4448
}
4549
const { payload } = opts
4650
// Add 1 to the limit so we will fetch an additional node with the current node
4751
const sorting = this.getSortFields(query, opts)
4852
const filter = mergeFilter(query.filter ?? {}, this.createFieldsFilter(sorting, payload))
49-
return { ...query, filter, paging, sorting }
53+
const createdQuery = { ...query, filter, sorting, paging }
54+
if (this.enableFetchAllWithNegative && opts.limit === -1) delete createdQuery.paging
55+
return createdQuery
5056
}
5157

5258
checkForExtraNode(nodes: DTO[], opts: KeySetPagingOpts<DTO>): DTO[] {
53-
const hasExtraNode = nodes.length > opts.limit
59+
const hasExtraNode = nodes.length > opts.limit && !(this.enableFetchAllWithNegative && opts.limit === -1)
5460
const returnNodes = [...nodes]
5561
if (hasExtraNode) {
5662
returnNodes.pop()

packages/query-graphql/src/types/connection/cursor/pager/strategies/limit-offset.pager-strategy.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { decodeBase64, encodeBase64, hasBeforeCursor, isBackwardPaging, isForwar
55
import { OffsetPagingOpts, PagerStrategy } from './pager-strategy'
66

77
export class LimitOffsetPagerStrategy<DTO> implements PagerStrategy<DTO> {
8+
constructor(private readonly enableFetchAllWithNegative?: boolean) {}
9+
810
private static CURSOR_PREFIX = 'arrayconnection:'
911

1012
toCursor(dto: DTO, index: number, pagingOpts: OffsetPagingOpts): string {
@@ -25,7 +27,7 @@ export class LimitOffsetPagerStrategy<DTO> implements PagerStrategy<DTO> {
2527
createQuery<Q extends Query<DTO>>(query: Q, opts: OffsetPagingOpts, includeExtraNode: boolean): Q {
2628
const { isBackward } = opts
2729
const paging = { limit: opts.limit, offset: opts.offset }
28-
if (includeExtraNode) {
30+
if (includeExtraNode && (!this.enableFetchAllWithNegative || opts.limit !== -1)) {
2931
// Add 1 to the limit so we will fetch an additional node
3032
paging.limit += 1
3133
// if paging backwards remove one from the offset to check for a previous page.
@@ -38,10 +40,15 @@ export class LimitOffsetPagerStrategy<DTO> implements PagerStrategy<DTO> {
3840
paging.limit = opts.limit
3941
}
4042
}
43+
if (this.enableFetchAllWithNegative && paging.limit === -1) delete paging.limit
4144
return { ...query, paging }
4245
}
4346

4447
checkForExtraNode(nodes: DTO[], opts: OffsetPagingOpts): DTO[] {
48+
if (opts.limit === -1) {
49+
// If we are fetching all the nodes we don't need to check for an extra node.
50+
return nodes
51+
}
4552
const returnNodes = [...nodes]
4653
// check if we have an additional node
4754
// if paging forward that indicates we have a next page
@@ -58,10 +65,11 @@ export class LimitOffsetPagerStrategy<DTO> implements PagerStrategy<DTO> {
5865
return returnNodes
5966
}
6067

61-
private getLimit(cursor: CursorPagingType): number {
68+
private getLimit(cursor: CursorPagingType): number | null {
6269
if (isBackwardPaging(cursor)) {
6370
const { last = 0, before } = cursor
6471
const offsetFromCursor = before ? LimitOffsetPagerStrategy.cursorToOffset(before) : 0
72+
if (this.enableFetchAllWithNegative && last === -1) return offsetFromCursor
6573
const offset = offsetFromCursor - last
6674
// Check to see if our before-page is underflowing past the 0th item
6775
if (offset < 0) {
@@ -70,11 +78,12 @@ export class LimitOffsetPagerStrategy<DTO> implements PagerStrategy<DTO> {
7078
}
7179
return last
7280
}
73-
return cursor.first || 0
81+
return cursor.first ?? 0
7482
}
7583

7684
private getOffset(cursor: CursorPagingType): number {
7785
if (isBackwardPaging(cursor)) {
86+
if (this.enableFetchAllWithNegative && cursor.last === -1) return 0
7887
const { last, before } = cursor
7988
const beforeOffset = before ? LimitOffsetPagerStrategy.cursorToOffset(before) : 0
8089
const offset = last ? beforeOffset - last : 0

packages/query-graphql/src/types/connection/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface BaseConnectionOptions {
88
enableTotalCount?: boolean
99
connectionName?: string
1010
disableKeySetPagination?: boolean
11+
enableFetchAllWithNegative?: boolean
1112
}
1213

1314
export interface CursorConnectionOptions extends BaseConnectionOptions {

packages/query-graphql/src/types/connection/offset/offset-connection.type.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function getOrCreateOffsetConnectionType<DTO>(
3636
): StaticConnectionType<DTO, PagingStrategies.OFFSET> {
3737
const connectionName = getOrCreateConnectionName(TItemClass, opts)
3838
return reflector.memoize(TItemClass, connectionName, () => {
39-
const pager = createPager<DTO>()
39+
const pager = createPager<DTO>(opts.enableFetchAllWithNegative)
4040
const PIT = getOrCreateOffsetPageInfoType()
4141

4242
@ObjectType(connectionName)

packages/query-graphql/src/types/connection/offset/pager/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ import { OffsetPager } from './pager'
55
export { OffsetPagerResult } from './interfaces'
66

77
// default pager factory to plug in addition paging strategies later on.
8-
export const createPager = <DTO>(): Pager<DTO, OffsetPagerResult<DTO>> => new OffsetPager<DTO>()
8+
export const createPager = <DTO>(enableFetchAllWithNegative?: boolean): Pager<DTO, OffsetPagerResult<DTO>> =>
9+
new OffsetPager<DTO>(enableFetchAllWithNegative)

packages/query-graphql/src/types/connection/offset/pager/pager.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const DEFAULT_PAGING_META = <DTO>(query: Query<DTO>): OffsetPagingMeta<DTO> => (
1616
})
1717

1818
export class OffsetPager<DTO> implements Pager<DTO, OffsetPagerResult<DTO>> {
19+
constructor(private readonly enableFetchAllWithNegative?: boolean) {}
20+
1921
async page<Q extends Query<DTO>>(queryMany: QueryMany<DTO, Q>, query: Q, count: Count<DTO>): Promise<OffsetPagerResult<DTO>> {
2022
const pagingMeta = this.getPageMeta(query)
2123
if (!this.isValidPaging(pagingMeta)) {
@@ -26,7 +28,7 @@ export class OffsetPager<DTO> implements Pager<DTO, OffsetPagerResult<DTO>> {
2628
}
2729

2830
private isValidPaging(pagingMeta: OffsetPagingMeta<DTO>): boolean {
29-
return pagingMeta.opts.limit > 0
31+
return pagingMeta.opts.limit >= (this.enableFetchAllWithNegative ? -1 : 1)
3032
}
3133

3234
private async runQuery<Q extends Query<DTO>>(
@@ -66,10 +68,13 @@ export class OffsetPager<DTO> implements Pager<DTO, OffsetPagerResult<DTO>> {
6668

6769
private createQuery<Q extends OffsetQueryArgsType<DTO>>(query: Q, pagingMeta: OffsetPagingMeta<DTO>): Q {
6870
const { limit, offset } = pagingMeta.opts
69-
return { ...query, paging: { limit: limit + 1, offset } }
71+
const paging = { limit: limit + 1, offset }
72+
if (this.enableFetchAllWithNegative && limit === -1) delete paging.limit
73+
return { ...query, paging }
7074
}
7175

7276
private checkForExtraNode(nodes: DTO[], opts: OffsetPagingOpts): DTO[] {
77+
if (this.enableFetchAllWithNegative && opts.limit === -1) return nodes
7378
const returnNodes = [...nodes]
7479
const hasExtraNode = nodes.length > opts.limit
7580
if (hasExtraNode) {

packages/query-graphql/src/types/query/paging/cursor-paging.type.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ import { Field, InputType, Int } from '@nestjs/graphql'
22
import { Class } from '@ptc-org/nestjs-query-core'
33
import { IsPositive, Min, Validate } from 'class-validator'
44

5+
import { SkipIf } from '../../../decorators'
56
import { ConnectionCursorScalar, ConnectionCursorType } from '../../cursor.scalar'
67
import { CannotUseWith, CannotUseWithout, IsUndefined } from '../../validators'
8+
import { CursorQueryArgsTypeOpts } from '..'
79
import { PagingStrategies } from './constants'
810
import { CursorPagingType } from './interfaces'
911

1012
/** @internal */
1113
let graphQLCursorPaging: Class<CursorPagingType> | null = null
1214
// eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional
13-
export const getOrCreateCursorPagingType = (): Class<CursorPagingType> => {
15+
export const getOrCreateCursorPagingType = <DTO>(opts: CursorQueryArgsTypeOpts<DTO>): Class<CursorPagingType> => {
1416
if (graphQLCursorPaging) {
1517
return graphQLCursorPaging
1618
}
@@ -40,8 +42,8 @@ export const getOrCreateCursorPagingType = (): Class<CursorPagingType> => {
4042

4143
@Field(() => Int, { nullable: true, description: 'Paginate first' })
4244
@IsUndefined()
43-
@IsPositive()
44-
@Min(1)
45+
@SkipIf(() => opts.enableFetchAllWithNegative, IsPositive())
46+
@Min(opts.enableFetchAllWithNegative ? -1 : 1)
4547
@Validate(CannotUseWith, ['before', 'last'])
4648
first?: number
4749

@@ -52,8 +54,8 @@ export const getOrCreateCursorPagingType = (): Class<CursorPagingType> => {
5254
// We'll just ignore it for now.
5355
@Validate(CannotUseWithout, ['before'])
5456
@Validate(CannotUseWith, ['after', 'first'])
55-
@Min(1)
56-
@IsPositive()
57+
@SkipIf(() => opts.enableFetchAllWithNegative, IsPositive())
58+
@Min(opts.enableFetchAllWithNegative ? -1 : 1)
5759
last?: number
5860
}
5961

packages/query-graphql/src/types/query/query-args/cursor-query-args.type.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function createCursorQueryArgsType<DTO>(
2020
): StaticQueryType<DTO, PagingStrategies.CURSOR> {
2121
const F = FilterType(DTOClass)
2222
const S = getOrCreateSortType(DTOClass)
23-
const P = getOrCreateCursorPagingType()
23+
const P = getOrCreateCursorPagingType(opts)
2424
const C = getOrCreateCursorConnectionType(DTOClass, opts)
2525

2626
@ArgsType()
@@ -38,8 +38,11 @@ export function createCursorQueryArgsType<DTO>(
3838
description: 'Limit or page results.'
3939
})
4040
@ValidateNested()
41-
@Validate(PropertyMax, ['first', opts.maxResultsSize ?? DEFAULT_QUERY_OPTS.maxResultsSize])
42-
@Validate(PropertyMax, ['last', opts.maxResultsSize ?? DEFAULT_QUERY_OPTS.maxResultsSize])
41+
@SkipIf(
42+
() => opts.enableFetchAllWithNegative,
43+
Validate(PropertyMax, ['first', opts.maxResultsSize ?? DEFAULT_QUERY_OPTS.maxResultsSize]),
44+
Validate(PropertyMax, ['last', opts.maxResultsSize ?? DEFAULT_QUERY_OPTS.maxResultsSize])
45+
)
4346
@Type(() => P)
4447
paging?: CursorPagingType
4548

0 commit comments

Comments
 (0)