api-paginate 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/README.md +284 -0
- package/dist/index.d.mts +109 -0
- package/dist/index.d.ts +109 -0
- package/dist/index.js +181 -0
- package/dist/index.mjs +149 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# api-paginate
|
|
2
|
+
|
|
3
|
+
Paginate arrays in **Node** (and browsers). In-memory pagination for arrays. Use it in your return when sending data to users—in controllers, API routes, and try/catch blocks. Works with any array, regardless of ORM or database.
|
|
4
|
+
|
|
5
|
+
Returns JSON with `data`, `meta`, and `links`—ready to send. Configure once, pass only `route`.
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **Framework-agnostic** — Use with Express, Next.js, Fastify, or plain Node
|
|
10
|
+
- **ORM-agnostic** — Works with Mongoose, Prisma, Sequelize, raw SQL, or plain arrays
|
|
11
|
+
- **JSON output** — `data`, `meta`, and `links` (familiar shape for API responses)
|
|
12
|
+
- **Configure once** — Set `baseUrl` at startup; use only `route` in return statements
|
|
13
|
+
- **Client & server** — Works in Node and browsers (auto-detects `window.location.origin`)
|
|
14
|
+
- **TypeScript** — Types included
|
|
15
|
+
- **Small** — No database queries, no heavy dependencies
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install api-paginate
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The package supports both **CommonJS** (`require`) and **ESM** (`import`). Use `require('api-paginate')` in Node CommonJS and `import { ... } from 'api-paginate'` in ESM or TypeScript.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### 1. Configure once at startup
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
// CommonJS
|
|
31
|
+
const { configure } = require('api-paginate');
|
|
32
|
+
|
|
33
|
+
// ESM / TypeScript
|
|
34
|
+
import { configure } from 'api-paginate';
|
|
35
|
+
|
|
36
|
+
configure({ baseUrl: process.env.API_BASE_URL || 'https://myserver.com' });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Use in your return—only route, never baseUrl
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// CommonJS
|
|
43
|
+
const { paginate, paginateFromRequest } = require('api-paginate');
|
|
44
|
+
|
|
45
|
+
// ESM / TypeScript
|
|
46
|
+
import { paginate, paginateFromRequest } from 'api-paginate';
|
|
47
|
+
|
|
48
|
+
// paginate:
|
|
49
|
+
return res.json(paginate(docs, { route: '/users', current_page: 1, per_page: 20 }));
|
|
50
|
+
|
|
51
|
+
// paginateFromRequest (page from req.query):
|
|
52
|
+
return res.json(paginateFromRequest(req, users, { route: '/users', per_page: 15 }));
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Express example
|
|
56
|
+
|
|
57
|
+
Both CommonJS and ESM are supported:
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
// CommonJS
|
|
61
|
+
const { paginateFromRequest, configure } = require('api-paginate');
|
|
62
|
+
|
|
63
|
+
// ESM / TypeScript
|
|
64
|
+
import { paginateFromRequest, configure } from 'api-paginate';
|
|
65
|
+
|
|
66
|
+
configure({ baseUrl: process.env.API_BASE_URL });
|
|
67
|
+
|
|
68
|
+
app.get('/users', async (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const users = await User.find().lean();
|
|
71
|
+
res.json(paginateFromRequest(req, users, { route: '/users', per_page: 15 }));
|
|
72
|
+
} catch (err) {
|
|
73
|
+
res.status(500).json({ error: err.message });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
Base URL is read from config—never pass it in return statements. In the browser, `route` uses `window.location.origin` if you skip `configure`.
|
|
80
|
+
|
|
81
|
+
### When to use `paginate` vs `paginateFromRequest`
|
|
82
|
+
|
|
83
|
+
| Use | When |
|
|
84
|
+
|-----|------|
|
|
85
|
+
| `paginate` | You have the page number (e.g. from `req.query`, search params, or state) |
|
|
86
|
+
| `paginateFromRequest` | Express/Node backend: it reads `page` from `req.query` and builds `baseUrl` from `req` automatically |
|
|
87
|
+
|
|
88
|
+
### Next.js App Router example
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
// app/api/users/route.js
|
|
92
|
+
import { paginate } from 'api-paginate';
|
|
93
|
+
|
|
94
|
+
export async function GET(request) {
|
|
95
|
+
const { searchParams } = new URL(request.url);
|
|
96
|
+
const users = await User.find().lean();
|
|
97
|
+
const result = paginate(users, {
|
|
98
|
+
current_page: parseInt(searchParams.get('page') || '1'),
|
|
99
|
+
per_page: 15,
|
|
100
|
+
route: '/api/users',
|
|
101
|
+
});
|
|
102
|
+
return Response.json(result);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Error handling
|
|
107
|
+
|
|
108
|
+
Paginator throws `PaginatorError` when validation fails (invalid data, unsafe `pageParam`, etc.):
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
// CommonJS: const { paginate, PaginatorError } = require('api-paginate');
|
|
112
|
+
// ESM / TypeScript:
|
|
113
|
+
import { paginate, PaginatorError } from 'api-paginate';
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
return res.json(paginate(docs, { route: '/users', per_page: 20 }));
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err instanceof PaginatorError) {
|
|
119
|
+
return res.status(400).json({ error: err.message });
|
|
120
|
+
}
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Options
|
|
126
|
+
|
|
127
|
+
| Parameter | Type | Default | Description |
|
|
128
|
+
|-----------|------|---------|-------------|
|
|
129
|
+
| `current_page` | number | 1 | Current page (1-based) |
|
|
130
|
+
| `per_page` | number | 15 | Items per page |
|
|
131
|
+
| `route` | string | - | Route for links (e.g. `/api/users`); combined with configured baseUrl or auto-detected origin |
|
|
132
|
+
| `baseUrl` | string | - | Override: full base URL (e.g. `https://api.example.com/users`); use route + configure instead |
|
|
133
|
+
| `path` | string | - | Deprecated alias for route |
|
|
134
|
+
| `pageParam` | string | 'page' | Query param name for page links (alphanumeric + underscore only; safe against injection) |
|
|
135
|
+
|
|
136
|
+
### Config (`configure()`)
|
|
137
|
+
|
|
138
|
+
| Parameter | Description |
|
|
139
|
+
|-----------|-------------|
|
|
140
|
+
| `baseUrl` | Application origin (e.g. `https://api.example.com`) |
|
|
141
|
+
| `per_page` | Default items per page |
|
|
142
|
+
| `pageParam` | Default query param name |
|
|
143
|
+
| `route` | Default route when omitted in `paginate()` |
|
|
144
|
+
|
|
145
|
+
## Response shape
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"data": [{ "id": 1, "name": "Alice" }],
|
|
150
|
+
"meta": {
|
|
151
|
+
"current_page": 2,
|
|
152
|
+
"per_page": 15,
|
|
153
|
+
"total": 150,
|
|
154
|
+
"total_pages": 10,
|
|
155
|
+
"has_next": true,
|
|
156
|
+
"has_prev": true,
|
|
157
|
+
"from": 16,
|
|
158
|
+
"to": 30
|
|
159
|
+
},
|
|
160
|
+
"links": {
|
|
161
|
+
"first": "https://api.example.com/users?page=1",
|
|
162
|
+
"prev": "https://api.example.com/users?page=1",
|
|
163
|
+
"next": "https://api.example.com/users?page=3",
|
|
164
|
+
"last": "https://api.example.com/users?page=10"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- **`data`** — Slice of items for the current page
|
|
170
|
+
- **`meta`** — Pagination metadata; `from` and `to` are 1-based (e.g. "items 16–30 of 47")
|
|
171
|
+
- **`links`** — `first`, `prev`, `next`, `last` URLs; `null` when not applicable (e.g. `prev` on page 1)
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Development (this package)
|
|
176
|
+
|
|
177
|
+
From the repository root:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npm install
|
|
181
|
+
npm run build # build dist (cjs + esm + types)
|
|
182
|
+
npm test # run tests
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The docs and interactive simulator live in a separate repo (see the **api-paginate-web** repository).
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Pagination Algorithm
|
|
190
|
+
|
|
191
|
+
This section describes the exact algorithm used for in-memory pagination. The implementation is deterministic and predictable.
|
|
192
|
+
|
|
193
|
+
### Overview
|
|
194
|
+
|
|
195
|
+
The algorithm takes a plain array and returns a subset (the current page) plus metadata and optional navigation links. All indexing is **1-based** for pages but **0-based** internally for array slicing.
|
|
196
|
+
|
|
197
|
+
### Step-by-step
|
|
198
|
+
|
|
199
|
+
#### 1. Input normalization
|
|
200
|
+
|
|
201
|
+
| Input | Rule | Example |
|
|
202
|
+
|-------|------|---------|
|
|
203
|
+
| `current_page` | `Math.max(1, current_page ?? 1)` | `0`, `-1` → `1` |
|
|
204
|
+
| `per_page` | `Math.max(1, per_page ?? 15)` | `0`, `-5` → `1` |
|
|
205
|
+
|
|
206
|
+
Invalid values are clamped to at least 1 so every call yields a valid page.
|
|
207
|
+
|
|
208
|
+
#### 2. Derived values
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
total = data.length
|
|
212
|
+
totalPages = total === 0 ? 1 : Math.ceil(total / per_page)
|
|
213
|
+
currentPage = Math.min(page, totalPages)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- `totalPages` is 1 when there are no items (empty result, not zero pages).
|
|
217
|
+
- `currentPage` is clamped so requesting page 99 with 10 total pages returns page 10 instead of an empty slice.
|
|
218
|
+
|
|
219
|
+
#### 3. Slice indices (0-based)
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
start = (currentPage - 1) * per_page
|
|
223
|
+
end = start + per_page
|
|
224
|
+
pageData = data.slice(start, end)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`Array.prototype.slice(start, end)` is used, so the range is `[start, end)` (end is exclusive).
|
|
228
|
+
|
|
229
|
+
**Example:** `total = 47`, `per_page = 10`, `current_page = 3`
|
|
230
|
+
- `totalPages = 5`
|
|
231
|
+
- `currentPage = 3`
|
|
232
|
+
- `start = 20`, `end = 30`
|
|
233
|
+
- `pageData = data[20..29]` (10 items)
|
|
234
|
+
|
|
235
|
+
#### 4. Meta
|
|
236
|
+
|
|
237
|
+
| Field | Formula | Notes |
|
|
238
|
+
|-------|---------|-------|
|
|
239
|
+
| `current_page` | `currentPage` | 1-based |
|
|
240
|
+
| `per_page` | per_page | Items per page |
|
|
241
|
+
| `total` | `data.length` | Total items |
|
|
242
|
+
| `total_pages` | `totalPages` | At least 1 |
|
|
243
|
+
| `has_next` | `currentPage < totalPages` | True if a next page exists |
|
|
244
|
+
| `has_prev` | `currentPage > 1` | True if a previous page exists |
|
|
245
|
+
| `from` | `start + 1` if page non-empty, else `null` | 1-based first item index |
|
|
246
|
+
| `to` | `Math.min(end, total)` if page non-empty, else `null` | 1-based last item index |
|
|
247
|
+
|
|
248
|
+
`from` and `to` use 1-based, human-readable indexing (e.g. “items 16–30 of 47”).
|
|
249
|
+
|
|
250
|
+
#### 5. Links (optional)
|
|
251
|
+
|
|
252
|
+
If `baseUrl` or `path` is provided:
|
|
253
|
+
|
|
254
|
+
- Append `?page=N` or `&page=N` depending on whether the URL already contains `?`
|
|
255
|
+
- Use `pageParam` (default `"page"`) for the query parameter name
|
|
256
|
+
- `first`, `prev`, `next`, `last` are built from `current_page` and `total_pages`
|
|
257
|
+
- Any link that does not apply (e.g. `prev` on page 1) is `null`
|
|
258
|
+
|
|
259
|
+
### Edge cases
|
|
260
|
+
|
|
261
|
+
| Case | Behavior |
|
|
262
|
+
|------|----------|
|
|
263
|
+
| Empty array | `data: []`, `meta.total: 0`, `meta.total_pages: 1`, `from`/`to`: `null` |
|
|
264
|
+
| Page beyond last page | Clamped to last page; returns last page’s data |
|
|
265
|
+
| `per_page` larger than total | Single page with all items |
|
|
266
|
+
| `per_page = 1` | One item per page |
|
|
267
|
+
|
|
268
|
+
### Complexity
|
|
269
|
+
|
|
270
|
+
- **Time:** O(1) for meta and links; O(`per_page`) for `Array.prototype.slice` (shallow copy of that slice).
|
|
271
|
+
- **Space:** O(`per_page`) for the `data` slice; O(1) extra for metadata and link strings.
|
|
272
|
+
|
|
273
|
+
### Invariants
|
|
274
|
+
|
|
275
|
+
1. `1 ≤ current_page ≤ total_pages` always.
|
|
276
|
+
2. `pageData.length ≤ per_page`.
|
|
277
|
+
3. `from` and `to` are 1-based indices, or `null` when the page is empty.
|
|
278
|
+
4. `from ≤ to` when both are non-null.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## License
|
|
283
|
+
|
|
284
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
interface PaginateOptions {
|
|
2
|
+
/** Current page (1-based); use this instead of deprecated page */
|
|
3
|
+
current_page?: number;
|
|
4
|
+
/** Items per page; use this instead of deprecated perPage */
|
|
5
|
+
per_page?: number;
|
|
6
|
+
/** Base URL for link generation; if omitted, uses configured baseUrl or auto-detects in browser */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
/** Route/path for links (e.g. '/api/users'). Combines with configured baseUrl or window.origin. */
|
|
9
|
+
route?: string;
|
|
10
|
+
/** @deprecated Use route instead. Path for link generation. */
|
|
11
|
+
path?: string;
|
|
12
|
+
/** Query param name for page (default: 'page') */
|
|
13
|
+
pageParam?: string;
|
|
14
|
+
}
|
|
15
|
+
interface PaginateMeta {
|
|
16
|
+
current_page: number;
|
|
17
|
+
per_page: number;
|
|
18
|
+
total: number;
|
|
19
|
+
total_pages: number;
|
|
20
|
+
has_next: boolean;
|
|
21
|
+
has_prev: boolean;
|
|
22
|
+
from: number | null;
|
|
23
|
+
to: number | null;
|
|
24
|
+
}
|
|
25
|
+
interface PaginateLinks {
|
|
26
|
+
first: string | null;
|
|
27
|
+
prev: string | null;
|
|
28
|
+
next: string | null;
|
|
29
|
+
last: string | null;
|
|
30
|
+
}
|
|
31
|
+
interface PaginatedResult<T> {
|
|
32
|
+
data: T[];
|
|
33
|
+
meta: PaginateMeta;
|
|
34
|
+
links: PaginateLinks;
|
|
35
|
+
}
|
|
36
|
+
/** Express-style request (subset needed for paginateFromRequest) */
|
|
37
|
+
interface PaginateRequest {
|
|
38
|
+
query?: Record<string, string | string[] | undefined>;
|
|
39
|
+
protocol?: string;
|
|
40
|
+
get?: (name: string) => string | undefined;
|
|
41
|
+
path?: string;
|
|
42
|
+
originalUrl?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface PaginatorConfig {
|
|
46
|
+
/** Application base URL (origin), e.g. https://myserver.com or http://localhost:3000 */
|
|
47
|
+
baseUrl?: string;
|
|
48
|
+
/** Default route, e.g. /api (used when paginate is called without route) */
|
|
49
|
+
route?: string;
|
|
50
|
+
/** @deprecated Use route instead */
|
|
51
|
+
path?: string;
|
|
52
|
+
/** Default page param name */
|
|
53
|
+
pageParam?: string;
|
|
54
|
+
/** Default per_page */
|
|
55
|
+
per_page?: number;
|
|
56
|
+
/** @deprecated Use per_page instead */
|
|
57
|
+
perPage?: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Configure default options once. These are used when not overridden per-call.
|
|
61
|
+
* Call at app startup, e.g. configure({ baseUrl: process.env.NEXT_PUBLIC_BASE_URL })
|
|
62
|
+
*/
|
|
63
|
+
declare function configure(options: PaginatorConfig): void;
|
|
64
|
+
/**
|
|
65
|
+
* Get current config (for testing or inspection)
|
|
66
|
+
*/
|
|
67
|
+
declare function getConfig(): Readonly<PaginatorConfig>;
|
|
68
|
+
/**
|
|
69
|
+
* Reset config (mainly for testing)
|
|
70
|
+
*/
|
|
71
|
+
declare function resetConfig(): void;
|
|
72
|
+
|
|
73
|
+
/** Custom error for paginator validation failures */
|
|
74
|
+
declare class PaginatorError extends Error {
|
|
75
|
+
constructor(message: string);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Paginate an array of data with Laravel-style meta and links.
|
|
80
|
+
* Validates inputs and throws PaginatorError on invalid data.
|
|
81
|
+
*/
|
|
82
|
+
declare function paginate<T>(data: T[], options?: PaginateOptions): PaginatedResult<T>;
|
|
83
|
+
|
|
84
|
+
interface PaginateFromRequestOptions {
|
|
85
|
+
/** Route/path for links (defaults to request path) */
|
|
86
|
+
route?: string;
|
|
87
|
+
/** Items per page (default: 15); use per_page instead of deprecated perPage */
|
|
88
|
+
per_page?: number;
|
|
89
|
+
/** @deprecated Use per_page instead */
|
|
90
|
+
perPage?: number;
|
|
91
|
+
/** Query param name for page (default: 'page') */
|
|
92
|
+
pageParam?: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Paginate data for a backend response. Use in controllers: res.json(paginateFromRequest(req, data))
|
|
96
|
+
*
|
|
97
|
+
* Extracts page from req.query, builds baseUrl from req, and returns { data, meta, links }
|
|
98
|
+
* ready for res.json() or Response.json().
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // Express
|
|
102
|
+
* app.get('/users', async (req, res) => {
|
|
103
|
+
* const users = await User.find().lean();
|
|
104
|
+
* res.json(paginateFromRequest(req, users, { per_page: 15 }));
|
|
105
|
+
* });
|
|
106
|
+
*/
|
|
107
|
+
declare function paginateFromRequest<T>(request: PaginateRequest, data: T[], options?: PaginateFromRequestOptions): PaginatedResult<T>;
|
|
108
|
+
|
|
109
|
+
export { type PaginateFromRequestOptions, type PaginateLinks, type PaginateMeta, type PaginateOptions, type PaginateRequest, type PaginatedResult, type PaginatorConfig, PaginatorError, configure, getConfig, paginate, paginateFromRequest, resetConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
interface PaginateOptions {
|
|
2
|
+
/** Current page (1-based); use this instead of deprecated page */
|
|
3
|
+
current_page?: number;
|
|
4
|
+
/** Items per page; use this instead of deprecated perPage */
|
|
5
|
+
per_page?: number;
|
|
6
|
+
/** Base URL for link generation; if omitted, uses configured baseUrl or auto-detects in browser */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
/** Route/path for links (e.g. '/api/users'). Combines with configured baseUrl or window.origin. */
|
|
9
|
+
route?: string;
|
|
10
|
+
/** @deprecated Use route instead. Path for link generation. */
|
|
11
|
+
path?: string;
|
|
12
|
+
/** Query param name for page (default: 'page') */
|
|
13
|
+
pageParam?: string;
|
|
14
|
+
}
|
|
15
|
+
interface PaginateMeta {
|
|
16
|
+
current_page: number;
|
|
17
|
+
per_page: number;
|
|
18
|
+
total: number;
|
|
19
|
+
total_pages: number;
|
|
20
|
+
has_next: boolean;
|
|
21
|
+
has_prev: boolean;
|
|
22
|
+
from: number | null;
|
|
23
|
+
to: number | null;
|
|
24
|
+
}
|
|
25
|
+
interface PaginateLinks {
|
|
26
|
+
first: string | null;
|
|
27
|
+
prev: string | null;
|
|
28
|
+
next: string | null;
|
|
29
|
+
last: string | null;
|
|
30
|
+
}
|
|
31
|
+
interface PaginatedResult<T> {
|
|
32
|
+
data: T[];
|
|
33
|
+
meta: PaginateMeta;
|
|
34
|
+
links: PaginateLinks;
|
|
35
|
+
}
|
|
36
|
+
/** Express-style request (subset needed for paginateFromRequest) */
|
|
37
|
+
interface PaginateRequest {
|
|
38
|
+
query?: Record<string, string | string[] | undefined>;
|
|
39
|
+
protocol?: string;
|
|
40
|
+
get?: (name: string) => string | undefined;
|
|
41
|
+
path?: string;
|
|
42
|
+
originalUrl?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface PaginatorConfig {
|
|
46
|
+
/** Application base URL (origin), e.g. https://myserver.com or http://localhost:3000 */
|
|
47
|
+
baseUrl?: string;
|
|
48
|
+
/** Default route, e.g. /api (used when paginate is called without route) */
|
|
49
|
+
route?: string;
|
|
50
|
+
/** @deprecated Use route instead */
|
|
51
|
+
path?: string;
|
|
52
|
+
/** Default page param name */
|
|
53
|
+
pageParam?: string;
|
|
54
|
+
/** Default per_page */
|
|
55
|
+
per_page?: number;
|
|
56
|
+
/** @deprecated Use per_page instead */
|
|
57
|
+
perPage?: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Configure default options once. These are used when not overridden per-call.
|
|
61
|
+
* Call at app startup, e.g. configure({ baseUrl: process.env.NEXT_PUBLIC_BASE_URL })
|
|
62
|
+
*/
|
|
63
|
+
declare function configure(options: PaginatorConfig): void;
|
|
64
|
+
/**
|
|
65
|
+
* Get current config (for testing or inspection)
|
|
66
|
+
*/
|
|
67
|
+
declare function getConfig(): Readonly<PaginatorConfig>;
|
|
68
|
+
/**
|
|
69
|
+
* Reset config (mainly for testing)
|
|
70
|
+
*/
|
|
71
|
+
declare function resetConfig(): void;
|
|
72
|
+
|
|
73
|
+
/** Custom error for paginator validation failures */
|
|
74
|
+
declare class PaginatorError extends Error {
|
|
75
|
+
constructor(message: string);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Paginate an array of data with Laravel-style meta and links.
|
|
80
|
+
* Validates inputs and throws PaginatorError on invalid data.
|
|
81
|
+
*/
|
|
82
|
+
declare function paginate<T>(data: T[], options?: PaginateOptions): PaginatedResult<T>;
|
|
83
|
+
|
|
84
|
+
interface PaginateFromRequestOptions {
|
|
85
|
+
/** Route/path for links (defaults to request path) */
|
|
86
|
+
route?: string;
|
|
87
|
+
/** Items per page (default: 15); use per_page instead of deprecated perPage */
|
|
88
|
+
per_page?: number;
|
|
89
|
+
/** @deprecated Use per_page instead */
|
|
90
|
+
perPage?: number;
|
|
91
|
+
/** Query param name for page (default: 'page') */
|
|
92
|
+
pageParam?: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Paginate data for a backend response. Use in controllers: res.json(paginateFromRequest(req, data))
|
|
96
|
+
*
|
|
97
|
+
* Extracts page from req.query, builds baseUrl from req, and returns { data, meta, links }
|
|
98
|
+
* ready for res.json() or Response.json().
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // Express
|
|
102
|
+
* app.get('/users', async (req, res) => {
|
|
103
|
+
* const users = await User.find().lean();
|
|
104
|
+
* res.json(paginateFromRequest(req, users, { per_page: 15 }));
|
|
105
|
+
* });
|
|
106
|
+
*/
|
|
107
|
+
declare function paginateFromRequest<T>(request: PaginateRequest, data: T[], options?: PaginateFromRequestOptions): PaginatedResult<T>;
|
|
108
|
+
|
|
109
|
+
export { type PaginateFromRequestOptions, type PaginateLinks, type PaginateMeta, type PaginateOptions, type PaginateRequest, type PaginatedResult, type PaginatorConfig, PaginatorError, configure, getConfig, paginate, paginateFromRequest, resetConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
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 index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
PaginatorError: () => PaginatorError,
|
|
24
|
+
configure: () => configure,
|
|
25
|
+
getConfig: () => getConfig,
|
|
26
|
+
paginate: () => paginate,
|
|
27
|
+
paginateFromRequest: () => paginateFromRequest,
|
|
28
|
+
resetConfig: () => resetConfig
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/config.ts
|
|
33
|
+
var config = {};
|
|
34
|
+
function configure(options) {
|
|
35
|
+
config = { ...config, ...options };
|
|
36
|
+
}
|
|
37
|
+
function getConfig() {
|
|
38
|
+
return { ...config };
|
|
39
|
+
}
|
|
40
|
+
function resetConfig() {
|
|
41
|
+
config = {};
|
|
42
|
+
}
|
|
43
|
+
function joinBaseAndPath(base, path) {
|
|
44
|
+
const b = base.replace(/\/+$/, "");
|
|
45
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
46
|
+
return `${b}${p}`;
|
|
47
|
+
}
|
|
48
|
+
function resolveBaseUrl(options) {
|
|
49
|
+
const opts = options ?? {};
|
|
50
|
+
if (typeof opts.baseUrl === "string" && opts.baseUrl.trim()) {
|
|
51
|
+
return opts.baseUrl.trim();
|
|
52
|
+
}
|
|
53
|
+
const path = (typeof opts.route === "string" ? opts.route : opts.path) ?? "";
|
|
54
|
+
const pathTrimmed = typeof path === "string" ? path.trim() : "";
|
|
55
|
+
if (pathTrimmed && config.baseUrl) {
|
|
56
|
+
return joinBaseAndPath(config.baseUrl.trim(), pathTrimmed);
|
|
57
|
+
}
|
|
58
|
+
if (pathTrimmed && typeof window !== "undefined" && window?.location?.origin) {
|
|
59
|
+
return joinBaseAndPath(window.location.origin, pathTrimmed);
|
|
60
|
+
}
|
|
61
|
+
if (pathTrimmed) return pathTrimmed;
|
|
62
|
+
const configRoute = config.route ?? config.path;
|
|
63
|
+
if (config.baseUrl && configRoute) {
|
|
64
|
+
return joinBaseAndPath(config.baseUrl.trim(), configRoute.trim());
|
|
65
|
+
}
|
|
66
|
+
return config.baseUrl?.trim() ?? "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/validate.ts
|
|
70
|
+
var MAX_PER_PAGE = 1e4;
|
|
71
|
+
var SAFE_PARAM_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
72
|
+
var PaginatorError = class _PaginatorError extends Error {
|
|
73
|
+
constructor(message) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = "PaginatorError";
|
|
76
|
+
Object.setPrototypeOf(this, _PaginatorError.prototype);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
function toSafeInt(value, defaults) {
|
|
80
|
+
if (value === void 0 || value === null) return defaults.fallback;
|
|
81
|
+
const n = typeof value === "number" ? value : parseInt(String(value), 10);
|
|
82
|
+
if (!Number.isFinite(n) || n < defaults.min) return defaults.min;
|
|
83
|
+
if (n > defaults.max) return defaults.max;
|
|
84
|
+
return Math.floor(n);
|
|
85
|
+
}
|
|
86
|
+
function validatePaginateInputs(data, options) {
|
|
87
|
+
if (data === null || data === void 0) {
|
|
88
|
+
throw new PaginatorError("paginate(data, options): data is required and must be an array");
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(data)) {
|
|
91
|
+
throw new PaginatorError(
|
|
92
|
+
`paginate(data, options): data must be an array, got ${typeof data}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const opts = options ?? {};
|
|
96
|
+
const page = toSafeInt(opts.current_page, { min: 1, max: Number.MAX_SAFE_INTEGER, fallback: 1 });
|
|
97
|
+
const per_page = toSafeInt(opts.per_page, { min: 1, max: MAX_PER_PAGE, fallback: 15 });
|
|
98
|
+
const baseUrl = resolveBaseUrl(opts);
|
|
99
|
+
const rawParam = opts.pageParam ?? "page";
|
|
100
|
+
const param = typeof rawParam === "string" ? rawParam.trim() : "page";
|
|
101
|
+
if (!param) {
|
|
102
|
+
throw new PaginatorError("paginate: pageParam cannot be empty");
|
|
103
|
+
}
|
|
104
|
+
if (!SAFE_PARAM_REGEX.test(param)) {
|
|
105
|
+
throw new PaginatorError(
|
|
106
|
+
`paginate: pageParam must be a safe query param name (letters, numbers, underscore), got "${param}"`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
data,
|
|
111
|
+
page,
|
|
112
|
+
per_page,
|
|
113
|
+
baseUrl,
|
|
114
|
+
pageParam: param
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/paginate.ts
|
|
119
|
+
function paginate(data, options) {
|
|
120
|
+
const { data: arr, page, per_page, baseUrl, pageParam } = validatePaginateInputs(data, options);
|
|
121
|
+
const total = arr.length;
|
|
122
|
+
const totalPages = total === 0 ? 1 : Math.ceil(total / per_page);
|
|
123
|
+
const currentPage = Math.min(page, totalPages);
|
|
124
|
+
const start = (currentPage - 1) * per_page;
|
|
125
|
+
const end = start + per_page;
|
|
126
|
+
const pageData = arr.slice(start, end);
|
|
127
|
+
const meta = {
|
|
128
|
+
current_page: currentPage,
|
|
129
|
+
per_page,
|
|
130
|
+
total,
|
|
131
|
+
total_pages: totalPages,
|
|
132
|
+
has_next: currentPage < totalPages,
|
|
133
|
+
has_prev: currentPage > 1,
|
|
134
|
+
from: pageData.length > 0 ? start + 1 : null,
|
|
135
|
+
to: pageData.length > 0 ? Math.min(end, total) : null
|
|
136
|
+
};
|
|
137
|
+
const links = buildLinks(baseUrl, pageParam, meta);
|
|
138
|
+
return { data: pageData, meta, links };
|
|
139
|
+
}
|
|
140
|
+
var UNSAFE_URL_PREFIX = /^(javascript|data|vbscript):/i;
|
|
141
|
+
function buildLinks(baseUrl, param, meta) {
|
|
142
|
+
if (!baseUrl || UNSAFE_URL_PREFIX.test(baseUrl)) {
|
|
143
|
+
return { first: null, prev: null, next: null, last: null };
|
|
144
|
+
}
|
|
145
|
+
const sep = baseUrl.includes("?") ? "&" : "?";
|
|
146
|
+
const pageNum = (n) => String(n);
|
|
147
|
+
return {
|
|
148
|
+
first: meta.total_pages > 0 ? `${baseUrl}${sep}${encodeURIComponent(param)}=1` : null,
|
|
149
|
+
prev: meta.current_page > 1 ? `${baseUrl}${sep}${encodeURIComponent(param)}=${pageNum(meta.current_page - 1)}` : null,
|
|
150
|
+
next: meta.current_page < meta.total_pages ? `${baseUrl}${sep}${encodeURIComponent(param)}=${pageNum(meta.current_page + 1)}` : null,
|
|
151
|
+
last: meta.total_pages > 0 ? `${baseUrl}${sep}${encodeURIComponent(param)}=${pageNum(meta.total_pages)}` : null
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/paginateFromRequest.ts
|
|
156
|
+
function paginateFromRequest(request, data, options = {}) {
|
|
157
|
+
const req = request;
|
|
158
|
+
const pageParam = options.pageParam ?? "page";
|
|
159
|
+
const pageStr = req.query?.[pageParam];
|
|
160
|
+
const page = Array.isArray(pageStr) ? Math.max(1, parseInt(pageStr[0] ?? "1", 10) || 1) : Math.max(1, parseInt(String(pageStr ?? "1"), 10) || 1);
|
|
161
|
+
const protocol = req.protocol ?? "http";
|
|
162
|
+
const host = req.get?.("host") ?? "";
|
|
163
|
+
const pathname = (req.originalUrl ?? req.path ?? "/").split("?")[0];
|
|
164
|
+
const baseUrl = host ? `${protocol}://${host}${pathname}` : "";
|
|
165
|
+
return paginate(data, {
|
|
166
|
+
current_page: page,
|
|
167
|
+
per_page: options.per_page ?? options.perPage ?? 15,
|
|
168
|
+
baseUrl: baseUrl || void 0,
|
|
169
|
+
route: baseUrl ? void 0 : options.route ?? pathname,
|
|
170
|
+
pageParam
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
174
|
+
0 && (module.exports = {
|
|
175
|
+
PaginatorError,
|
|
176
|
+
configure,
|
|
177
|
+
getConfig,
|
|
178
|
+
paginate,
|
|
179
|
+
paginateFromRequest,
|
|
180
|
+
resetConfig
|
|
181
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var config = {};
|
|
3
|
+
function configure(options) {
|
|
4
|
+
config = { ...config, ...options };
|
|
5
|
+
}
|
|
6
|
+
function getConfig() {
|
|
7
|
+
return { ...config };
|
|
8
|
+
}
|
|
9
|
+
function resetConfig() {
|
|
10
|
+
config = {};
|
|
11
|
+
}
|
|
12
|
+
function joinBaseAndPath(base, path) {
|
|
13
|
+
const b = base.replace(/\/+$/, "");
|
|
14
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
15
|
+
return `${b}${p}`;
|
|
16
|
+
}
|
|
17
|
+
function resolveBaseUrl(options) {
|
|
18
|
+
const opts = options ?? {};
|
|
19
|
+
if (typeof opts.baseUrl === "string" && opts.baseUrl.trim()) {
|
|
20
|
+
return opts.baseUrl.trim();
|
|
21
|
+
}
|
|
22
|
+
const path = (typeof opts.route === "string" ? opts.route : opts.path) ?? "";
|
|
23
|
+
const pathTrimmed = typeof path === "string" ? path.trim() : "";
|
|
24
|
+
if (pathTrimmed && config.baseUrl) {
|
|
25
|
+
return joinBaseAndPath(config.baseUrl.trim(), pathTrimmed);
|
|
26
|
+
}
|
|
27
|
+
if (pathTrimmed && typeof window !== "undefined" && window?.location?.origin) {
|
|
28
|
+
return joinBaseAndPath(window.location.origin, pathTrimmed);
|
|
29
|
+
}
|
|
30
|
+
if (pathTrimmed) return pathTrimmed;
|
|
31
|
+
const configRoute = config.route ?? config.path;
|
|
32
|
+
if (config.baseUrl && configRoute) {
|
|
33
|
+
return joinBaseAndPath(config.baseUrl.trim(), configRoute.trim());
|
|
34
|
+
}
|
|
35
|
+
return config.baseUrl?.trim() ?? "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/validate.ts
|
|
39
|
+
var MAX_PER_PAGE = 1e4;
|
|
40
|
+
var SAFE_PARAM_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
41
|
+
var PaginatorError = class _PaginatorError extends Error {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "PaginatorError";
|
|
45
|
+
Object.setPrototypeOf(this, _PaginatorError.prototype);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
function toSafeInt(value, defaults) {
|
|
49
|
+
if (value === void 0 || value === null) return defaults.fallback;
|
|
50
|
+
const n = typeof value === "number" ? value : parseInt(String(value), 10);
|
|
51
|
+
if (!Number.isFinite(n) || n < defaults.min) return defaults.min;
|
|
52
|
+
if (n > defaults.max) return defaults.max;
|
|
53
|
+
return Math.floor(n);
|
|
54
|
+
}
|
|
55
|
+
function validatePaginateInputs(data, options) {
|
|
56
|
+
if (data === null || data === void 0) {
|
|
57
|
+
throw new PaginatorError("paginate(data, options): data is required and must be an array");
|
|
58
|
+
}
|
|
59
|
+
if (!Array.isArray(data)) {
|
|
60
|
+
throw new PaginatorError(
|
|
61
|
+
`paginate(data, options): data must be an array, got ${typeof data}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const opts = options ?? {};
|
|
65
|
+
const page = toSafeInt(opts.current_page, { min: 1, max: Number.MAX_SAFE_INTEGER, fallback: 1 });
|
|
66
|
+
const per_page = toSafeInt(opts.per_page, { min: 1, max: MAX_PER_PAGE, fallback: 15 });
|
|
67
|
+
const baseUrl = resolveBaseUrl(opts);
|
|
68
|
+
const rawParam = opts.pageParam ?? "page";
|
|
69
|
+
const param = typeof rawParam === "string" ? rawParam.trim() : "page";
|
|
70
|
+
if (!param) {
|
|
71
|
+
throw new PaginatorError("paginate: pageParam cannot be empty");
|
|
72
|
+
}
|
|
73
|
+
if (!SAFE_PARAM_REGEX.test(param)) {
|
|
74
|
+
throw new PaginatorError(
|
|
75
|
+
`paginate: pageParam must be a safe query param name (letters, numbers, underscore), got "${param}"`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
data,
|
|
80
|
+
page,
|
|
81
|
+
per_page,
|
|
82
|
+
baseUrl,
|
|
83
|
+
pageParam: param
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/paginate.ts
|
|
88
|
+
function paginate(data, options) {
|
|
89
|
+
const { data: arr, page, per_page, baseUrl, pageParam } = validatePaginateInputs(data, options);
|
|
90
|
+
const total = arr.length;
|
|
91
|
+
const totalPages = total === 0 ? 1 : Math.ceil(total / per_page);
|
|
92
|
+
const currentPage = Math.min(page, totalPages);
|
|
93
|
+
const start = (currentPage - 1) * per_page;
|
|
94
|
+
const end = start + per_page;
|
|
95
|
+
const pageData = arr.slice(start, end);
|
|
96
|
+
const meta = {
|
|
97
|
+
current_page: currentPage,
|
|
98
|
+
per_page,
|
|
99
|
+
total,
|
|
100
|
+
total_pages: totalPages,
|
|
101
|
+
has_next: currentPage < totalPages,
|
|
102
|
+
has_prev: currentPage > 1,
|
|
103
|
+
from: pageData.length > 0 ? start + 1 : null,
|
|
104
|
+
to: pageData.length > 0 ? Math.min(end, total) : null
|
|
105
|
+
};
|
|
106
|
+
const links = buildLinks(baseUrl, pageParam, meta);
|
|
107
|
+
return { data: pageData, meta, links };
|
|
108
|
+
}
|
|
109
|
+
var UNSAFE_URL_PREFIX = /^(javascript|data|vbscript):/i;
|
|
110
|
+
function buildLinks(baseUrl, param, meta) {
|
|
111
|
+
if (!baseUrl || UNSAFE_URL_PREFIX.test(baseUrl)) {
|
|
112
|
+
return { first: null, prev: null, next: null, last: null };
|
|
113
|
+
}
|
|
114
|
+
const sep = baseUrl.includes("?") ? "&" : "?";
|
|
115
|
+
const pageNum = (n) => String(n);
|
|
116
|
+
return {
|
|
117
|
+
first: meta.total_pages > 0 ? `${baseUrl}${sep}${encodeURIComponent(param)}=1` : null,
|
|
118
|
+
prev: meta.current_page > 1 ? `${baseUrl}${sep}${encodeURIComponent(param)}=${pageNum(meta.current_page - 1)}` : null,
|
|
119
|
+
next: meta.current_page < meta.total_pages ? `${baseUrl}${sep}${encodeURIComponent(param)}=${pageNum(meta.current_page + 1)}` : null,
|
|
120
|
+
last: meta.total_pages > 0 ? `${baseUrl}${sep}${encodeURIComponent(param)}=${pageNum(meta.total_pages)}` : null
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/paginateFromRequest.ts
|
|
125
|
+
function paginateFromRequest(request, data, options = {}) {
|
|
126
|
+
const req = request;
|
|
127
|
+
const pageParam = options.pageParam ?? "page";
|
|
128
|
+
const pageStr = req.query?.[pageParam];
|
|
129
|
+
const page = Array.isArray(pageStr) ? Math.max(1, parseInt(pageStr[0] ?? "1", 10) || 1) : Math.max(1, parseInt(String(pageStr ?? "1"), 10) || 1);
|
|
130
|
+
const protocol = req.protocol ?? "http";
|
|
131
|
+
const host = req.get?.("host") ?? "";
|
|
132
|
+
const pathname = (req.originalUrl ?? req.path ?? "/").split("?")[0];
|
|
133
|
+
const baseUrl = host ? `${protocol}://${host}${pathname}` : "";
|
|
134
|
+
return paginate(data, {
|
|
135
|
+
current_page: page,
|
|
136
|
+
per_page: options.per_page ?? options.perPage ?? 15,
|
|
137
|
+
baseUrl: baseUrl || void 0,
|
|
138
|
+
route: baseUrl ? void 0 : options.route ?? pathname,
|
|
139
|
+
pageParam
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
export {
|
|
143
|
+
PaginatorError,
|
|
144
|
+
configure,
|
|
145
|
+
getConfig,
|
|
146
|
+
paginate,
|
|
147
|
+
paginateFromRequest,
|
|
148
|
+
resetConfig
|
|
149
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "api-paginate",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Paginate arrays for Node and browsers. Returns JSON with data, meta, and links—ready for API responses.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist", "README.md"],
|
|
16
|
+
"keywords": ["pagination", "array", "node", "nodejs", "api", "express", "nextjs", "react", "angular", "json", "res.json", "meta", "links"],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Kabanda Kpanti Michael <michaelkpantiramp@gmail.com> (https://github.com/Michael-Builds)",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/Michael-Builds/paginate-json.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/Michael-Builds/paginate-json#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Michael-Builds/paginate-json/issues"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest",
|
|
31
|
+
"demo": "npm run build && npx serve -p 3000",
|
|
32
|
+
"fixtures": "node scripts/generate-fixtures.js",
|
|
33
|
+
"web": "cd web && npm run dev -- -p 3001"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@tailwindcss/postcss": "^4",
|
|
37
|
+
"tailwindcss": "^4",
|
|
38
|
+
"tsup": "^8.0.0",
|
|
39
|
+
"typescript": "^5.0.0",
|
|
40
|
+
"vitest": "^1.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|