Skip to content

Commit a5316d7

Browse files
authored
feat: handling new Date(0) for cursor (#1069)
1 parent b3517c3 commit a5316d7

File tree

2 files changed

+104
-12
lines changed

2 files changed

+104
-12
lines changed

src/paginate.spec.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3648,6 +3648,88 @@ describe('paginate', () => {
36483648
expect(result.links.previous).toBe(`?limit=2&sortBy=lastVetVisit:ASC&cursor=V998328469600000`) // lastVetVisit=2022-12-20T10:00:00.000Z, ASC (Garfield) -> V + 10^15 - 1671530400000
36493649
expect(result.links.next).toBe(`?limit=2&sortBy=lastVetVisit:DESC&cursor=V001671444000000`) // lastVetVisit=2022-12-19T10:00:00.000Z, DESC (Milo) -> V + LPAD(1671444000000, 15, '0')
36503650
})
3651+
3652+
// The range of mysql timestamp is from 1970-01-01 00:00:01
3653+
if (process.env.DB !== 'mariadb') {
3654+
it('should handle date type cursor column with zero timestamp (lastVetVisit, ASC)', async () => {
3655+
// Create a new cat with lastVetVisit = new Date(0)
3656+
const zeroDateCat = await catRepo.save(
3657+
catRepo.create({
3658+
name: 'ZeroCat',
3659+
color: 'grey',
3660+
age: 1,
3661+
cutenessLevel: CutenessLevel.LOW,
3662+
lastVetVisit: isoStringToDate('1970-01-01T00:00:00.000Z'), // new Date(0)
3663+
size: { height: 20, width: 10, length: 30 },
3664+
weightChange: 0,
3665+
})
3666+
)
3667+
3668+
const config: PaginateConfig<CatEntity> = {
3669+
sortableColumns: ['id', 'lastVetVisit'],
3670+
paginationType: PaginationType.CURSOR,
3671+
defaultSortBy: [['lastVetVisit', 'ASC']],
3672+
defaultLimit: 2,
3673+
}
3674+
const query: PaginateQuery = {
3675+
path: '',
3676+
}
3677+
3678+
const result = await paginate<CatEntity>(query, catRepo, config)
3679+
3680+
// Should appear first as it has the earliest possible timestamp
3681+
expect(result.data[0]).toStrictEqual(zeroDateCat)
3682+
expect(result.links.previous).toBe(`?limit=2&sortBy=lastVetVisit:DESC&cursor=V000000000000000`) // lastVetVisit=1970-01-01T00:00:00.000Z, DESC (ZeroCat)
3683+
expect(result.links.next).toBe(`?limit=2&sortBy=lastVetVisit:ASC&cursor=V998328556000000`) // lastVetVisit=2022-12-19T10:00:00.000Z, ASC (Milo)
3684+
3685+
// Clean up
3686+
await catRepo.remove(zeroDateCat)
3687+
})
3688+
3689+
it('should handle date type cursor column with zero timestamp (lastVetVisit, DESC)', async () => {
3690+
// Create a new cat with lastVetVisit = new Date(0)
3691+
const zeroDateCat = await catRepo.save(
3692+
catRepo.create({
3693+
name: 'ZeroCat',
3694+
color: 'grey',
3695+
age: 1,
3696+
cutenessLevel: CutenessLevel.LOW,
3697+
lastVetVisit: isoStringToDate('1970-01-01T00:00:00.000Z'), // new Date(0)
3698+
size: { height: 20, width: 10, length: 30 },
3699+
weightChange: 0,
3700+
})
3701+
)
3702+
3703+
const config: PaginateConfig<CatEntity> = {
3704+
sortableColumns: ['id', 'lastVetVisit'],
3705+
paginationType: PaginationType.CURSOR,
3706+
defaultSortBy: [['lastVetVisit', 'DESC']],
3707+
defaultLimit: 2,
3708+
filterableColumns: {
3709+
lastVetVisit: [FilterOperator.NULL, FilterSuffix.NOT],
3710+
},
3711+
}
3712+
const query: PaginateQuery = {
3713+
path: '',
3714+
filter: { lastVetVisit: '$not:$null' }, // to ensure null values are not included
3715+
cursor: 'V001671444000000', // lastVetVisit=2022-12-19T10:00:00.000Z, DESC (Milo)
3716+
}
3717+
3718+
const result = await paginate<CatEntity>(query, catRepo, config)
3719+
3720+
// Should appear last as it has the earliest possible timestamp
3721+
expect(result.data[result.data.length - 1]).toStrictEqual(zeroDateCat)
3722+
expect(result.links.previous).toBe(
3723+
`?limit=2&sortBy=lastVetVisit:ASC&filter.lastVetVisit=$not:$null&cursor=X000000000000000`
3724+
) // lastVetVisit=1970-01-01T00:00:00.000Z, ASC (ZeroCat)
3725+
expect(result.links.next).toBe(
3726+
`?limit=2&sortBy=lastVetVisit:DESC&filter.lastVetVisit=$not:$null&cursor=V000000000000000`
3727+
) // lastVetVisit=1970-01-01T00:00:00.000Z, DESC (ZeroCat)
3728+
3729+
// Clean up
3730+
await catRepo.remove(zeroDateCat)
3731+
})
3732+
}
36513733
})
36523734

