orchid-pagination 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/index.cjs +156 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +58 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/base.ts +28 -0
- package/src/cursor/utils.ts +47 -0
- package/src/cursor.ts +98 -0
- package/src/index.ts +3 -0
- package/src/page.ts +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Ilya Semenov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# orchid-pagination
|
|
2
|
+
|
|
3
|
+
Pagination helpers for Orchid ORM:
|
|
4
|
+
|
|
5
|
+
- Page number pagination.
|
|
6
|
+
- Cursor pagination (better for larger datasets).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install orchid-pagination
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Page number pagination
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { paginateByPage } from "orchid-pagination"
|
|
18
|
+
|
|
19
|
+
defineEventHandler(async (ctx) => {
|
|
20
|
+
const query = db.user.where(conditions).order({ name: "ASC", id: "DESC" })
|
|
21
|
+
const params = getValidatedParams(ctx) // prepare object with { page?, size? }
|
|
22
|
+
const page = await paginateByPage(query, { pageSize: 10, maxPageSize: 1000 }, ctx.params)
|
|
23
|
+
return page
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Alternatively, pre-create the paginator:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createPagePaginator } from "orchid-pagination"
|
|
31
|
+
|
|
32
|
+
const paginate = createPagePaginator({ pageSize: 10, maxPageSize: 1000 })
|
|
33
|
+
|
|
34
|
+
defineEventHandler(async (ctx) => {
|
|
35
|
+
const query = db.user.where(conditions).order({ name: "ASC", id: "DESC" })
|
|
36
|
+
const params = getValidatedParams(ctx) // prepare object with { page?, size? }
|
|
37
|
+
const page = await paginate(query, params)
|
|
38
|
+
return page
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The page will have `items`, and possibly `nextPage` and/or `prevPage`.
|
|
43
|
+
|
|
44
|
+
## Cursor pagination
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { paginateByCursor } from "orchid-pagination"
|
|
48
|
+
|
|
49
|
+
defineEventHandler(async (ctx) => {
|
|
50
|
+
const query = db.user.where(conditions).order({ name: "ASC", id: "DESC" })
|
|
51
|
+
const params = getValidatedParams(ctx) // prepare object with { cursor?, size? }
|
|
52
|
+
const page = await paginateByCursor(query, { pageSize: 10, maxPageSize: 1000 }, ctx.params)
|
|
53
|
+
return page
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Alternatively, pre-create the paginator:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { createCursorPagination } from "orchid-pagination"
|
|
61
|
+
|
|
62
|
+
const paginate = createCursorPagination({ pageSize: 10, maxPageSize: 1000 })
|
|
63
|
+
|
|
64
|
+
defineEventHandler(async (ctx) => {
|
|
65
|
+
const query = db.user.where(conditions).order({ name: "ASC", id: "DESC" })
|
|
66
|
+
const params = getValidatedParams(ctx) // prepare object with { cursor?, size? }
|
|
67
|
+
const page = await paginate(query, params)
|
|
68
|
+
return page
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The page will have `items`, and possibly `nextCursor` and/or `prevCursor` which should be sent in subsequent calls to paginate forth and back.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
createCursorPaginator: () => createCursorPaginator,
|
|
24
|
+
createPagePaginator: () => createPagePaginator,
|
|
25
|
+
getPageSize: () => getPageSize,
|
|
26
|
+
paginateByCursor: () => paginateByCursor,
|
|
27
|
+
paginateByPage: () => paginateByPage
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(src_exports);
|
|
30
|
+
|
|
31
|
+
// src/base.ts
|
|
32
|
+
function getPageSize(query, config, params) {
|
|
33
|
+
const queryLimit = query.q.limit;
|
|
34
|
+
const maxPageSize = config?.maxPageSize ?? config?.pageSize ?? queryLimit;
|
|
35
|
+
if (!maxPageSize) {
|
|
36
|
+
throw new Error("Set query limit, config.maxPageSize, config.pageSize or params.limit.");
|
|
37
|
+
}
|
|
38
|
+
return Math.max(1, Math.min(params?.size ?? config?.pageSize ?? maxPageSize, maxPageSize));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/cursor.ts
|
|
42
|
+
var import_orchid_orm = require("orchid-orm");
|
|
43
|
+
|
|
44
|
+
// src/cursor/utils.ts
|
|
45
|
+
var import_node_buffer = require("buffer");
|
|
46
|
+
function getQueryOrderFields(query) {
|
|
47
|
+
const orderFields = query.q.order?.flatMap((orderItem) => {
|
|
48
|
+
if (typeof orderItem === "string") {
|
|
49
|
+
return [[orderItem, true]];
|
|
50
|
+
} else if (typeof orderItem === "object") {
|
|
51
|
+
return Object.entries(orderItem).map(([field, order]) => {
|
|
52
|
+
if (order === "ASC" || order === "DESC") {
|
|
53
|
+
return [field, order === "ASC"];
|
|
54
|
+
} else {
|
|
55
|
+
throw new Error("Unsupported order: " + order);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
throw new TypeError("Unsupported order type: " + orderItem);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
if (!orderFields?.length) {
|
|
63
|
+
throw new Error("Query must be ordered.");
|
|
64
|
+
}
|
|
65
|
+
return orderFields;
|
|
66
|
+
}
|
|
67
|
+
function createCursor(parts) {
|
|
68
|
+
return import_node_buffer.Buffer.from(parts.map(String).join(String.fromCharCode(0))).toString("base64url");
|
|
69
|
+
}
|
|
70
|
+
function parseCursor(cursor) {
|
|
71
|
+
return import_node_buffer.Buffer.from(cursor, "base64url").toString().split(String.fromCharCode(0));
|
|
72
|
+
}
|
|
73
|
+
function createDirectedCursor(parts, reverse) {
|
|
74
|
+
const cursor = createCursor(parts);
|
|
75
|
+
return reverse ? "-" + cursor : cursor;
|
|
76
|
+
}
|
|
77
|
+
function parseDirectedCursor(directedCursor) {
|
|
78
|
+
const [cursor, reverse] = directedCursor.startsWith("-") ? [directedCursor.slice(1), true] : [directedCursor, false];
|
|
79
|
+
return { cursor, parts: parseCursor(cursor), reverse };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/cursor.ts
|
|
83
|
+
async function createCursorPaginator(config) {
|
|
84
|
+
return function paginate(query, params) {
|
|
85
|
+
return paginateByCursor(query, config, params);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async function paginateByCursor(query, config, params) {
|
|
89
|
+
const size = getPageSize(query, config, params);
|
|
90
|
+
const orderFields = getQueryOrderFields(query);
|
|
91
|
+
const parsedCursorMaybeValid = params?.cursor ? parseDirectedCursor(params.cursor) : void 0;
|
|
92
|
+
const parsedCursor = parsedCursorMaybeValid && parsedCursorMaybeValid.parts.length >= orderFields.length ? parsedCursorMaybeValid : void 0;
|
|
93
|
+
const reverse = parsedCursor?.reverse ?? false;
|
|
94
|
+
if (reverse) {
|
|
95
|
+
orderFields.forEach((of) => {
|
|
96
|
+
of[1] = !of[1];
|
|
97
|
+
});
|
|
98
|
+
const orderArg = Object.fromEntries(orderFields.map(([field, asc]) => [field, asc ? "ASC" : "DESC"]));
|
|
99
|
+
query = query.clear("order").order(orderArg);
|
|
100
|
+
}
|
|
101
|
+
if (parsedCursor) {
|
|
102
|
+
const leftSqlExpr = orderFields.map(([field, asc]) => asc ? field : `$${field}`).join(",");
|
|
103
|
+
const rightSqlExp = orderFields.map(([field, asc]) => asc ? `$${field}` : field).join(",");
|
|
104
|
+
const sqlExpr = `(${leftSqlExpr}) > (${rightSqlExp})`;
|
|
105
|
+
const values = Object.fromEntries(orderFields.map(([field], i) => [field, parsedCursor.parts[i]]));
|
|
106
|
+
query = query.where((0, import_orchid_orm.raw)({ raw: sqlExpr, values }));
|
|
107
|
+
}
|
|
108
|
+
const items = await query.limit(size + 1);
|
|
109
|
+
if (!Array.isArray(items)) {
|
|
110
|
+
throw new TypeError("Query must return an array.");
|
|
111
|
+
}
|
|
112
|
+
const hasContinuation = items.length > size;
|
|
113
|
+
if (hasContinuation) {
|
|
114
|
+
items.splice(size);
|
|
115
|
+
}
|
|
116
|
+
if (reverse) {
|
|
117
|
+
items.reverse();
|
|
118
|
+
}
|
|
119
|
+
function createItemCursor(item, reverse2) {
|
|
120
|
+
return createDirectedCursor(orderFields.map(([field]) => {
|
|
121
|
+
return String(item[field]);
|
|
122
|
+
}), reverse2);
|
|
123
|
+
}
|
|
124
|
+
const prevCursor = parsedCursor && (parsedCursor.reverse === false || hasContinuation) ? createItemCursor(items[0], true) : void 0;
|
|
125
|
+
const nextCursor = parsedCursor?.reverse === true || hasContinuation ? createItemCursor(items.at(-1), false) : void 0;
|
|
126
|
+
return { items, size, prevCursor, nextCursor };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/page.ts
|
|
130
|
+
function createPagePaginator(config) {
|
|
131
|
+
return function paginate(query, params) {
|
|
132
|
+
return paginateByPage(query, config, params);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function paginateByPage(query, config, params) {
|
|
136
|
+
const size = getPageSize(query, config, params);
|
|
137
|
+
const page = Math.max(1, params?.page ?? 1);
|
|
138
|
+
const offset = (page - 1) * size;
|
|
139
|
+
const items = await query.offset(offset).limit(size + 1);
|
|
140
|
+
const hasContinuation = items.length > size;
|
|
141
|
+
if (hasContinuation) {
|
|
142
|
+
items.splice(size);
|
|
143
|
+
}
|
|
144
|
+
const prevPage = page > 1 ? page - 1 : void 0;
|
|
145
|
+
const nextPage = hasContinuation ? page + 1 : void 0;
|
|
146
|
+
return { items, page, size, offset, prevPage, nextPage };
|
|
147
|
+
}
|
|
148
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
149
|
+
0 && (module.exports = {
|
|
150
|
+
createCursorPaginator,
|
|
151
|
+
createPagePaginator,
|
|
152
|
+
getPageSize,
|
|
153
|
+
paginateByCursor,
|
|
154
|
+
paginateByPage
|
|
155
|
+
});
|
|
156
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/base.ts","../src/cursor.ts","../src/cursor/utils.ts","../src/page.ts"],"sourcesContent":["export * from \"./base\"\nexport * from \"./cursor\"\nexport * from \"./page\"\n","import type { Query, QueryThen, SelectQueryData } from \"orchid-orm\"\n\nexport interface ListQuery extends Query {\n then: QueryThen<unknown[]>\n}\n\nexport interface PaginationConfig {\n /** Default page size. If not set, `maxPageSize` is used. */\n pageSize?: number\n /** Max page size allowed to be passed in params. If not set, `pageSize` is used. */\n maxPageSize?: number\n}\n\nexport interface PaginationParams {\n /** Requested page size. */\n size?: number\n}\n\nexport function getPageSize(query: ListQuery, config?: PaginationConfig, params?: PaginationParams): number {\n const queryLimit = (query.q as SelectQueryData).limit\n\n const maxPageSize = config?.maxPageSize ?? config?.pageSize ?? queryLimit\n if (!maxPageSize) {\n throw new Error(\"Set query limit, config.maxPageSize, config.pageSize or params.limit.\")\n }\n\n return Math.max(1, Math.min(params?.size ?? config?.pageSize ?? maxPageSize, maxPageSize))\n}\n","import type { SortDir } from \"orchid-orm\"\nimport { raw } from \"orchid-orm\"\n\nimport type { ListQuery, PaginationConfig } from \"./base\"\nimport { getPageSize } from \"./base\"\nimport { createDirectedCursor, getQueryOrderFields, parseDirectedCursor } from \"./cursor/utils\"\n\nexport interface CursorPaginationParams {\n /** Page cursor, as returned by previous call in prevCursor / nextCursor. */\n cursor?: string\n /** Page size. */\n size?: number\n}\n\nexport type CursorPaginationPage<T extends ListQuery = ListQuery> = {\n items: Awaited<T>\n /** Effective page size. Number of items is guaranteed to be less or equal. */\n size: number\n /** Cursor pointing to previous page. */\n prevCursor?: string\n /** Cursor pointing to next page. */\n nextCursor?: string\n}\n\nexport async function createCursorPaginator(config?: PaginationConfig) {\n return function paginate<T extends ListQuery>(query: T, params?: CursorPaginationParams) {\n return paginateByCursor(query, config, params)\n }\n}\n\nexport async function paginateByCursor<T extends ListQuery>(query: T, config?: PaginationConfig, params?: CursorPaginationParams): Promise<CursorPaginationPage<T>> {\n const size = getPageSize(query, config, params)\n\n const orderFields = getQueryOrderFields(query)\n\n // poor man validation, TODO improve\n const parsedCursorMaybeValid = params?.cursor ? parseDirectedCursor(params.cursor) : undefined\n const parsedCursor = parsedCursorMaybeValid && parsedCursorMaybeValid.parts.length >= orderFields.length ? parsedCursorMaybeValid : undefined\n\n const reverse = parsedCursor?.reverse ?? false\n\n if (reverse) {\n // Reverse parsed order fields + reverse query ordering.\n orderFields.forEach((of) => {\n of[1] = !of[1]\n })\n const orderArg = Object.fromEntries(orderFields.map<[string, SortDir]>(([field, asc]) => [field, asc ? \"ASC\" : \"DESC\"]))\n query = query.clear(\"order\").order(orderArg as any)\n }\n\n if (parsedCursor) {\n // Prepare raw SQL.\n // For example, for (amount asc, id asc) order, that would be:\n // (amount, $id) >= ($amount, id)\n const leftSqlExpr = orderFields.map(([field, asc]) => asc ? field : `$${field}`).join(\",\")\n const rightSqlExp = orderFields.map(([field, asc]) => asc ? `$${field}` : field).join(\",\")\n const sqlExpr = `(${leftSqlExpr}) > (${rightSqlExp})`\n const values = Object.fromEntries(orderFields.map(([field], i) => [field, parsedCursor.parts[i]]))\n query = query.where(raw({ raw: sqlExpr, values }))\n }\n\n // Query 1 extra item to see if we can paginate farther in current direction.\n const items = await query.limit(size + 1)\n if (!Array.isArray(items)) {\n throw new TypeError(\"Query must return an array.\")\n }\n const hasContinuation = items.length > size\n if (hasContinuation) {\n items.splice(size)\n }\n if (reverse) {\n items.reverse()\n }\n\n function createItemCursor(item: any, reverse: boolean) {\n return createDirectedCursor(orderFields.map(([field]) => {\n // Can add custom serializer here if needed.\n return String(item[field])\n }), reverse)\n }\n\n // Prev cursor:\n // - for initial pagination, there is no prev page\n // - for forward pagination, prev page exists always\n // - for reverse pagination, prev page exists if we have a continuation\n const prevCursor = (parsedCursor && (parsedCursor.reverse === false || hasContinuation))\n ? createItemCursor(items[0], true)\n : undefined\n\n // Next cursor:\n // - for reverse pagination, next page exists always\n // - for initial or forward pagination, next page exists if we have a continuation\n const nextCursor = (parsedCursor?.reverse === true || hasContinuation)\n ? createItemCursor(items.at(-1), false)\n : undefined\n\n return { items, size, prevCursor, nextCursor }\n}\n","import { Buffer } from \"node:buffer\"\n\nimport type { SelectQueryData } from \"orchid-orm\"\n\nimport type { ListQuery } from \"../base\"\n\ntype OrderField = [field: string, asc: boolean]\n\nexport function getQueryOrderFields(query: ListQuery): OrderField[] {\n const orderFields = (query.q as SelectQueryData).order?.flatMap<[field: string, asc: boolean]>((orderItem) => {\n if (typeof orderItem === \"string\") {\n return [[orderItem, true]]\n } else if (typeof orderItem === \"object\") {\n return Object.entries(orderItem).map<[string, boolean]>(([field, order]) => {\n if (order === \"ASC\" || order === \"DESC\") {\n return [field, order === \"ASC\"]\n } else {\n throw new Error(\"Unsupported order: \" + order)\n }\n })\n } else {\n throw new TypeError(\"Unsupported order type: \" + orderItem)\n }\n })\n if (!orderFields?.length) {\n throw new Error(\"Query must be ordered.\")\n }\n return orderFields\n}\n\nexport function createCursor(parts: string[]) {\n return Buffer.from(parts.map(String).join(String.fromCharCode(0))).toString(\"base64url\")\n}\n\nexport function parseCursor(cursor: string) {\n return Buffer.from(cursor, \"base64url\").toString().split(String.fromCharCode(0))\n}\n\nexport function createDirectedCursor(parts: string[], reverse: boolean) {\n const cursor = createCursor(parts)\n return reverse ? \"-\" + cursor : cursor\n}\n\nexport function parseDirectedCursor(directedCursor: string) {\n const [cursor, reverse] = directedCursor.startsWith(\"-\") ? [directedCursor.slice(1), true] : [directedCursor, false]\n return { cursor, parts: parseCursor(cursor), reverse }\n}\n","import { getPageSize, type ListQuery, type PaginationConfig } from \"./base\"\n\nexport interface PagePaginationParams {\n /** Page, 1-based. */\n page?: number\n /** Page size. */\n size?: number\n}\n\nexport type PagePaginationPage<T extends ListQuery = ListQuery> = {\n items: Awaited<T>\n /** Effective page number. */\n page: number\n /** Effective page size. Number of items is guaranteed to be less or equal. */\n size: number\n /** Offset of the first item, 1-based. */\n offset: number\n /** Prev page numberm (if exists). */\n prevPage?: number\n /** Next page number (if exists). */\n nextPage?: number\n}\n\nexport function createPagePaginator(config?: PaginationConfig) {\n return function paginate<T extends ListQuery>(query: T, params?: PagePaginationParams) {\n return paginateByPage(query, config, params)\n }\n}\n\nexport async function paginateByPage<T extends ListQuery>(query: T, config?: PaginationConfig, params?: PagePaginationParams): Promise<PagePaginationPage<T>> {\n const size = getPageSize(query, config, params)\n\n const page = Math.max(1, params?.page ?? 1)\n const offset = (page - 1) * size\n\n const items = await query.offset(offset).limit(size + 1)\n const hasContinuation = items.length > size\n if (hasContinuation) {\n items.splice(size)\n }\n\n const prevPage = page > 1 ? page - 1 : undefined\n const nextPage = hasContinuation ? page + 1 : undefined\n\n return { items, page, size, offset, prevPage, nextPage }\n}\n"],"mappings":"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","names":["reverse"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Query, QueryThen } from 'orchid-orm';
|
|
2
|
+
|
|
3
|
+
interface ListQuery extends Query {
|
|
4
|
+
then: QueryThen<unknown[]>;
|
|
5
|
+
}
|
|
6
|
+
interface PaginationConfig {
|
|
7
|
+
/** Default page size. If not set, `maxPageSize` is used. */
|
|
8
|
+
pageSize?: number;
|
|
9
|
+
/** Max page size allowed to be passed in params. If not set, `pageSize` is used. */
|
|
10
|
+
maxPageSize?: number;
|
|
11
|
+
}
|
|
12
|
+
interface PaginationParams {
|
|
13
|
+
/** Requested page size. */
|
|
14
|
+
size?: number;
|
|
15
|
+
}
|
|
16
|
+
declare function getPageSize(query: ListQuery, config?: PaginationConfig, params?: PaginationParams): number;
|
|
17
|
+
|
|
18
|
+
interface CursorPaginationParams {
|
|
19
|
+
/** Page cursor, as returned by previous call in prevCursor / nextCursor. */
|
|
20
|
+
cursor?: string;
|
|
21
|
+
/** Page size. */
|
|
22
|
+
size?: number;
|
|
23
|
+
}
|
|
24
|
+
type CursorPaginationPage<T extends ListQuery = ListQuery> = {
|
|
25
|
+
items: Awaited<T>;
|
|
26
|
+
/** Effective page size. Number of items is guaranteed to be less or equal. */
|
|
27
|
+
size: number;
|
|
28
|
+
/** Cursor pointing to previous page. */
|
|
29
|
+
prevCursor?: string;
|
|
30
|
+
/** Cursor pointing to next page. */
|
|
31
|
+
nextCursor?: string;
|
|
32
|
+
};
|
|
33
|
+
declare function createCursorPaginator(config?: PaginationConfig): Promise<(<T extends ListQuery>(query: T, params?: CursorPaginationParams) => Promise<CursorPaginationPage<T>>)>;
|
|
34
|
+
declare function paginateByCursor<T extends ListQuery>(query: T, config?: PaginationConfig, params?: CursorPaginationParams): Promise<CursorPaginationPage<T>>;
|
|
35
|
+
|
|
36
|
+
interface PagePaginationParams {
|
|
37
|
+
/** Page, 1-based. */
|
|
38
|
+
page?: number;
|
|
39
|
+
/** Page size. */
|
|
40
|
+
size?: number;
|
|
41
|
+
}
|
|
42
|
+
type PagePaginationPage<T extends ListQuery = ListQuery> = {
|
|
43
|
+
items: Awaited<T>;
|
|
44
|
+
/** Effective page number. */
|
|
45
|
+
page: number;
|
|
46
|
+
/** Effective page size. Number of items is guaranteed to be less or equal. */
|
|
47
|
+
size: number;
|
|
48
|
+
/** Offset of the first item, 1-based. */
|
|
49
|
+
offset: number;
|
|
50
|
+
/** Prev page numberm (if exists). */
|
|
51
|
+
prevPage?: number;
|
|
52
|
+
/** Next page number (if exists). */
|
|
53
|
+
nextPage?: number;
|
|
54
|
+
};
|
|
55
|
+
declare function createPagePaginator(config?: PaginationConfig): <T extends ListQuery>(query: T, params?: PagePaginationParams) => Promise<PagePaginationPage<T>>;
|
|
56
|
+
declare function paginateByPage<T extends ListQuery>(query: T, config?: PaginationConfig, params?: PagePaginationParams): Promise<PagePaginationPage<T>>;
|
|
57
|
+
|
|
58
|
+
export { type CursorPaginationPage, type CursorPaginationParams, type ListQuery, type PagePaginationPage, type PagePaginationParams, type PaginationConfig, type PaginationParams, createCursorPaginator, createPagePaginator, getPageSize, paginateByCursor, paginateByPage };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Query, QueryThen } from 'orchid-orm';
|
|
2
|
+
|
|
3
|
+
interface ListQuery extends Query {
|
|
4
|
+
then: QueryThen<unknown[]>;
|
|
5
|
+
}
|
|
6
|
+
interface PaginationConfig {
|
|
7
|
+
/** Default page size. If not set, `maxPageSize` is used. */
|
|
8
|
+
pageSize?: number;
|
|
9
|
+
/** Max page size allowed to be passed in params. If not set, `pageSize` is used. */
|
|
10
|
+
maxPageSize?: number;
|
|
11
|
+
}
|
|
12
|
+
interface PaginationParams {
|
|
13
|
+
/** Requested page size. */
|
|
14
|
+
size?: number;
|
|
15
|
+
}
|
|
16
|
+
declare function getPageSize(query: ListQuery, config?: PaginationConfig, params?: PaginationParams): number;
|
|
17
|
+
|
|
18
|
+
interface CursorPaginationParams {
|
|
19
|
+
/** Page cursor, as returned by previous call in prevCursor / nextCursor. */
|
|
20
|
+
cursor?: string;
|
|
21
|
+
/** Page size. */
|
|
22
|
+
size?: number;
|
|
23
|
+
}
|
|
24
|
+
type CursorPaginationPage<T extends ListQuery = ListQuery> = {
|
|
25
|
+
items: Awaited<T>;
|
|
26
|
+
/** Effective page size. Number of items is guaranteed to be less or equal. */
|
|
27
|
+
size: number;
|
|
28
|
+
/** Cursor pointing to previous page. */
|
|
29
|
+
prevCursor?: string;
|
|
30
|
+
/** Cursor pointing to next page. */
|
|
31
|
+
nextCursor?: string;
|
|
32
|
+
};
|
|
33
|
+
declare function createCursorPaginator(config?: PaginationConfig): Promise<(<T extends ListQuery>(query: T, params?: CursorPaginationParams) => Promise<CursorPaginationPage<T>>)>;
|
|
34
|
+
declare function paginateByCursor<T extends ListQuery>(query: T, config?: PaginationConfig, params?: CursorPaginationParams): Promise<CursorPaginationPage<T>>;
|
|
35
|
+
|
|
36
|
+
interface PagePaginationParams {
|
|
37
|
+
/** Page, 1-based. */
|
|
38
|
+
page?: number;
|
|
39
|
+
/** Page size. */
|
|
40
|
+
size?: number;
|
|
41
|
+
}
|
|
42
|
+
type PagePaginationPage<T extends ListQuery = ListQuery> = {
|
|
43
|
+
items: Awaited<T>;
|
|
44
|
+
/** Effective page number. */
|
|
45
|
+
page: number;
|
|
46
|
+
/** Effective page size. Number of items is guaranteed to be less or equal. */
|
|
47
|
+
size: number;
|
|
48
|
+
/** Offset of the first item, 1-based. */
|
|
49
|
+
offset: number;
|
|
50
|
+
/** Prev page numberm (if exists). */
|
|
51
|
+
prevPage?: number;
|
|
52
|
+
/** Next page number (if exists). */
|
|
53
|
+
nextPage?: number;
|
|
54
|
+
};
|
|
55
|
+
declare function createPagePaginator(config?: PaginationConfig): <T extends ListQuery>(query: T, params?: PagePaginationParams) => Promise<PagePaginationPage<T>>;
|
|
56
|
+
declare function paginateByPage<T extends ListQuery>(query: T, config?: PaginationConfig, params?: PagePaginationParams): Promise<PagePaginationPage<T>>;
|
|
57
|
+
|
|
58
|
+
export { type CursorPaginationPage, type CursorPaginationParams, type ListQuery, type PagePaginationPage, type PagePaginationParams, type PaginationConfig, type PaginationParams, createCursorPaginator, createPagePaginator, getPageSize, paginateByCursor, paginateByPage };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// src/base.ts
|
|
2
|
+
function getPageSize(query, config, params) {
|
|
3
|
+
const queryLimit = query.q.limit;
|
|
4
|
+
const maxPageSize = config?.maxPageSize ?? config?.pageSize ?? queryLimit;
|
|
5
|
+
if (!maxPageSize) {
|
|
6
|
+
throw new Error("Set query limit, config.maxPageSize, config.pageSize or params.limit.");
|
|
7
|
+
}
|
|
8
|
+
return Math.max(1, Math.min(params?.size ?? config?.pageSize ?? maxPageSize, maxPageSize));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/cursor.ts
|
|
12
|
+
import { raw } from "orchid-orm";
|
|
13
|
+
|
|
14
|
+
// src/cursor/utils.ts
|
|
15
|
+
import { Buffer } from "node:buffer";
|
|
16
|
+
function getQueryOrderFields(query) {
|
|
17
|
+
const orderFields = query.q.order?.flatMap((orderItem) => {
|
|
18
|
+
if (typeof orderItem === "string") {
|
|
19
|
+
return [[orderItem, true]];
|
|
20
|
+
} else if (typeof orderItem === "object") {
|
|
21
|
+
return Object.entries(orderItem).map(([field, order]) => {
|
|
22
|
+
if (order === "ASC" || order === "DESC") {
|
|
23
|
+
return [field, order === "ASC"];
|
|
24
|
+
} else {
|
|
25
|
+
throw new Error("Unsupported order: " + order);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
} else {
|
|
29
|
+
throw new TypeError("Unsupported order type: " + orderItem);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
if (!orderFields?.length) {
|
|
33
|
+
throw new Error("Query must be ordered.");
|
|
34
|
+
}
|
|
35
|
+
return orderFields;
|
|
36
|
+
}
|
|
37
|
+
function createCursor(parts) {
|
|
38
|
+
return Buffer.from(parts.map(String).join(String.fromCharCode(0))).toString("base64url");
|
|
39
|
+
}
|
|
40
|
+
function parseCursor(cursor) {
|
|
41
|
+
return Buffer.from(cursor, "base64url").toString().split(String.fromCharCode(0));
|
|
42
|
+
}
|
|
43
|
+
function createDirectedCursor(parts, reverse) {
|
|
44
|
+
const cursor = createCursor(parts);
|
|
45
|
+
return reverse ? "-" + cursor : cursor;
|
|
46
|
+
}
|
|
47
|
+
function parseDirectedCursor(directedCursor) {
|
|
48
|
+
const [cursor, reverse] = directedCursor.startsWith("-") ? [directedCursor.slice(1), true] : [directedCursor, false];
|
|
49
|
+
return { cursor, parts: parseCursor(cursor), reverse };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/cursor.ts
|
|
53
|
+
async function createCursorPaginator(config) {
|
|
54
|
+
return function paginate(query, params) {
|
|
55
|
+
return paginateByCursor(query, config, params);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function paginateByCursor(query, config, params) {
|
|
59
|
+
const size = getPageSize(query, config, params);
|
|
60
|
+
const orderFields = getQueryOrderFields(query);
|
|
61
|
+
const parsedCursorMaybeValid = params?.cursor ? parseDirectedCursor(params.cursor) : void 0;
|
|
62
|
+
const parsedCursor = parsedCursorMaybeValid && parsedCursorMaybeValid.parts.length >= orderFields.length ? parsedCursorMaybeValid : void 0;
|
|
63
|
+
const reverse = parsedCursor?.reverse ?? false;
|
|
64
|
+
if (reverse) {
|
|
65
|
+
orderFields.forEach((of) => {
|
|
66
|
+
of[1] = !of[1];
|
|
67
|
+
});
|
|
68
|
+
const orderArg = Object.fromEntries(orderFields.map(([field, asc]) => [field, asc ? "ASC" : "DESC"]));
|
|
69
|
+
query = query.clear("order").order(orderArg);
|
|
70
|
+
}
|
|
71
|
+
if (parsedCursor) {
|
|
72
|
+
const leftSqlExpr = orderFields.map(([field, asc]) => asc ? field : `$${field}`).join(",");
|
|
73
|
+
const rightSqlExp = orderFields.map(([field, asc]) => asc ? `$${field}` : field).join(",");
|
|
74
|
+
const sqlExpr = `(${leftSqlExpr}) > (${rightSqlExp})`;
|
|
75
|
+
const values = Object.fromEntries(orderFields.map(([field], i) => [field, parsedCursor.parts[i]]));
|
|
76
|
+
query = query.where(raw({ raw: sqlExpr, values }));
|
|
77
|
+
}
|
|
78
|
+
const items = await query.limit(size + 1);
|
|
79
|
+
if (!Array.isArray(items)) {
|
|
80
|
+
throw new TypeError("Query must return an array.");
|
|
81
|
+
}
|
|
82
|
+
const hasContinuation = items.length > size;
|
|
83
|
+
if (hasContinuation) {
|
|
84
|
+
items.splice(size);
|
|
85
|
+
}
|
|
86
|
+
if (reverse) {
|
|
87
|
+
items.reverse();
|
|
88
|
+
}
|
|
89
|
+
function createItemCursor(item, reverse2) {
|
|
90
|
+
return createDirectedCursor(orderFields.map(([field]) => {
|
|
91
|
+
return String(item[field]);
|
|
92
|
+
}), reverse2);
|
|
93
|
+
}
|
|
94
|
+
const prevCursor = parsedCursor && (parsedCursor.reverse === false || hasContinuation) ? createItemCursor(items[0], true) : void 0;
|
|
95
|
+
const nextCursor = parsedCursor?.reverse === true || hasContinuation ? createItemCursor(items.at(-1), false) : void 0;
|
|
96
|
+
return { items, size, prevCursor, nextCursor };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/page.ts
|
|
100
|
+
function createPagePaginator(config) {
|
|
101
|
+
return function paginate(query, params) {
|
|
102
|
+
return paginateByPage(query, config, params);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function paginateByPage(query, config, params) {
|
|
106
|
+
const size = getPageSize(query, config, params);
|
|
107
|
+
const page = Math.max(1, params?.page ?? 1);
|
|
108
|
+
const offset = (page - 1) * size;
|
|
109
|
+
const items = await query.offset(offset).limit(size + 1);
|
|
110
|
+
const hasContinuation = items.length > size;
|
|
111
|
+
if (hasContinuation) {
|
|
112
|
+
items.splice(size);
|
|
113
|
+
}
|
|
114
|
+
const prevPage = page > 1 ? page - 1 : void 0;
|
|
115
|
+
const nextPage = hasContinuation ? page + 1 : void 0;
|
|
116
|
+
return { items, page, size, offset, prevPage, nextPage };
|
|
117
|
+
}
|
|
118
|
+
export {
|
|
119
|
+
createCursorPaginator,
|
|
120
|
+
createPagePaginator,
|
|
121
|
+
getPageSize,
|
|
122
|
+
paginateByCursor,
|
|
123
|
+
paginateByPage
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/base.ts","../src/cursor.ts","../src/cursor/utils.ts","../src/page.ts"],"sourcesContent":["import type { Query, QueryThen, SelectQueryData } from \"orchid-orm\"\n\nexport interface ListQuery extends Query {\n then: QueryThen<unknown[]>\n}\n\nexport interface PaginationConfig {\n /** Default page size. If not set, `maxPageSize` is used. */\n pageSize?: number\n /** Max page size allowed to be passed in params. If not set, `pageSize` is used. */\n maxPageSize?: number\n}\n\nexport interface PaginationParams {\n /** Requested page size. */\n size?: number\n}\n\nexport function getPageSize(query: ListQuery, config?: PaginationConfig, params?: PaginationParams): number {\n const queryLimit = (query.q as SelectQueryData).limit\n\n const maxPageSize = config?.maxPageSize ?? config?.pageSize ?? queryLimit\n if (!maxPageSize) {\n throw new Error(\"Set query limit, config.maxPageSize, config.pageSize or params.limit.\")\n }\n\n return Math.max(1, Math.min(params?.size ?? config?.pageSize ?? maxPageSize, maxPageSize))\n}\n","import type { SortDir } from \"orchid-orm\"\nimport { raw } from \"orchid-orm\"\n\nimport type { ListQuery, PaginationConfig } from \"./base\"\nimport { getPageSize } from \"./base\"\nimport { createDirectedCursor, getQueryOrderFields, parseDirectedCursor } from \"./cursor/utils\"\n\nexport interface CursorPaginationParams {\n /** Page cursor, as returned by previous call in prevCursor / nextCursor. */\n cursor?: string\n /** Page size. */\n size?: number\n}\n\nexport type CursorPaginationPage<T extends ListQuery = ListQuery> = {\n items: Awaited<T>\n /** Effective page size. Number of items is guaranteed to be less or equal. */\n size: number\n /** Cursor pointing to previous page. */\n prevCursor?: string\n /** Cursor pointing to next page. */\n nextCursor?: string\n}\n\nexport async function createCursorPaginator(config?: PaginationConfig) {\n return function paginate<T extends ListQuery>(query: T, params?: CursorPaginationParams) {\n return paginateByCursor(query, config, params)\n }\n}\n\nexport async function paginateByCursor<T extends ListQuery>(query: T, config?: PaginationConfig, params?: CursorPaginationParams): Promise<CursorPaginationPage<T>> {\n const size = getPageSize(query, config, params)\n\n const orderFields = getQueryOrderFields(query)\n\n // poor man validation, TODO improve\n const parsedCursorMaybeValid = params?.cursor ? parseDirectedCursor(params.cursor) : undefined\n const parsedCursor = parsedCursorMaybeValid && parsedCursorMaybeValid.parts.length >= orderFields.length ? parsedCursorMaybeValid : undefined\n\n const reverse = parsedCursor?.reverse ?? false\n\n if (reverse) {\n // Reverse parsed order fields + reverse query ordering.\n orderFields.forEach((of) => {\n of[1] = !of[1]\n })\n const orderArg = Object.fromEntries(orderFields.map<[string, SortDir]>(([field, asc]) => [field, asc ? \"ASC\" : \"DESC\"]))\n query = query.clear(\"order\").order(orderArg as any)\n }\n\n if (parsedCursor) {\n // Prepare raw SQL.\n // For example, for (amount asc, id asc) order, that would be:\n // (amount, $id) >= ($amount, id)\n const leftSqlExpr = orderFields.map(([field, asc]) => asc ? field : `$${field}`).join(\",\")\n const rightSqlExp = orderFields.map(([field, asc]) => asc ? `$${field}` : field).join(\",\")\n const sqlExpr = `(${leftSqlExpr}) > (${rightSqlExp})`\n const values = Object.fromEntries(orderFields.map(([field], i) => [field, parsedCursor.parts[i]]))\n query = query.where(raw({ raw: sqlExpr, values }))\n }\n\n // Query 1 extra item to see if we can paginate farther in current direction.\n const items = await query.limit(size + 1)\n if (!Array.isArray(items)) {\n throw new TypeError(\"Query must return an array.\")\n }\n const hasContinuation = items.length > size\n if (hasContinuation) {\n items.splice(size)\n }\n if (reverse) {\n items.reverse()\n }\n\n function createItemCursor(item: any, reverse: boolean) {\n return createDirectedCursor(orderFields.map(([field]) => {\n // Can add custom serializer here if needed.\n return String(item[field])\n }), reverse)\n }\n\n // Prev cursor:\n // - for initial pagination, there is no prev page\n // - for forward pagination, prev page exists always\n // - for reverse pagination, prev page exists if we have a continuation\n const prevCursor = (parsedCursor && (parsedCursor.reverse === false || hasContinuation))\n ? createItemCursor(items[0], true)\n : undefined\n\n // Next cursor:\n // - for reverse pagination, next page exists always\n // - for initial or forward pagination, next page exists if we have a continuation\n const nextCursor = (parsedCursor?.reverse === true || hasContinuation)\n ? createItemCursor(items.at(-1), false)\n : undefined\n\n return { items, size, prevCursor, nextCursor }\n}\n","import { Buffer } from \"node:buffer\"\n\nimport type { SelectQueryData } from \"orchid-orm\"\n\nimport type { ListQuery } from \"../base\"\n\ntype OrderField = [field: string, asc: boolean]\n\nexport function getQueryOrderFields(query: ListQuery): OrderField[] {\n const orderFields = (query.q as SelectQueryData).order?.flatMap<[field: string, asc: boolean]>((orderItem) => {\n if (typeof orderItem === \"string\") {\n return [[orderItem, true]]\n } else if (typeof orderItem === \"object\") {\n return Object.entries(orderItem).map<[string, boolean]>(([field, order]) => {\n if (order === \"ASC\" || order === \"DESC\") {\n return [field, order === \"ASC\"]\n } else {\n throw new Error(\"Unsupported order: \" + order)\n }\n })\n } else {\n throw new TypeError(\"Unsupported order type: \" + orderItem)\n }\n })\n if (!orderFields?.length) {\n throw new Error(\"Query must be ordered.\")\n }\n return orderFields\n}\n\nexport function createCursor(parts: string[]) {\n return Buffer.from(parts.map(String).join(String.fromCharCode(0))).toString(\"base64url\")\n}\n\nexport function parseCursor(cursor: string) {\n return Buffer.from(cursor, \"base64url\").toString().split(String.fromCharCode(0))\n}\n\nexport function createDirectedCursor(parts: string[], reverse: boolean) {\n const cursor = createCursor(parts)\n return reverse ? \"-\" + cursor : cursor\n}\n\nexport function parseDirectedCursor(directedCursor: string) {\n const [cursor, reverse] = directedCursor.startsWith(\"-\") ? [directedCursor.slice(1), true] : [directedCursor, false]\n return { cursor, parts: parseCursor(cursor), reverse }\n}\n","import { getPageSize, type ListQuery, type PaginationConfig } from \"./base\"\n\nexport interface PagePaginationParams {\n /** Page, 1-based. */\n page?: number\n /** Page size. */\n size?: number\n}\n\nexport type PagePaginationPage<T extends ListQuery = ListQuery> = {\n items: Awaited<T>\n /** Effective page number. */\n page: number\n /** Effective page size. Number of items is guaranteed to be less or equal. */\n size: number\n /** Offset of the first item, 1-based. */\n offset: number\n /** Prev page numberm (if exists). */\n prevPage?: number\n /** Next page number (if exists). */\n nextPage?: number\n}\n\nexport function createPagePaginator(config?: PaginationConfig) {\n return function paginate<T extends ListQuery>(query: T, params?: PagePaginationParams) {\n return paginateByPage(query, config, params)\n }\n}\n\nexport async function paginateByPage<T extends ListQuery>(query: T, config?: PaginationConfig, params?: PagePaginationParams): Promise<PagePaginationPage<T>> {\n const size = getPageSize(query, config, params)\n\n const page = Math.max(1, params?.page ?? 1)\n const offset = (page - 1) * size\n\n const items = await query.offset(offset).limit(size + 1)\n const hasContinuation = items.length > size\n if (hasContinuation) {\n items.splice(size)\n }\n\n const prevPage = page > 1 ? page - 1 : undefined\n const nextPage = hasContinuation ? page + 1 : undefined\n\n return { items, page, size, offset, prevPage, nextPage }\n}\n"],"mappings":"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","names":["reverse"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "orchid-pagination",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Orchid ORM query pagination helpers",
|
|
6
|
+
"author": "Ilya Semenov",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/IlyaSemenov/orchid-pagination"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"main": "dist/index.cjs",
|
|
20
|
+
"types": "dist/index.d.ts",
|
|
21
|
+
"files": [
|
|
22
|
+
"!src/**/*.test.*",
|
|
23
|
+
"!src/**/*.tst.*",
|
|
24
|
+
"dist",
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@types/node": ">=18",
|
|
29
|
+
"orchid-orm": "^1.12.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@changesets/cli": "^2.27.10",
|
|
33
|
+
"@ilyasemenov/eslint-config": "^1.0.1",
|
|
34
|
+
"@types/node": "^22.10.1",
|
|
35
|
+
"eslint": "^9.15.0",
|
|
36
|
+
"husky": "^9.1.7",
|
|
37
|
+
"lint-staged": "^15.2.10",
|
|
38
|
+
"tsconfig-vite-node": "^1.1.2",
|
|
39
|
+
"tstyche": "^3.1.1",
|
|
40
|
+
"tsup": "^8.3.5",
|
|
41
|
+
"typescript": "^5.6.3",
|
|
42
|
+
"vitest": "^2.1.5"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"lint": "eslint --fix .",
|
|
47
|
+
"test": "vitest run && tstyche"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/base.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Query, QueryThen, SelectQueryData } from "orchid-orm"
|
|
2
|
+
|
|
3
|
+
export interface ListQuery extends Query {
|
|
4
|
+
then: QueryThen<unknown[]>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface PaginationConfig {
|
|
8
|
+
/** Default page size. If not set, `maxPageSize` is used. */
|
|
9
|
+
pageSize?: number
|
|
10
|
+
/** Max page size allowed to be passed in params. If not set, `pageSize` is used. */
|
|
11
|
+
maxPageSize?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PaginationParams {
|
|
15
|
+
/** Requested page size. */
|
|
16
|
+
size?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getPageSize(query: ListQuery, config?: PaginationConfig, params?: PaginationParams): number {
|
|
20
|
+
const queryLimit = (query.q as SelectQueryData).limit
|
|
21
|
+
|
|
22
|
+
const maxPageSize = config?.maxPageSize ?? config?.pageSize ?? queryLimit
|
|
23
|
+
if (!maxPageSize) {
|
|
24
|
+
throw new Error("Set query limit, config.maxPageSize, config.pageSize or params.limit.")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return Math.max(1, Math.min(params?.size ?? config?.pageSize ?? maxPageSize, maxPageSize))
|
|
28
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer"
|
|
2
|
+
|
|
3
|
+
import type { SelectQueryData } from "orchid-orm"
|
|
4
|
+
|
|
5
|
+
import type { ListQuery } from "../base"
|
|
6
|
+
|
|
7
|
+
type OrderField = [field: string, asc: boolean]
|
|
8
|
+
|
|
9
|
+
export function getQueryOrderFields(query: ListQuery): OrderField[] {
|
|
10
|
+
const orderFields = (query.q as SelectQueryData).order?.flatMap<[field: string, asc: boolean]>((orderItem) => {
|
|
11
|
+
if (typeof orderItem === "string") {
|
|
12
|
+
return [[orderItem, true]]
|
|
13
|
+
} else if (typeof orderItem === "object") {
|
|
14
|
+
return Object.entries(orderItem).map<[string, boolean]>(([field, order]) => {
|
|
15
|
+
if (order === "ASC" || order === "DESC") {
|
|
16
|
+
return [field, order === "ASC"]
|
|
17
|
+
} else {
|
|
18
|
+
throw new Error("Unsupported order: " + order)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
} else {
|
|
22
|
+
throw new TypeError("Unsupported order type: " + orderItem)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
if (!orderFields?.length) {
|
|
26
|
+
throw new Error("Query must be ordered.")
|
|
27
|
+
}
|
|
28
|
+
return orderFields
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createCursor(parts: string[]) {
|
|
32
|
+
return Buffer.from(parts.map(String).join(String.fromCharCode(0))).toString("base64url")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseCursor(cursor: string) {
|
|
36
|
+
return Buffer.from(cursor, "base64url").toString().split(String.fromCharCode(0))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createDirectedCursor(parts: string[], reverse: boolean) {
|
|
40
|
+
const cursor = createCursor(parts)
|
|
41
|
+
return reverse ? "-" + cursor : cursor
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseDirectedCursor(directedCursor: string) {
|
|
45
|
+
const [cursor, reverse] = directedCursor.startsWith("-") ? [directedCursor.slice(1), true] : [directedCursor, false]
|
|
46
|
+
return { cursor, parts: parseCursor(cursor), reverse }
|
|
47
|
+
}
|
package/src/cursor.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { SortDir } from "orchid-orm"
|
|
2
|
+
import { raw } from "orchid-orm"
|
|
3
|
+
|
|
4
|
+
import type { ListQuery, PaginationConfig } from "./base"
|
|
5
|
+
import { getPageSize } from "./base"
|
|
6
|
+
import { createDirectedCursor, getQueryOrderFields, parseDirectedCursor } from "./cursor/utils"
|
|
7
|
+
|
|
8
|
+
export interface CursorPaginationParams {
|
|
9
|
+
/** Page cursor, as returned by previous call in prevCursor / nextCursor. */
|
|
10
|
+
cursor?: string
|
|
11
|
+
/** Page size. */
|
|
12
|
+
size?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type CursorPaginationPage<T extends ListQuery = ListQuery> = {
|
|
16
|
+
items: Awaited<T>
|
|
17
|
+
/** Effective page size. Number of items is guaranteed to be less or equal. */
|
|
18
|
+
size: number
|
|
19
|
+
/** Cursor pointing to previous page. */
|
|
20
|
+
prevCursor?: string
|
|
21
|
+
/** Cursor pointing to next page. */
|
|
22
|
+
nextCursor?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function createCursorPaginator(config?: PaginationConfig) {
|
|
26
|
+
return function paginate<T extends ListQuery>(query: T, params?: CursorPaginationParams) {
|
|
27
|
+
return paginateByCursor(query, config, params)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function paginateByCursor<T extends ListQuery>(query: T, config?: PaginationConfig, params?: CursorPaginationParams): Promise<CursorPaginationPage<T>> {
|
|
32
|
+
const size = getPageSize(query, config, params)
|
|
33
|
+
|
|
34
|
+
const orderFields = getQueryOrderFields(query)
|
|
35
|
+
|
|
36
|
+
// poor man validation, TODO improve
|
|
37
|
+
const parsedCursorMaybeValid = params?.cursor ? parseDirectedCursor(params.cursor) : undefined
|
|
38
|
+
const parsedCursor = parsedCursorMaybeValid && parsedCursorMaybeValid.parts.length >= orderFields.length ? parsedCursorMaybeValid : undefined
|
|
39
|
+
|
|
40
|
+
const reverse = parsedCursor?.reverse ?? false
|
|
41
|
+
|
|
42
|
+
if (reverse) {
|
|
43
|
+
// Reverse parsed order fields + reverse query ordering.
|
|
44
|
+
orderFields.forEach((of) => {
|
|
45
|
+
of[1] = !of[1]
|
|
46
|
+
})
|
|
47
|
+
const orderArg = Object.fromEntries(orderFields.map<[string, SortDir]>(([field, asc]) => [field, asc ? "ASC" : "DESC"]))
|
|
48
|
+
query = query.clear("order").order(orderArg as any)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (parsedCursor) {
|
|
52
|
+
// Prepare raw SQL.
|
|
53
|
+
// For example, for (amount asc, id asc) order, that would be:
|
|
54
|
+
// (amount, $id) >= ($amount, id)
|
|
55
|
+
const leftSqlExpr = orderFields.map(([field, asc]) => asc ? field : `$${field}`).join(",")
|
|
56
|
+
const rightSqlExp = orderFields.map(([field, asc]) => asc ? `$${field}` : field).join(",")
|
|
57
|
+
const sqlExpr = `(${leftSqlExpr}) > (${rightSqlExp})`
|
|
58
|
+
const values = Object.fromEntries(orderFields.map(([field], i) => [field, parsedCursor.parts[i]]))
|
|
59
|
+
query = query.where(raw({ raw: sqlExpr, values }))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Query 1 extra item to see if we can paginate farther in current direction.
|
|
63
|
+
const items = await query.limit(size + 1)
|
|
64
|
+
if (!Array.isArray(items)) {
|
|
65
|
+
throw new TypeError("Query must return an array.")
|
|
66
|
+
}
|
|
67
|
+
const hasContinuation = items.length > size
|
|
68
|
+
if (hasContinuation) {
|
|
69
|
+
items.splice(size)
|
|
70
|
+
}
|
|
71
|
+
if (reverse) {
|
|
72
|
+
items.reverse()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createItemCursor(item: any, reverse: boolean) {
|
|
76
|
+
return createDirectedCursor(orderFields.map(([field]) => {
|
|
77
|
+
// Can add custom serializer here if needed.
|
|
78
|
+
return String(item[field])
|
|
79
|
+
}), reverse)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Prev cursor:
|
|
83
|
+
// - for initial pagination, there is no prev page
|
|
84
|
+
// - for forward pagination, prev page exists always
|
|
85
|
+
// - for reverse pagination, prev page exists if we have a continuation
|
|
86
|
+
const prevCursor = (parsedCursor && (parsedCursor.reverse === false || hasContinuation))
|
|
87
|
+
? createItemCursor(items[0], true)
|
|
88
|
+
: undefined
|
|
89
|
+
|
|
90
|
+
// Next cursor:
|
|
91
|
+
// - for reverse pagination, next page exists always
|
|
92
|
+
// - for initial or forward pagination, next page exists if we have a continuation
|
|
93
|
+
const nextCursor = (parsedCursor?.reverse === true || hasContinuation)
|
|
94
|
+
? createItemCursor(items.at(-1), false)
|
|
95
|
+
: undefined
|
|
96
|
+
|
|
97
|
+
return { items, size, prevCursor, nextCursor }
|
|
98
|
+
}
|
package/src/index.ts
ADDED
package/src/page.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { getPageSize, type ListQuery, type PaginationConfig } from "./base"
|
|
2
|
+
|
|
3
|
+
export interface PagePaginationParams {
|
|
4
|
+
/** Page, 1-based. */
|
|
5
|
+
page?: number
|
|
6
|
+
/** Page size. */
|
|
7
|
+
size?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type PagePaginationPage<T extends ListQuery = ListQuery> = {
|
|
11
|
+
items: Awaited<T>
|
|
12
|
+
/** Effective page number. */
|
|
13
|
+
page: number
|
|
14
|
+
/** Effective page size. Number of items is guaranteed to be less or equal. */
|
|
15
|
+
size: number
|
|
16
|
+
/** Offset of the first item, 1-based. */
|
|
17
|
+
offset: number
|
|
18
|
+
/** Prev page numberm (if exists). */
|
|
19
|
+
prevPage?: number
|
|
20
|
+
/** Next page number (if exists). */
|
|
21
|
+
nextPage?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createPagePaginator(config?: PaginationConfig) {
|
|
25
|
+
return function paginate<T extends ListQuery>(query: T, params?: PagePaginationParams) {
|
|
26
|
+
return paginateByPage(query, config, params)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function paginateByPage<T extends ListQuery>(query: T, config?: PaginationConfig, params?: PagePaginationParams): Promise<PagePaginationPage<T>> {
|
|
31
|
+
const size = getPageSize(query, config, params)
|
|
32
|
+
|
|
33
|
+
const page = Math.max(1, params?.page ?? 1)
|
|
34
|
+
const offset = (page - 1) * size
|
|
35
|
+
|
|
36
|
+
const items = await query.offset(offset).limit(size + 1)
|
|
37
|
+
const hasContinuation = items.length > size
|
|
38
|
+
if (hasContinuation) {
|
|
39
|
+
items.splice(size)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const prevPage = page > 1 ? page - 1 : undefined
|
|
43
|
+
const nextPage = hasContinuation ? page + 1 : undefined
|
|
44
|
+
|
|
45
|
+
return { items, page, size, offset, prevPage, nextPage }
|
|
46
|
+
}
|