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 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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACkBO,SAAS,YAAY,OAAkB,QAA2B,QAAmC;AAC1G,QAAM,aAAc,MAAM,EAAsB;AAEhD,QAAM,cAAc,QAAQ,eAAe,QAAQ,YAAY;AAC/D,MAAI,CAAC,aAAa;AAChB,UAAM,IAAI,MAAM,uEAAuE;AAAA,EACzF;AAEA,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,QAAQ,QAAQ,YAAY,aAAa,WAAW,CAAC;AAC3F;;;AC1BA,wBAAoB;;;ACDpB,yBAAuB;AAQhB,SAAS,oBAAoB,OAAgC;AAClE,QAAM,cAAe,MAAM,EAAsB,OAAO,QAAuC,CAAC,cAAc;AAC5G,QAAI,OAAO,cAAc,UAAU;AACjC,aAAO,CAAC,CAAC,WAAW,IAAI,CAAC;AAAA,IAC3B,WAAW,OAAO,cAAc,UAAU;AACxC,aAAO,OAAO,QAAQ,SAAS,EAAE,IAAuB,CAAC,CAAC,OAAO,KAAK,MAAM;AAC1E,YAAI,UAAU,SAAS,UAAU,QAAQ;AACvC,iBAAO,CAAC,OAAO,UAAU,KAAK;AAAA,QAChC,OAAO;AACL,gBAAM,IAAI,MAAM,wBAAwB,KAAK;AAAA,QAC/C;AAAA,MACF,CAAC;AAAA,IACH,OAAO;AACL,YAAM,IAAI,UAAU,6BAA6B,SAAS;AAAA,IAC5D;AAAA,EACF,CAAC;AACD,MAAI,CAAC,aAAa,QAAQ;AACxB,UAAM,IAAI,MAAM,wBAAwB;AAAA,EAC1C;AACA,SAAO;AACT;AAEO,SAAS,aAAa,OAAiB;AAC5C,SAAO,0BAAO,KAAK,MAAM,IAAI,MAAM,EAAE,KAAK,OAAO,aAAa,CAAC,CAAC,CAAC,EAAE,SAAS,WAAW;AACzF;AAEO,SAAS,YAAY,QAAgB;AAC1C,SAAO,0BAAO,KAAK,QAAQ,WAAW,EAAE,SAAS,EAAE,MAAM,OAAO,aAAa,CAAC,CAAC;AACjF;AAEO,SAAS,qBAAqB,OAAiB,SAAkB;AACtE,QAAM,SAAS,aAAa,KAAK;AACjC,SAAO,UAAU,MAAM,SAAS;AAClC;AAEO,SAAS,oBAAoB,gBAAwB;AAC1D,QAAM,CAAC,QAAQ,OAAO,IAAI,eAAe,WAAW,GAAG,IAAI,CAAC,eAAe,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,gBAAgB,KAAK;AACnH,SAAO,EAAE,QAAQ,OAAO,YAAY,MAAM,GAAG,QAAQ;AACvD;;;ADtBA,eAAsB,sBAAsB,QAA2B;AACrE,SAAO,SAAS,SAA8B,OAAU,QAAiC;AACvF,WAAO,iBAAiB,OAAO,QAAQ,MAAM;AAAA,EAC/C;AACF;AAEA,eAAsB,iBAAsC,OAAU,QAA2B,QAAmE;AAClK,QAAM,OAAO,YAAY,OAAO,QAAQ,MAAM;AAE9C,QAAM,cAAc,oBAAoB,KAAK;AAG7C,QAAM,yBAAyB,QAAQ,SAAS,oBAAoB,OAAO,MAAM,IAAI;AACrF,QAAM,eAAe,0BAA0B,uBAAuB,MAAM,UAAU,YAAY,SAAS,yBAAyB;AAEpI,QAAM,UAAU,cAAc,WAAW;AAEzC,MAAI,SAAS;AAEX,gBAAY,QAAQ,CAAC,OAAO;AAC1B,SAAG,CAAC,IAAI,CAAC,GAAG,CAAC;AAAA,IACf,CAAC;AACD,UAAM,WAAW,OAAO,YAAY,YAAY,IAAuB,CAAC,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,MAAM,QAAQ,MAAM,CAAC,CAAC;AACvH,YAAQ,MAAM,MAAM,OAAO,EAAE,MAAM,QAAe;AAAA,EACpD;AAEA,MAAI,cAAc;AAIhB,UAAM,cAAc,YAAY,IAAI,CAAC,CAAC,OAAO,GAAG,MAAM,MAAM,QAAQ,IAAI,KAAK,EAAE,EAAE,KAAK,GAAG;AACzF,UAAM,cAAc,YAAY,IAAI,CAAC,CAAC,OAAO,GAAG,MAAM,MAAM,IAAI,KAAK,KAAK,KAAK,EAAE,KAAK,GAAG;AACzF,UAAM,UAAU,IAAI,WAAW,QAAQ,WAAW;AAClD,UAAM,SAAS,OAAO,YAAY,YAAY,IAAI,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,OAAO,aAAa,MAAM,CAAC,CAAC,CAAC,CAAC;AACjG,YAAQ,MAAM,UAAM,uBAAI,EAAE,KAAK,SAAS,OAAO,CAAC,CAAC;AAAA,EACnD;AAGA,QAAM,QAAQ,MAAM,MAAM,MAAM,OAAO,CAAC;AACxC,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,UAAM,IAAI,UAAU,6BAA6B;AAAA,EACnD;AACA,QAAM,kBAAkB,MAAM,SAAS;AACvC,MAAI,iBAAiB;AACnB,UAAM,OAAO,IAAI;AAAA,EACnB;AACA,MAAI,SAAS;AACX,UAAM,QAAQ;AAAA,EAChB;AAEA,WAAS,iBAAiB,MAAWA,UAAkB;AACrD,WAAO,qBAAqB,YAAY,IAAI,CAAC,CAAC,KAAK,MAAM;AAEvD,aAAO,OAAO,KAAK,KAAK,CAAC;AAAA,IAC3B,CAAC,GAAGA,QAAO;AAAA,EACb;AAMA,QAAM,aAAc,iBAAiB,aAAa,YAAY,SAAS,mBACnE,iBAAiB,MAAM,CAAC,GAAG,IAAI,IAC/B;AAKJ,QAAM,aAAc,cAAc,YAAY,QAAQ,kBAClD,iBAAiB,MAAM,GAAG,EAAE,GAAG,KAAK,IACpC;AAEJ,SAAO,EAAE,OAAO,MAAM,YAAY,WAAW;AAC/C;;;AE1EO,SAAS,oBAAoB,QAA2B;AAC7D,SAAO,SAAS,SAA8B,OAAU,QAA+B;AACrF,WAAO,eAAe,OAAO,QAAQ,MAAM;AAAA,EAC7C;AACF;AAEA,eAAsB,eAAoC,OAAU,QAA2B,QAA+D;AAC5J,QAAM,OAAO,YAAY,OAAO,QAAQ,MAAM;AAE9C,QAAM,OAAO,KAAK,IAAI,GAAG,QAAQ,QAAQ,CAAC;AAC1C,QAAM,UAAU,OAAO,KAAK;AAE5B,QAAM,QAAQ,MAAM,MAAM,OAAO,MAAM,EAAE,MAAM,OAAO,CAAC;AACvD,QAAM,kBAAkB,MAAM,SAAS;AACvC,MAAI,iBAAiB;AACnB,UAAM,OAAO,IAAI;AAAA,EACnB;AAEA,QAAM,WAAW,OAAO,IAAI,OAAO,IAAI;AACvC,QAAM,WAAW,kBAAkB,OAAO,IAAI;AAE9C,SAAO,EAAE,OAAO,MAAM,MAAM,QAAQ,UAAU,SAAS;AACzD;","names":["reverse"]}
@@ -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 };
@@ -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
@@ -0,0 +1,3 @@
1
+ export * from "./base"
2
+ export * from "./cursor"
3
+ export * from "./page"
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
+ }