Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,17 @@ users.findMany(
)
```

The negative value for `take` is also supported to have backward pagination:

```ts
users.findMany(
(q) => q.where({ email: (email) => email.includes('@google.com') }),
{
take: -5,
},
)
````

### Cursor-based pagination

Provide a reference to the record of the same collection as the `cursor` property for cursor-based pagination.
Expand Down
59 changes: 35 additions & 24 deletions src/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,13 @@ export type CollectionOptions<Schema extends StandardSchemaV1> = {
extensions?: Array<Extension>
}

export type PaginationOptions<Schema extends StandardSchemaV1> =
| OffsetPaginationOptions
| CursorPaginationOptions<Schema>

export interface OffsetPaginationOptions {
cursor?: never
export interface PaginationOptions<Schema extends StandardSchemaV1> {
/**
* A number of matching records to take (after the skip).
* A reference to a record to use as a cursor to start the querying from.
*/
cursor?: RecordType<StandardSchemaV1.InferOutput<Schema>>
/**
* A number of matching records to take (after `skip`, if any).
*/
take?: number
/**
Expand All @@ -51,18 +50,6 @@ export interface OffsetPaginationOptions {
skip?: number
}

export interface CursorPaginationOptions<Schema extends StandardSchemaV1> {
skip?: never
/**
* A reference to a record to use as a cursor to start the querying from.
*/
cursor: RecordType<StandardSchemaV1.InferOutput<Schema>>
/**
* A number of matching records to take (after the skip).
*/
take?: number
}

export interface UpdateOptions<T> {
data: UpdateFunction<T>
}
Expand Down Expand Up @@ -619,23 +606,47 @@ export class Collection<Schema extends StandardSchemaV1> {
> {
const { take, cursor, skip } = options

invariant(
typeof skip !== 'undefined' ? Number.isInteger(skip) && skip >= 0 : true,
'Failed to query the collection: expected the "skip" pagination option to be a number larger or equal to 0 but got %j',
skip,
)

let taken = 0
let skipped = 0
let store = this.#records

// if (cursor != null) {
// const cursorIndex = store.findIndex((record) => {
// return record[kPrimaryKey] === cursor[kPrimaryKey]
// })

// if (cursorIndex === -1) {
// return
// }

// store = store.slice(cursorIndex + 1)
// }

const shouldTake = Math.abs(take ?? Infinity)
const delta = take && take < 0 ? -1 : 1
let start = delta === 1 ? 0 : this.#records.length - 1
const end = delta === 1 ? this.#records.length : -1

if (cursor != null) {
const cursorIndex = store.findIndex((record) => {
const cursorIndex = this.#records.findIndex((record) => {
return record[kPrimaryKey] === cursor[kPrimaryKey]
})

if (cursorIndex === -1) {
return
}

store = store.slice(cursorIndex + 1)
start = cursorIndex
}

for (const record of store) {
for (let i = start; i !== end; i += delta) {
const record = this.#records[i]

if (query.test(record)) {
if (skip != null) {
if (skipped < skip) {
Expand All @@ -648,7 +659,7 @@ export class Collection<Schema extends StandardSchemaV1> {
taken++
}

if (taken >= (take ?? Infinity)) {
if (taken >= shouldTake) {
break
}
}
Expand Down
70 changes: 69 additions & 1 deletion tests/pagination-cursor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ it('returns all the matching records after the cursor', async () => {
expect(
users.findMany(undefined, { cursor }),
'Supports match-all queries',
).toEqual([{ id: 8 }, { id: 9 }, { id: 10 }])
).toEqual([{ id: 7 }, { id: 8 }, { id: 9 }, { id: 10 }])
})

it('returns the `take` number of results after the cursor', async () => {
Expand All @@ -45,5 +45,73 @@ it('returns the `take` number of results after the cursor', async () => {
expect(
users.findMany(undefined, { cursor, take: 3 }),
'Supports match-all queries',
).toEqual([{ id: 7 }, { id: 8 }, { id: 9 }])
})

it('supports skipping the cursor', async () => {
const users = new Collection({ schema: userSchema })
await users.createMany(10, (index) => ({
id: index + 1,
}))

const cursor = users.findFirst((q) => q.where({ id: 7 }))!

expect(
users.findMany(undefined, { cursor, skip: 1, take: 3 }),
'Supports match-all queries',
).toEqual([{ id: 8 }, { id: 9 }, { id: 10 }])
})

it('supports negative values for `take`', async () => {
const users = new Collection({ schema: userSchema })
await users.createMany(10, (index) => ({
id: index + 1,
}))

const cursor = users.findFirst((q) => q.where({ id: 10 }))

expect(
users.findMany(undefined, {
cursor,
take: -3,
}),
).toEqual([{ id: 10 }, { id: 9 }, { id: 8 }])

expect(
users.findMany(undefined, {
cursor,
skip: 1,
take: -3,
}),
'Supports skipping the cursor',
).toEqual([{ id: 9 }, { id: 8 }, { id: 7 }])

expect(
users.findMany((q) => q.where({ id: (id) => id > 2 }), {
cursor: users.findFirst((q) => q.where({ id: 8 })),
take: -3,
}),
).toEqual([{ id: 8 }, { id: 7 }, { id: 6 }])

expect(
users.findMany((q) => q.where({ id: (id) => id > 2 }), {
cursor: users.findFirst((q) => q.where({ id: 8 })),
skip: 1,
take: -3,
}),
).toEqual([{ id: 7 }, { id: 6 }, { id: 5 }])

expect(
users.findMany((q) => q.where({ id: (id) => id > 2 }), {
cursor: users.findFirst((q) => q.where({ id: 3 })),
take: -3,
}),
).toEqual([{ id: 3 }])

expect(
users.findMany((q) => q.where({ id: (id) => id > 2 }), {
cursor: users.findFirst((q) => q.where({ id: 2 })),
take: -3,
}),
).toEqual([])
})
68 changes: 68 additions & 0 deletions tests/pagination-offset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,71 @@ it('returns an empty array if all the results were skipped', async () => {
'Supports regular queries',
).toEqual([])
})

it('throws if providing an invalid value for `skip`', async () => {
const users = new Collection({ schema: userSchema })
await users.createMany(10, (index) => ({
id: index + 1,
}))

expect(() => users.findMany(undefined, { skip: -1 })).toThrow(
'Failed to query the collection: expected the "skip" pagination option to be a number larger or equal to 0 but got -1',
)

expect(() =>
users.findMany(undefined, {
// @ts-expect-error Intentionally invalid value.
skip: false,
}),
).toThrow(
'Failed to query the collection: expected the "skip" pagination option to be a number larger or equal to 0 but got false',
)

expect(() =>
users.findMany(undefined, {
// @ts-expect-error Intentionally invalid value.
skip: null,
}),
).toThrow(
'Failed to query the collection: expected the "skip" pagination option to be a number larger or equal to 0 but got null',
)

expect(() =>
users.findMany(undefined, {
// @ts-expect-error Intentionally invalid value.
skip: 'invalid',
}),
).toThrow(
'Failed to query the collection: expected the "skip" pagination option to be a number larger or equal to 0 but got "invalid"',
)
})

it('supports negative values for `take`', async () => {
const users = new Collection({ schema: userSchema })
await users.createMany(10, (index) => ({
id: index + 1,
}))

expect(
users.findMany(undefined, { take: -3 }),
'Returns the last n results if `skip` is not provided',
).toEqual([{ id: 10 }, { id: 9 }, { id: 8 }])

expect(users.findMany(undefined, { skip: 3, take: -3 })).toEqual([
{ id: 7 },
{ id: 6 },
{ id: 5 },
])

expect(
users.findMany((q) => q.where({ id: (id) => id > 2 }), { take: -3 }),
'Does not loop the results',
).toEqual([{ id: 10 }, { id: 9 }, { id: 8 }])

expect(
users.findMany((q) => q.where({ id: (id) => id > 2 }), {
skip: 3,
take: -3,
}),
).toEqual([{ id: 7 }, { id: 6 }, { id: 5 }])
})
2 changes: 0 additions & 2 deletions tests/types/find-many.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ it('supports offset-based pagination', () => {

users.findMany(undefined, {
skip: 5,
// @ts-expect-error
cursor: users.findFirst(),
})
})
Expand All @@ -94,7 +93,6 @@ it('supports cursor-based pagination', () => {
users.findMany(undefined, { take: 5, cursor })

users.findMany(undefined, {
// @ts-expect-error
cursor: users.findFirst(),
skip: 5,
})
Expand Down