@sambuddha_npm_dev/pagecraft 0.0.0-stage → 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 +17 -0
- package/README.md +94 -2
- package/package.json +30 -4
- package/src/index.js +109 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PageCraft contributors
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,95 @@
|
|
|
1
|
-
#
|
|
1
|
+
# PageCraft
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
PageCraft is a small, dependency-free JavaScript utility for filtering, searching, sorting, and paginating arrays. It uses ES modules and runs on Node.js 18 or newer.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install pagecraft
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { paginate } from "pagecraft";
|
|
15
|
+
|
|
16
|
+
const users = [
|
|
17
|
+
{ id: 1, name: "Sam", role: "student" },
|
|
18
|
+
{ id: 2, name: "Alex", role: "admin" },
|
|
19
|
+
{ id: 3, name: "Samira", role: "student" }
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const result = paginate(users, {
|
|
23
|
+
page: 1,
|
|
24
|
+
limit: 2,
|
|
25
|
+
filters: { role: "student" },
|
|
26
|
+
search: { query: "sam", fields: ["name"] },
|
|
27
|
+
sortBy: "name",
|
|
28
|
+
order: "asc"
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
console.log(result.data); // Sam, Samira
|
|
32
|
+
console.log(result.pagination);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## API
|
|
36
|
+
|
|
37
|
+
### `paginate(data, options)`
|
|
38
|
+
|
|
39
|
+
Options are `page` (default `1`), `limit` (default `10`, maximum `100`), `filters` (exact matches; all must match), `search` (`{ query, fields }`, case-insensitive substring match), `sortBy` (property name), and `order` (`"asc"` or `"desc"`, default `"asc"`). Processing order is filtering, searching, sorting, then pagination. Inputs are not mutated. An out-of-range page returns an empty `data` array; empty input has `totalPages: 0`.
|
|
40
|
+
|
|
41
|
+
Result shape:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
{
|
|
45
|
+
data: [],
|
|
46
|
+
pagination: {
|
|
47
|
+
page: 1, limit: 10, totalItems: 0, totalPages: 0,
|
|
48
|
+
hasNextPage: false, hasPreviousPage: false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Helpers
|
|
54
|
+
|
|
55
|
+
- `filterData(data, filters)` returns items that exactly match every filter.
|
|
56
|
+
- `searchData(data, query, fields)` searches selected properties. A blank query returns a shallow copy of the input array.
|
|
57
|
+
- `sortData(data, field, order)` returns a sorted copy; numbers compare numerically and other values compare as case-insensitive strings. Missing values sort last in ascending order.
|
|
58
|
+
- `validateQuery(options)` checks pagination and sorting options and returns normalized `{ page, limit, order }` values.
|
|
59
|
+
|
|
60
|
+
Invalid arrays, options, page values, limits, orders, and search configurations throw `TypeError` or `RangeError` with a descriptive message.
|
|
61
|
+
|
|
62
|
+
## Express example
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import express from "express";
|
|
66
|
+
import { paginate } from "pagecraft";
|
|
67
|
+
|
|
68
|
+
const app = express();
|
|
69
|
+
app.get("/users", (req, res) => {
|
|
70
|
+
try {
|
|
71
|
+
const result = paginate(users, {
|
|
72
|
+
page: req.query.page === undefined ? 1 : Number(req.query.page),
|
|
73
|
+
limit: req.query.limit === undefined ? 10 : Number(req.query.limit),
|
|
74
|
+
sortBy: req.query.sortBy,
|
|
75
|
+
order: req.query.order
|
|
76
|
+
});
|
|
77
|
+
res.json(result);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
res.status(400).json({ error: error.message });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
npm test
|
|
88
|
+
npm pack
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Tests use Node's built-in test runner. PageCraft only operates on arrays already in memory; use database-level queries for large datasets.
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT. See [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sambuddha_npm_dev/pagecraft",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Small in-memory helpers for pagination, sorting, filtering, searching, and query validation.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/SammyBanner45/PageCraft.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/SammyBanner45/PageCraft#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/SammyBanner45/PageCraft/issues"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./src/index.js",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/index.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src/",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "node --test",
|
|
25
|
+
"test:watch": "node --test --watch"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"keywords": ["pagination", "sorting", "filtering", "search", "query"]
|
|
32
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const DEFAULT_LIMIT = 10;
|
|
2
|
+
const MAX_LIMIT = 100;
|
|
3
|
+
|
|
4
|
+
export function validateQuery(options = {}) {
|
|
5
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
6
|
+
throw new TypeError("options must be an object");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const page = options.page ?? 1;
|
|
10
|
+
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
11
|
+
const order = options.order ?? "asc";
|
|
12
|
+
|
|
13
|
+
if (!Number.isInteger(page) || page < 1) {
|
|
14
|
+
throw new RangeError("page must be a positive integer");
|
|
15
|
+
}
|
|
16
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIMIT) {
|
|
17
|
+
throw new RangeError(`limit must be an integer between 1 and ${MAX_LIMIT}`);
|
|
18
|
+
}
|
|
19
|
+
if (order !== "asc" && order !== "desc") {
|
|
20
|
+
throw new RangeError('order must be "asc" or "desc"');
|
|
21
|
+
}
|
|
22
|
+
if (options.sortBy !== undefined && typeof options.sortBy !== "string") {
|
|
23
|
+
throw new TypeError("sortBy must be a string");
|
|
24
|
+
}
|
|
25
|
+
if (options.filters !== undefined && (options.filters === null || typeof options.filters !== "object" || Array.isArray(options.filters))) {
|
|
26
|
+
throw new TypeError("filters must be an object");
|
|
27
|
+
}
|
|
28
|
+
if (options.search !== undefined) {
|
|
29
|
+
const search = options.search;
|
|
30
|
+
if (search === null || typeof search !== "object" || Array.isArray(search)) {
|
|
31
|
+
throw new TypeError("search must be an object with query and fields");
|
|
32
|
+
}
|
|
33
|
+
if (typeof search.query !== "string") throw new TypeError("search.query must be a string");
|
|
34
|
+
if (!Array.isArray(search.fields) || search.fields.some((field) => typeof field !== "string")) {
|
|
35
|
+
throw new TypeError("search.fields must be an array of strings");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { page, limit, order };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function filterData(data, filters = {}) {
|
|
43
|
+
assertArray(data);
|
|
44
|
+
if (filters === null || typeof filters !== "object" || Array.isArray(filters)) {
|
|
45
|
+
throw new TypeError("filters must be an object");
|
|
46
|
+
}
|
|
47
|
+
const entries = Object.entries(filters);
|
|
48
|
+
return data.filter((item) => item !== null && typeof item === "object" && entries.every(([key, value]) => item[key] === value));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function searchData(data, query, fields) {
|
|
52
|
+
assertArray(data);
|
|
53
|
+
if (typeof query !== "string") throw new TypeError("query must be a string");
|
|
54
|
+
if (!Array.isArray(fields) || fields.some((field) => typeof field !== "string")) {
|
|
55
|
+
throw new TypeError("fields must be an array of strings");
|
|
56
|
+
}
|
|
57
|
+
const needle = query.trim().toLocaleLowerCase();
|
|
58
|
+
if (!needle) return [...data];
|
|
59
|
+
return data.filter((item) => item !== null && typeof item === "object" && fields.some((field) => {
|
|
60
|
+
const value = item[field];
|
|
61
|
+
return value !== null && value !== undefined && String(value).toLocaleLowerCase().includes(needle);
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function sortData(data, field, order = "asc") {
|
|
66
|
+
assertArray(data);
|
|
67
|
+
if (typeof field !== "string" || !field) throw new TypeError("field must be a non-empty string");
|
|
68
|
+
if (order !== "asc" && order !== "desc") throw new RangeError('order must be "asc" or "desc"');
|
|
69
|
+
const direction = order === "asc" ? 1 : -1;
|
|
70
|
+
return [...data].sort((left, right) => {
|
|
71
|
+
const a = left?.[field];
|
|
72
|
+
const b = right?.[field];
|
|
73
|
+
if (a == null && b == null) return 0;
|
|
74
|
+
if (a == null) return 1;
|
|
75
|
+
if (b == null) return -1;
|
|
76
|
+
const comparison = typeof a === "number" && typeof b === "number"
|
|
77
|
+
? a - b
|
|
78
|
+
: String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: "base" });
|
|
79
|
+
return comparison * direction;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function paginate(data, options = {}) {
|
|
84
|
+
assertArray(data);
|
|
85
|
+
const { page, limit } = validateQuery(options);
|
|
86
|
+
let result = [...data];
|
|
87
|
+
if (options.filters) result = filterData(result, options.filters);
|
|
88
|
+
if (options.search) result = searchData(result, options.search.query, options.search.fields);
|
|
89
|
+
if (options.sortBy) result = sortData(result, options.sortBy, options.order ?? "asc");
|
|
90
|
+
|
|
91
|
+
const totalItems = result.length;
|
|
92
|
+
const totalPages = Math.ceil(totalItems / limit);
|
|
93
|
+
const start = (page - 1) * limit;
|
|
94
|
+
return {
|
|
95
|
+
data: result.slice(start, start + limit),
|
|
96
|
+
pagination: {
|
|
97
|
+
page,
|
|
98
|
+
limit,
|
|
99
|
+
totalItems,
|
|
100
|
+
totalPages,
|
|
101
|
+
hasNextPage: page < totalPages,
|
|
102
|
+
hasPreviousPage: page > 1 && totalPages > 0
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function assertArray(data) {
|
|
108
|
+
if (!Array.isArray(data)) throw new TypeError("data must be an array");
|
|
109
|
+
}
|