36533735
describe('sortBy: age, lastVetVisit', () => {

src/paginate.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,12 @@ export async function paginate<T extends ObjectLiteral>(
222222
}
223223

224224
const generateDateCursor = (value: number, direction: 'ASC' | 'DESC'): string => {
225+
if (direction === 'ASC' && value === 0) {
226+
return 'X' + '0'.repeat(15)
227+
}
228+
225229
const finalValue = direction === 'ASC' ? Math.pow(10, 15) - value : value
230+
226231
return 'V' + String(finalValue).padStart(15, '0')
227232
}
228233

@@ -375,6 +380,9 @@ export async function paginate<T extends ObjectLiteral>(
375380
const fixedScale = Math.pow(10, 4)
376381
const maxIntegerDigit = Math.pow(10, 11)
377382

383+
const concat = (parts: string[]): string =>
384+
isMySqlOrMariaDb ? `CONCAT(${parts.join(', ')})` : parts.join(' || ')
385+
378386
const generateNullCursorExpr = (): string => {
379387
const zeroPaddedExpr = getPaddedExpr('0', padLength, dbType)
380388
const prefix = 'A'
@@ -389,17 +397,22 @@ export async function paginate<T extends ObjectLiteral>(
389397
const paddedExpr = getPaddedExpr(sqlExpr, padLength, dbType)
390398
const zeroPaddedExpr = getPaddedExpr('0', padLength, dbType)
391399

392-
const prefixNull = 'A'
393-
const prefixValue = 'V'
394-
return isMySqlOrMariaDb
395-
? `CASE
396-
WHEN ${columnExpr} IS NULL THEN CONCAT('${prefixNull}', ${zeroPaddedExpr})
397-
ELSE CONCAT('${prefixValue}', ${paddedExpr})
400+
const prefixNull = "'A'"
401+
const prefixValue = "'V'"
402+
const prefixZero = "'X'"
403+
404+
if (direction === 'ASC') {
405+
return `CASE
406+
WHEN ${columnExpr} IS NULL THEN ${concat([prefixNull, zeroPaddedExpr])}
407+
WHEN ${columnExpr} = 0 THEN ${concat([prefixZero, zeroPaddedExpr])}
408+
ELSE ${concat([prefixValue, paddedExpr])}
398409
END`
399-
: `CASE
400-
WHEN ${columnExpr} IS NULL THEN '${prefixNull}' || ${zeroPaddedExpr}
401-
ELSE '${prefixValue}' || ${paddedExpr}
410+
} else {
411+
return `CASE
412+
WHEN ${columnExpr} IS NULL THEN ${concat([prefixNull, zeroPaddedExpr])}
413+
ELSE ${concat([prefixValue, paddedExpr])}
402414
END`
415+
}
403416
}
404417

405418
const generateNumberCursorExpr = (columnExpr: string, direction: 'ASC' | 'DESC'): string => {
@@ -418,9 +431,6 @@ export async function paginate<T extends ObjectLiteral>(
418431
const zeroPaddedIntExpr = getPaddedExpr('0', integerLength, dbType)
419432
const zeroPaddedDecExpr = getPaddedExpr('0', decimalLength, dbType)
420433

421-
const concat = (parts: string[]): string =>
422-
isMySqlOrMariaDb ? `CONCAT(${parts.join(', ')})` : parts.join(' || ')
423-
424434
if (direction === 'ASC') {
425435
return `CASE
426436
WHEN ${columnExpr} IS NULL THEN ${generateNullCursorExpr()}

0 commit comments

Comments
 (0)