Skip to content

Commit

Permalink
implement makePaginateLazy
Browse files Browse the repository at this point in the history
  • Loading branch information
Kaltsoon committed Feb 15, 2025
1 parent 8e26a40 commit cd90b01
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 86 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,30 @@ const thirdResult = await Counter.paginate({
});
```

## Only fetching specific pagination connection attributes

One pagination operation (including `edges`, `totalCount` and `pageInfo`) requires three database queries, but if you are only insterested in the `edges`, one database query is enough. The `makePaginateLazy` function can be used to create a "lazy evaluation" version of the `paginate` function. With this version, the `paginateLazy` function returns a `LazyPaginationConnection` object, containing methods `getEdges`, `getTotalCount`, and `getPageInfo`. These methods can be used to fetch the edges, total count, and page info, respectively:

```javascript
import { makePaginateLazy } from 'sequelize-cursor-pagination';

Counter.paginateLazy = makePaginateLazy(Counter);

// Same options are supported as with the regular paginate function
const connection = Counter.paginateLazy({
limit: 10,
});

// Only one database query is performed in case we are only insterested in the edges
const edges = await connection.getEdges();

// Otherwise, we can fetch the total count and page info as well
const totalCount = await connection.getTotalCount();
const pageInfo = await connection.getPageInfo();
```

The database queries are cached, so there's no extra overhead when fetching the edges, total count, or page info multiple times.

## TypeScript

The library is written in TypeScript, so types are on the house!
Expand All @@ -125,6 +149,10 @@ export class Counter extends Model<
declare static paginate: (
options: PaginateOptions<Counter>,
) => Promise<PaginationConnection<Counter>>;

declare static paginateLazy: (
options: PaginateOptions<Counter>,
) => LazyPaginationConnection<Counter>;
}

// ...
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

82 changes: 82 additions & 0 deletions src/LazyPaginationConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { PaginationEdge } from './types';

interface LazyPaginationConnectionOptions<Node = any> {
getEdgesPromise: () => Promise<PaginationEdge<Node>[]>;
getCursorCountPromise: () => Promise<number>;
getTotalCountPromise: () => Promise<number>;
isBefore: boolean;
}

class CachedPromise<T> {
#promiseGetter: () => Promise<T>;
#cachedPromise: Promise<T> | undefined;

constructor(promiseGetter: () => Promise<T>) {
this.#promiseGetter = promiseGetter;
}

public get(): Promise<T> {
if (this.#cachedPromise) {
return this.#cachedPromise;
}

this.#cachedPromise = this.#promiseGetter();

return this.#cachedPromise;
}
}

export default class LazyPaginationConnection<Node = any> {
#edgesCachedPromise: CachedPromise<PaginationEdge<Node>[]>;
#cursorCountCachedPromise: CachedPromise<number>;
#totalCountCachedPromise: CachedPromise<number>;
#isBefore: boolean;

constructor(options: LazyPaginationConnectionOptions<Node>) {
this.#edgesCachedPromise = new CachedPromise(options.getEdgesPromise);
this.#cursorCountCachedPromise = new CachedPromise(
options.getCursorCountPromise,
);
this.#totalCountCachedPromise = new CachedPromise(
options.getTotalCountPromise,
);
this.#isBefore = options.isBefore;
}

async getEdges(): Promise<PaginationEdge<Node>[]> {
return this.#edgesCachedPromise.get();
}

async getTotalCount(): Promise<number> {
return this.#totalCountCachedPromise.get();
}

async getPageInfo() {
const [edges, totalCount, cursorCount] = await Promise.all([
this.getEdges(),
this.getTotalCount(),
this.#getCursorCount(),
]);

const remaining = cursorCount - edges.length;

const hasNextPage =
(!this.#isBefore && remaining > 0) ||
(this.#isBefore && totalCount - cursorCount > 0);

const hasPreviousPage =
(this.#isBefore && remaining > 0) ||
(!this.#isBefore && totalCount - cursorCount > 0);

return {
hasNextPage,
hasPreviousPage,
startCursor: edges.length > 0 ? edges[0].cursor : null,
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null,
};
}

async #getCursorCount(): Promise<number> {
return this.#cursorCountCachedPromise.get();
}
}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as makePaginate } from './makePaginate';

export { makePaginateLazy } from './makePaginate';
export { default as LazyPaginationConnection } from './LazyPaginationConnection';
export * from './types';
191 changes: 110 additions & 81 deletions src/makePaginate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,107 +11,114 @@ import {
getCount,
} from './utils';

import LazyPaginationConnection from './LazyPaginationConnection';

import {
MakePaginateOptions,
PaginateOptions,
PaginationConnection,
} from './types';

function getLazyPaginationConnection<ModelType extends Model>(
modelClass: ModelStatic<ModelType>,
paginateOptions: PaginateOptions<ModelType>,
makePaginateOptions?: MakePaginateOptions,
) {
const primaryKeyField =
makePaginateOptions?.primaryKeyField ?? getPrimaryKeyFields(modelClass);

const omitPrimaryKeyFromOrder =
makePaginateOptions?.omitPrimaryKeyFromOrder ?? false;

const {
order: orderOption,
where,
after,
before,
limit,
...restQueryOptions
} = paginateOptions;

const normalizedOrder = normalizeOrder(
orderOption,
primaryKeyField,
omitPrimaryKeyFromOrder,
);

const order = before ? reverseOrder(normalizedOrder) : normalizedOrder;

const cursor = after
? parseCursor(after)
: before
? parseCursor(before)
: null;

const paginationQuery = cursor ? getPaginationQuery(order, cursor) : null;

const paginationWhere: WhereOptions | undefined = paginationQuery
? { [Op.and]: [paginationQuery, where] }
: where;

const paginationQueryOptions = {
where: paginationWhere,
limit,
order,
...restQueryOptions,
};

const totalCountQueryOptions = {
where,
...restQueryOptions,
};

const cursorCountQueryOptions = {
where: paginationWhere,
...restQueryOptions,
};

return new LazyPaginationConnection({
getEdgesPromise: async () => {
const instances = await modelClass.findAll(paginationQueryOptions);

if (before) {
instances.reverse();
}

return instances.map((node) => ({
node,
cursor: createCursor(node, order),
}));
},
getTotalCountPromise: () => getCount(modelClass, totalCountQueryOptions),
getCursorCountPromise: () => getCount(modelClass, cursorCountQueryOptions),
isBefore: Boolean(before),
});
}

const makePaginate = <ModelType extends Model>(
model: ModelStatic<ModelType>,
options?: MakePaginateOptions,
makePaginateOptions?: MakePaginateOptions,
) => {
const primaryKeyField =
options?.primaryKeyField ?? getPrimaryKeyFields(model);

const omitPrimaryKeyFromOrder = options?.omitPrimaryKeyFromOrder ?? false;

async function paginate(
this: unknown,
queryOptions: PaginateOptions<ModelType>,
paginateOptions: PaginateOptions<ModelType>,
): Promise<PaginationConnection<ModelType>> {
const modelClass: ModelStatic<ModelType> = isModelClass(this)
? this
: model;

const {
order: orderOption,
where,
after,
before,
limit,
...restQueryOptions
} = queryOptions;

const normalizedOrder = normalizeOrder(
orderOption,
primaryKeyField,
omitPrimaryKeyFromOrder,
const connection = getLazyPaginationConnection(
modelClass,
paginateOptions,
makePaginateOptions,
);

const order = before ? reverseOrder(normalizedOrder) : normalizedOrder;

const cursor = after
? parseCursor(after)
: before
? parseCursor(before)
: null;

const paginationQuery = cursor ? getPaginationQuery(order, cursor) : null;

const paginationWhere: WhereOptions | undefined = paginationQuery
? { [Op.and]: [paginationQuery, where] }
: where;

const paginationQueryOptions = {
where: paginationWhere,
limit,
order,
...restQueryOptions,
};

const totalCountQueryOptions = {
where,
...restQueryOptions,
};

const cursorCountQueryOptions = {
where: paginationWhere,
...restQueryOptions,
};

const [instances, totalCount, cursorCount] = await Promise.all([
modelClass.findAll(paginationQueryOptions),
getCount(modelClass, totalCountQueryOptions),
getCount(modelClass, cursorCountQueryOptions),
const [edges, totalCount, pageInfo] = await Promise.all([
connection.getEdges(),
connection.getTotalCount(),
connection.getPageInfo(),
]);

if (before) {
instances.reverse();
}

const remaining = cursorCount - instances.length;

const hasNextPage =
(!before && remaining > 0) ||
(Boolean(before) && totalCount - cursorCount > 0);

const hasPreviousPage =
(Boolean(before) && remaining > 0) ||
(!before && totalCount - cursorCount > 0);

const edges = instances.map((node) => ({
node,
cursor: createCursor(node, order),
}));

const pageInfo = {
hasNextPage,
hasPreviousPage,
startCursor: edges.length > 0 ? edges[0].cursor : null,
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null,
};

return {
totalCount,
edges,
Expand All @@ -122,4 +129,26 @@ const makePaginate = <ModelType extends Model>(
return paginate;
};

export function makePaginateLazy<ModelType extends Model>(
model: ModelStatic<ModelType>,
makePaginateOptions?: MakePaginateOptions,
) {
function paginateLazy(
this: unknown,
paginateOptions: PaginateOptions<ModelType>,
) {
const modelClass: ModelStatic<ModelType> = isModelClass(this)
? this
: model;

return getLazyPaginationConnection(
modelClass,
paginateOptions,
makePaginateOptions,
);
}

return paginateLazy;
}

export default makePaginate;
Loading

0 comments on commit cd90b01

Please sign in to comment.