Skip to content

Commit 85c9d9a

Browse files
committed
implement makePaginateLazy
1 parent 8e26a40 commit 85c9d9a

File tree

7 files changed

+296
-86
lines changed

7 files changed

+296
-86
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,30 @@ const thirdResult = await Counter.paginate({
102102
});
103103
```
104104

105+
## Only fetching specific pagination connection attributes
106+
107+
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:
108+
109+
```javascript
110+
import { makePaginateLazy } from 'sequelize-cursor-pagination';
111+
112+
Counter.paginateLazy = makePaginateLazy(Counter);
113+
114+
// Same options are supported as with the regular paginate function
115+
const connection = Counter.paginateLazy({
116+
limit: 10,
117+
});
118+
119+
// Only one database query is performed in case we are only insterested in the edges
120+
const edges = await connection.getEdges();
121+
122+
// Otherwise, we can fetch the total count and page info as well
123+
const totalCount = await connection.getTotalCount();
124+
const pageInfo = await connection.getPageInfo();
125+
```
126+
127+
The database queries are cached, so there's no extra overhead when fetching the edges, total count, or page info multiple times.
128+
105129
## TypeScript
106130

107131
The library is written in TypeScript, so types are on the house!
@@ -125,6 +149,10 @@ export class Counter extends Model<
125149
declare static paginate: (
126150
options: PaginateOptions<Counter>,
127151
) => Promise<PaginationConnection<Counter>>;
152+
153+
declare static paginateLazy: (
154+
options: PaginateOptions<Counter>,
155+
) => LazyPaginationConnection<Counter>;
128156
}
129157

130158
// ...

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/LazyPaginationConnection.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { PaginationEdge } from './types';
2+
3+
interface LazyPaginationConnectionOptions<Node = any> {
4+
getEdgesPromise: () => Promise<PaginationEdge<Node>[]>;
5+
getCursorCountPromise: () => Promise<number>;
6+
getTotalCountPromise: () => Promise<number>;
7+
isBefore: boolean;
8+
}
9+
10+
class CachedPromise<T> {
11+
#promiseGetter: () => Promise<T>;
12+
#cachedPromise: Promise<T> | undefined;
13+
14+
constructor(promiseGetter: () => Promise<T>) {
15+
this.#promiseGetter = promiseGetter;
16+
}
17+
18+
public get(): Promise<T> {
19+
if (this.#cachedPromise) {
20+
return this.#cachedPromise;
21+
}
22+
23+
this.#cachedPromise = this.#promiseGetter();
24+
25+
return this.#cachedPromise;
26+
}
27+
}
28+
29+
export default class LazyPaginationConnection<Node = any> {
30+
#edgesCachedPromise: CachedPromise<PaginationEdge<Node>[]>;
31+
#cursorCountCachedPromise: CachedPromise<number>;
32+
#totalCountCachedPromise: CachedPromise<number>;
33+
#isBefore: boolean;
34+
35+
constructor(options: LazyPaginationConnectionOptions<Node>) {
36+
this.#edgesCachedPromise = new CachedPromise(options.getEdgesPromise);
37+
this.#cursorCountCachedPromise = new CachedPromise(
38+
options.getCursorCountPromise,
39+
);
40+
this.#totalCountCachedPromise = new CachedPromise(
41+
options.getTotalCountPromise,
42+
);
43+
this.#isBefore = options.isBefore;
44+
}
45+
46+
async getEdges(): Promise<PaginationEdge<Node>[]> {
47+
return this.#edgesCachedPromise.get();
48+
}
49+
50+
async getTotalCount(): Promise<number> {
51+
return this.#totalCountCachedPromise.get();
52+
}
53+
54+
async getPageInfo() {
55+
const [edges, totalCount, cursorCount] = await Promise.all([
56+
this.getEdges(),
57+
this.getTotalCount(),
58+
this.#getCursorCount(),
59+
]);
60+
61+
const remaining = cursorCount - edges.length;
62+
63+
const hasNextPage =
64+
(!this.#isBefore && remaining > 0) ||
65+
(this.#isBefore && totalCount - cursorCount > 0);
66+
67+
const hasPreviousPage =
68+
(this.#isBefore && remaining > 0) ||
69+
(!this.#isBefore && totalCount - cursorCount > 0);
70+
71+
return {
72+
hasNextPage,
73+
hasPreviousPage,
74+
startCursor: edges.length > 0 ? edges[0].cursor : null,
75+
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null,
76+
};
77+
}
78+
79+
async #getCursorCount(): Promise<number> {
80+
return this.#cursorCountCachedPromise.get();
81+
}
82+
}

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { default as makePaginate } from './makePaginate';
2-
2+
export { makePaginateLazy } from './makePaginate';
3+
export { default as LazyPaginationConnection } from './LazyPaginationConnection';
34
export * from './types';

src/makePaginate.ts

Lines changed: 110 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -11,107 +11,114 @@ import {
1111
getCount,
1212
} from './utils';
1313

14+
import LazyPaginationConnection from './LazyPaginationConnection';
15+
1416
import {
1517
MakePaginateOptions,
1618
PaginateOptions,
1719
PaginationConnection,
1820
} from './types';
1921

22+
function getLazyPaginationConnection<ModelType extends Model>(
23+
modelClass: ModelStatic<ModelType>,
24+
paginateOptions: PaginateOptions<ModelType>,
25+
makePaginateOptions?: MakePaginateOptions,
26+
) {
27+
const primaryKeyField =
28+
makePaginateOptions?.primaryKeyField ?? getPrimaryKeyFields(modelClass);
29+
30+
const omitPrimaryKeyFromOrder =
31+
makePaginateOptions?.omitPrimaryKeyFromOrder ?? false;
32+
33+
const {
34+
order: orderOption,
35+
where,
36+
after,
37+
before,
38+
limit,
39+
...restQueryOptions
40+
} = paginateOptions;
41+
42+
const normalizedOrder = normalizeOrder(
43+
orderOption,
44+
primaryKeyField,
45+
omitPrimaryKeyFromOrder,
46+
);
47+
48+
const order = before ? reverseOrder(normalizedOrder) : normalizedOrder;
49+
50+
const cursor = after
51+
? parseCursor(after)
52+
: before
53+
? parseCursor(before)
54+
: null;
55+
56+
const paginationQuery = cursor ? getPaginationQuery(order, cursor) : null;
57+
58+
const paginationWhere: WhereOptions | undefined = paginationQuery
59+
? { [Op.and]: [paginationQuery, where] }
60+
: where;
61+
62+
const paginationQueryOptions = {
63+
where: paginationWhere,
64+
limit,
65+
order,
66+
...restQueryOptions,
67+
};
68+
69+
const totalCountQueryOptions = {
70+
where,
71+
...restQueryOptions,
72+
};
73+
74+
const cursorCountQueryOptions = {
75+
where: paginationWhere,
76+
...restQueryOptions,
77+
};
78+
79+
return new LazyPaginationConnection({
80+
getEdgesPromise: async () => {
81+
const instances = await modelClass.findAll(paginationQueryOptions);
82+
83+
if (before) {
84+
instances.reverse();
85+
}
86+
87+
return instances.map((node) => ({
88+
node,
89+
cursor: createCursor(node, order),
90+
}));
91+
},
92+
getTotalCountPromise: () => getCount(modelClass, totalCountQueryOptions),
93+
getCursorCountPromise: () => getCount(modelClass, cursorCountQueryOptions),
94+
isBefore: Boolean(before),
95+
});
96+
}
97+
2098
const makePaginate = <ModelType extends Model>(
2199
model: ModelStatic<ModelType>,
22-
options?: MakePaginateOptions,
100+
makePaginateOptions?: MakePaginateOptions,
23101
) => {
24-
const primaryKeyField =
25-
options?.primaryKeyField ?? getPrimaryKeyFields(model);
26-
27-
const omitPrimaryKeyFromOrder = options?.omitPrimaryKeyFromOrder ?? false;
28-
29102
async function paginate(
30103
this: unknown,
31-
queryOptions: PaginateOptions<ModelType>,
104+
paginateOptions: PaginateOptions<ModelType>,
32105
): Promise<PaginationConnection<ModelType>> {
33106
const modelClass: ModelStatic<ModelType> = isModelClass(this)
34107
? this
35108
: model;
36109

37-
const {
38-
order: orderOption,
39-
where,
40-
after,
41-
before,
42-
limit,
43-
...restQueryOptions
44-
} = queryOptions;
45-
46-
const normalizedOrder = normalizeOrder(
47-
orderOption,
48-
primaryKeyField,
49-
omitPrimaryKeyFromOrder,
110+
const connection = getLazyPaginationConnection(
111+
modelClass,
112+
paginateOptions,
113+
makePaginateOptions,
50114
);
51115

52-
const order = before ? reverseOrder(normalizedOrder) : normalizedOrder;
53-
54-
const cursor = after
55-
? parseCursor(after)
56-
: before
57-
? parseCursor(before)
58-
: null;
59-
60-
const paginationQuery = cursor ? getPaginationQuery(order, cursor) : null;
61-
62-
const paginationWhere: WhereOptions | undefined = paginationQuery
63-
? { [Op.and]: [paginationQuery, where] }
64-
: where;
65-
66-
const paginationQueryOptions = {
67-
where: paginationWhere,
68-
limit,
69-
order,
70-
...restQueryOptions,
71-
};
72-
73-
const totalCountQueryOptions = {
74-
where,
75-
...restQueryOptions,
76-
};
77-
78-
const cursorCountQueryOptions = {
79-
where: paginationWhere,
80-
...restQueryOptions,
81-
};
82-
83-
const [instances, totalCount, cursorCount] = await Promise.all([
84-
modelClass.findAll(paginationQueryOptions),
85-
getCount(modelClass, totalCountQueryOptions),
86-
getCount(modelClass, cursorCountQueryOptions),
116+
const [edges, totalCount, pageInfo] = await Promise.all([
117+
connection.getEdges(),
118+
connection.getTotalCount(),
119+
connection.getPageInfo(),
87120
]);
88121

89-
if (before) {
90-
instances.reverse();
91-
}
92-
93-
const remaining = cursorCount - instances.length;
94-
95-
const hasNextPage =
96-
(!before && remaining > 0) ||
97-
(Boolean(before) && totalCount - cursorCount > 0);
98-
99-
const hasPreviousPage =
100-
(Boolean(before) && remaining > 0) ||
101-
(!before && totalCount - cursorCount > 0);
102-
103-
const edges = instances.map((node) => ({
104-
node,
105-
cursor: createCursor(node, order),
106-
}));
107-
108-
const pageInfo = {
109-
hasNextPage,
110-
hasPreviousPage,
111-
startCursor: edges.length > 0 ? edges[0].cursor : null,
112-
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null,
113-
};
114-
115122
return {
116123
totalCount,
117124
edges,
@@ -122,4 +129,26 @@ const makePaginate = <ModelType extends Model>(
122129
return paginate;
123130
};
124131

132+
export function makePaginateLazy<ModelType extends Model>(
133+
model: ModelStatic<ModelType>,
134+
makePaginateOptions?: MakePaginateOptions,
135+
) {
136+
function paginateLazy(
137+
this: unknown,
138+
paginateOptions: PaginateOptions<ModelType>,
139+
) {
140+
const modelClass: ModelStatic<ModelType> = isModelClass(this)
141+
? this
142+
: model;
143+
144+
return getLazyPaginationConnection(
145+
modelClass,
146+
paginateOptions,
147+
makePaginateOptions,
148+
);
149+
}
150+
151+
return paginateLazy;
152+
}
153+
125154
export default makePaginate;

0 commit comments

Comments
 (0)