json-server 1.0.0-beta.4 → 1.0.0-beta.6
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 +56 -52
- package/lib/app.js +66 -46
- package/lib/matches-where.js +64 -0
- package/lib/paginate.js +24 -0
- package/lib/parse-where.js +52 -0
- package/lib/service.js +15 -149
- package/lib/where-operators.js +4 -0
- package/package.json +3 -2
- package/schema.json +10 -0
package/README.md
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
[](https://github.com/typicode/json-server/actions/workflows/node.js.yml)
|
|
4
4
|
|
|
5
5
|
> [!IMPORTANT]
|
|
6
|
-
> Viewing beta v1 documentation – usable but expect breaking changes. For stable version, see [here](https://github.com/typicode/json-server/tree/v0)
|
|
6
|
+
> Viewing beta v1 documentation – usable but expect breaking changes. For stable version, see [here](https://github.com/typicode/json-server/tree/v0.17.4)
|
|
7
7
|
|
|
8
8
|
> [!NOTE]
|
|
9
|
-
> Using React ⚛️
|
|
9
|
+
> Using React ⚛️ and tired of CSS-in-JS? See [MistCSS](https://github.com/typicode/mistcss) 👀
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -20,6 +20,7 @@ Create a `db.json` or `db.json5` file
|
|
|
20
20
|
|
|
21
21
|
```json
|
|
22
22
|
{
|
|
23
|
+
"$schema": "./node_modules/json-server/schema.json",
|
|
23
24
|
"posts": [
|
|
24
25
|
{ "id": "1", "title": "a title", "views": 100 },
|
|
25
26
|
{ "id": "2", "title": "another title", "views": 200 }
|
|
@@ -86,6 +87,7 @@ Run `json-server --help` for a list of options
|
|
|
86
87
|
| <a href="https://mockend.com/" target="_blank"><img src="https://jsonplaceholder.typicode.com/mockend.svg" height="100px"></a> |
|
|
87
88
|
| <a href="https://zuplo.link/json-server-gh"><img src="https://github.com/user-attachments/assets/adfee31f-a8b6-4684-9a9b-af4f03ac5b75" height="100px"></a> |
|
|
88
89
|
| <a href="https://www.mintlify.com/"><img src="https://github.com/user-attachments/assets/bcc8cc48-b2d9-4577-8939-1eb4196b7cc5" height="100px"></a> |
|
|
90
|
+
| <a href="http://git-tower.com/?utm_source=husky&utm_medium=referral"><img height="100px" alt="tower-dock-icon-light" src="https://github.com/user-attachments/assets/b6b4ab20-beff-4e5c-9845-bb9d60057196" /></a>
|
|
89
91
|
|
|
90
92
|
### Silver
|
|
91
93
|
|
|
@@ -108,108 +110,110 @@ Run `json-server --help` for a list of options
|
|
|
108
110
|
>
|
|
109
111
|
> For more information, FAQs, and the rationale behind this, visit [https://fair.io/](https://fair.io/).
|
|
110
112
|
|
|
113
|
+
## Query capabilities overview
|
|
114
|
+
|
|
115
|
+
```http
|
|
116
|
+
GET /posts?views:gt=100
|
|
117
|
+
GET /posts?_sort=-views
|
|
118
|
+
GET /posts?_page=1&_per_page=10
|
|
119
|
+
GET /posts?_embed=comments
|
|
120
|
+
GET /posts?_where={"or":[{"views":{"gt":100}},{"title":{"eq":"Hello"}}]}
|
|
121
|
+
```
|
|
122
|
+
|
|
111
123
|
## Routes
|
|
112
124
|
|
|
113
|
-
|
|
125
|
+
For array resources (`posts`, `comments`):
|
|
114
126
|
|
|
115
|
-
```
|
|
127
|
+
```text
|
|
116
128
|
GET /posts
|
|
117
129
|
GET /posts/:id
|
|
118
130
|
POST /posts
|
|
119
131
|
PUT /posts/:id
|
|
120
132
|
PATCH /posts/:id
|
|
121
133
|
DELETE /posts/:id
|
|
122
|
-
|
|
123
|
-
# Same for comments
|
|
124
134
|
```
|
|
125
135
|
|
|
126
|
-
|
|
136
|
+
For object resources (`profile`):
|
|
137
|
+
|
|
138
|
+
```text
|
|
127
139
|
GET /profile
|
|
128
140
|
PUT /profile
|
|
129
141
|
PATCH /profile
|
|
130
142
|
```
|
|
131
143
|
|
|
132
|
-
##
|
|
144
|
+
## Query params
|
|
133
145
|
|
|
134
146
|
### Conditions
|
|
135
147
|
|
|
136
|
-
|
|
137
|
-
- `lt` → `<`
|
|
138
|
-
- `lte` → `<=`
|
|
139
|
-
- `gt` → `>`
|
|
140
|
-
- `gte` → `>=`
|
|
141
|
-
- `ne` → `!=`
|
|
148
|
+
Use `field:operator=value`.
|
|
142
149
|
|
|
143
|
-
|
|
144
|
-
GET /posts?views_gt=9000
|
|
145
|
-
```
|
|
150
|
+
Operators:
|
|
146
151
|
|
|
147
|
-
|
|
152
|
+
- no operator -> `eq` (equal)
|
|
153
|
+
- `lt` less than, `lte` less than or equal
|
|
154
|
+
- `gt` greater than, `gte` greater than or equal
|
|
155
|
+
- `eq` equal, `ne` not equal
|
|
148
156
|
|
|
149
|
-
|
|
150
|
-
- `end`
|
|
151
|
-
- `limit`
|
|
157
|
+
Examples:
|
|
152
158
|
|
|
153
|
-
```
|
|
154
|
-
GET /posts?
|
|
155
|
-
GET /posts?
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
### Paginate
|
|
159
|
-
|
|
160
|
-
- `page`
|
|
161
|
-
- `per_page` (default = 10)
|
|
162
|
-
|
|
163
|
-
```
|
|
164
|
-
GET /posts?_page=1&_per_page=25
|
|
159
|
+
```http
|
|
160
|
+
GET /posts?views:gt=100
|
|
161
|
+
GET /posts?title:eq=Hello
|
|
162
|
+
GET /posts?author.name:eq=typicode
|
|
165
163
|
```
|
|
166
164
|
|
|
167
165
|
### Sort
|
|
168
166
|
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
```http
|
|
168
|
+
GET /posts?_sort=title
|
|
169
|
+
GET /posts?_sort=-views
|
|
170
|
+
GET /posts?_sort=author.name,-views
|
|
171
171
|
```
|
|
172
|
-
GET /posts?_sort=id,-views
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### Nested and array fields
|
|
176
172
|
|
|
177
|
-
|
|
178
|
-
- `x.y.z[i]...`
|
|
173
|
+
### Pagination
|
|
179
174
|
|
|
175
|
+
```http
|
|
176
|
+
GET /posts?_page=1&_per_page=25
|
|
180
177
|
```
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
```
|
|
178
|
+
|
|
179
|
+
- `_per_page` default is `10`
|
|
180
|
+
- invalid page/per_page values are normalized
|
|
185
181
|
|
|
186
182
|
### Embed
|
|
187
183
|
|
|
188
|
-
```
|
|
184
|
+
```http
|
|
189
185
|
GET /posts?_embed=comments
|
|
190
186
|
GET /comments?_embed=post
|
|
191
187
|
```
|
|
192
188
|
|
|
193
|
-
|
|
189
|
+
### Complex filter with `_where`
|
|
194
190
|
|
|
191
|
+
`_where` accepts a JSON object and overrides normal query params when valid.
|
|
192
|
+
|
|
193
|
+
```http
|
|
194
|
+
GET /posts?_where={"or":[{"views":{"gt":100}},{"author":{"name":{"lt":"m"}}}]}
|
|
195
195
|
```
|
|
196
|
-
|
|
196
|
+
|
|
197
|
+
## Delete dependents
|
|
198
|
+
|
|
199
|
+
```http
|
|
197
200
|
DELETE /posts/1?_dependent=comments
|
|
198
201
|
```
|
|
199
202
|
|
|
200
|
-
##
|
|
203
|
+
## Static files
|
|
201
204
|
|
|
202
|
-
|
|
205
|
+
JSON Server serves `./public` automatically.
|
|
203
206
|
|
|
204
|
-
|
|
207
|
+
Add more static dirs:
|
|
205
208
|
|
|
206
209
|
```sh
|
|
207
210
|
json-server -s ./static
|
|
208
211
|
json-server -s ./static -s ./node_modules
|
|
209
212
|
```
|
|
210
213
|
|
|
211
|
-
##
|
|
214
|
+
## Behavior notes
|
|
212
215
|
|
|
213
216
|
- `id` is always a string and will be generated for you if missing
|
|
214
217
|
- use `_per_page` with `_page` instead of `_limit`for pagination
|
|
218
|
+
- use `_embed` instead of `_expand`
|
|
215
219
|
- use Chrome's `Network tab > throtling` to delay requests instead of `--delay` CLI option
|
package/lib/app.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Eta } from 'eta';
|
|
|
6
6
|
import { Low } from 'lowdb';
|
|
7
7
|
import { json } from 'milliparsec';
|
|
8
8
|
import sirv from 'sirv';
|
|
9
|
+
import { parseWhere } from "./parse-where.js";
|
|
9
10
|
import { isItem, Service } from "./service.js";
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const isProduction = process.env['NODE_ENV'] === 'production';
|
|
@@ -13,6 +14,59 @@ const eta = new Eta({
|
|
|
13
14
|
views: join(__dirname, '../views'),
|
|
14
15
|
cache: isProduction,
|
|
15
16
|
});
|
|
17
|
+
const RESERVED_QUERY_KEYS = new Set(['_sort', '_page', '_per_page', '_embed', '_where']);
|
|
18
|
+
function parseListParams(req) {
|
|
19
|
+
const queryString = req.url.split('?')[1] ?? '';
|
|
20
|
+
const params = new URLSearchParams(queryString);
|
|
21
|
+
const filterParams = new URLSearchParams();
|
|
22
|
+
for (const [key, value] of params.entries()) {
|
|
23
|
+
if (!RESERVED_QUERY_KEYS.has(key)) {
|
|
24
|
+
filterParams.append(key, value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
let where = parseWhere(filterParams.toString());
|
|
28
|
+
const rawWhere = params.get('_where');
|
|
29
|
+
if (typeof rawWhere === 'string') {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(rawWhere);
|
|
32
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
33
|
+
where = parsed;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Ignore invalid JSON and fallback to parsed query params
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const pageRaw = params.get('_page');
|
|
41
|
+
const perPageRaw = params.get('_per_page');
|
|
42
|
+
const page = pageRaw === null ? undefined : Number.parseInt(pageRaw, 10);
|
|
43
|
+
const perPage = perPageRaw === null ? undefined : Number.parseInt(perPageRaw, 10);
|
|
44
|
+
return {
|
|
45
|
+
where,
|
|
46
|
+
sort: params.get('_sort') ?? undefined,
|
|
47
|
+
page: Number.isNaN(page) ? undefined : page,
|
|
48
|
+
perPage: Number.isNaN(perPage) ? undefined : perPage,
|
|
49
|
+
embed: req.query['_embed'],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function withBody(action) {
|
|
53
|
+
return async (req, res, next) => {
|
|
54
|
+
const { name = '' } = req.params;
|
|
55
|
+
if (isItem(req.body)) {
|
|
56
|
+
res.locals['data'] = await action(name, req.body);
|
|
57
|
+
}
|
|
58
|
+
next?.();
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function withIdAndBody(action) {
|
|
62
|
+
return async (req, res, next) => {
|
|
63
|
+
const { name = '', id = '' } = req.params;
|
|
64
|
+
if (isItem(req.body)) {
|
|
65
|
+
res.locals['data'] = await action(name, id, req.body);
|
|
66
|
+
}
|
|
67
|
+
next?.();
|
|
68
|
+
};
|
|
69
|
+
}
|
|
16
70
|
export function createApp(db, options = {}) {
|
|
17
71
|
// Create service
|
|
18
72
|
const service = new Service(db);
|
|
@@ -38,18 +92,14 @@ export function createApp(db, options = {}) {
|
|
|
38
92
|
app.get('/', (_req, res) => res.send(eta.render('index.html', { data: db.data })));
|
|
39
93
|
app.get('/:name', (req, res, next) => {
|
|
40
94
|
const { name = '' } = req.params;
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (!Number.isNaN(value)) {
|
|
49
|
-
query[key] = value;
|
|
50
|
-
}
|
|
95
|
+
const { where, sort, page, perPage, embed } = parseListParams(req);
|
|
96
|
+
res.locals['data'] = service.find(name, {
|
|
97
|
+
where,
|
|
98
|
+
sort,
|
|
99
|
+
page,
|
|
100
|
+
perPage,
|
|
101
|
+
embed,
|
|
51
102
|
});
|
|
52
|
-
res.locals['data'] = service.find(name, query);
|
|
53
103
|
next?.();
|
|
54
104
|
});
|
|
55
105
|
app.get('/:name/:id', (req, res, next) => {
|
|
@@ -57,41 +107,11 @@ export function createApp(db, options = {}) {
|
|
|
57
107
|
res.locals['data'] = service.findById(name, id, req.query);
|
|
58
108
|
next?.();
|
|
59
109
|
});
|
|
60
|
-
app.post('/:name',
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
next?.();
|
|
66
|
-
});
|
|
67
|
-
app.put('/:name', async (req, res, next) => {
|
|
68
|
-
const { name = '' } = req.params;
|
|
69
|
-
if (isItem(req.body)) {
|
|
70
|
-
res.locals['data'] = await service.update(name, req.body);
|
|
71
|
-
}
|
|
72
|
-
next?.();
|
|
73
|
-
});
|
|
74
|
-
app.put('/:name/:id', async (req, res, next) => {
|
|
75
|
-
const { name = '', id = '' } = req.params;
|
|
76
|
-
if (isItem(req.body)) {
|
|
77
|
-
res.locals['data'] = await service.updateById(name, id, req.body);
|
|
78
|
-
}
|
|
79
|
-
next?.();
|
|
80
|
-
});
|
|
81
|
-
app.patch('/:name', async (req, res, next) => {
|
|
82
|
-
const { name = '' } = req.params;
|
|
83
|
-
if (isItem(req.body)) {
|
|
84
|
-
res.locals['data'] = await service.patch(name, req.body);
|
|
85
|
-
}
|
|
86
|
-
next?.();
|
|
87
|
-
});
|
|
88
|
-
app.patch('/:name/:id', async (req, res, next) => {
|
|
89
|
-
const { name = '', id = '' } = req.params;
|
|
90
|
-
if (isItem(req.body)) {
|
|
91
|
-
res.locals['data'] = await service.patchById(name, id, req.body);
|
|
92
|
-
}
|
|
93
|
-
next?.();
|
|
94
|
-
});
|
|
110
|
+
app.post('/:name', withBody(service.create.bind(service)));
|
|
111
|
+
app.put('/:name', withBody(service.update.bind(service)));
|
|
112
|
+
app.put('/:name/:id', withIdAndBody(service.updateById.bind(service)));
|
|
113
|
+
app.patch('/:name', withBody(service.patch.bind(service)));
|
|
114
|
+
app.patch('/:name/:id', withIdAndBody(service.patchById.bind(service)));
|
|
95
115
|
app.delete('/:name/:id', async (req, res, next) => {
|
|
96
116
|
const { name = '', id = '' } = req.params;
|
|
97
117
|
res.locals['data'] = await service.destroyById(name, id, req.query['_dependent']);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { WHERE_OPERATORS } from "./where-operators.js";
|
|
2
|
+
function isJSONObject(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function getKnownOperators(value) {
|
|
6
|
+
if (!isJSONObject(value))
|
|
7
|
+
return [];
|
|
8
|
+
const ops = [];
|
|
9
|
+
for (const op of WHERE_OPERATORS) {
|
|
10
|
+
if (op in value) {
|
|
11
|
+
ops.push(op);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return ops;
|
|
15
|
+
}
|
|
16
|
+
export function matchesWhere(obj, where) {
|
|
17
|
+
for (const [key, value] of Object.entries(where)) {
|
|
18
|
+
if (key === 'or') {
|
|
19
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
20
|
+
return false;
|
|
21
|
+
let matched = false;
|
|
22
|
+
for (const subWhere of value) {
|
|
23
|
+
if (isJSONObject(subWhere) && matchesWhere(obj, subWhere)) {
|
|
24
|
+
matched = true;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!matched)
|
|
29
|
+
return false;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const field = obj[key];
|
|
33
|
+
if (isJSONObject(value)) {
|
|
34
|
+
const knownOps = getKnownOperators(value);
|
|
35
|
+
if (knownOps.length > 0) {
|
|
36
|
+
if (field === undefined)
|
|
37
|
+
return false;
|
|
38
|
+
const op = value;
|
|
39
|
+
if (knownOps.includes('lt') && !(field < op.lt))
|
|
40
|
+
return false;
|
|
41
|
+
if (knownOps.includes('lte') && !(field <= op.lte))
|
|
42
|
+
return false;
|
|
43
|
+
if (knownOps.includes('gt') && !(field > op.gt))
|
|
44
|
+
return false;
|
|
45
|
+
if (knownOps.includes('gte') && !(field >= op.gte))
|
|
46
|
+
return false;
|
|
47
|
+
if (knownOps.includes('eq') && !(field === op.eq))
|
|
48
|
+
return false;
|
|
49
|
+
if (knownOps.includes('ne') && !(field !== op.ne))
|
|
50
|
+
return false;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (isJSONObject(field)) {
|
|
54
|
+
if (!matchesWhere(field, value))
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (field === undefined)
|
|
60
|
+
return false;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
package/lib/paginate.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function paginate(items, page, perPage) {
|
|
2
|
+
const totalItems = items.length;
|
|
3
|
+
const safePerPage = Number.isFinite(perPage) && perPage > 0 ? Math.floor(perPage) : 1;
|
|
4
|
+
const pages = Math.max(1, Math.ceil(totalItems / safePerPage));
|
|
5
|
+
// Ensure page is within the valid range
|
|
6
|
+
const safePage = Number.isFinite(page) ? Math.floor(page) : 1;
|
|
7
|
+
const currentPage = Math.max(1, Math.min(safePage, pages));
|
|
8
|
+
const first = 1;
|
|
9
|
+
const prev = currentPage > 1 ? currentPage - 1 : null;
|
|
10
|
+
const next = currentPage < pages ? currentPage + 1 : null;
|
|
11
|
+
const last = pages;
|
|
12
|
+
const start = (currentPage - 1) * safePerPage;
|
|
13
|
+
const end = start + safePerPage;
|
|
14
|
+
const data = items.slice(start, end);
|
|
15
|
+
return {
|
|
16
|
+
first,
|
|
17
|
+
prev,
|
|
18
|
+
next,
|
|
19
|
+
last,
|
|
20
|
+
pages,
|
|
21
|
+
items: totalItems,
|
|
22
|
+
data,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { setProperty } from 'dot-prop';
|
|
2
|
+
import { isWhereOperator } from "./where-operators.js";
|
|
3
|
+
function splitKey(key) {
|
|
4
|
+
const colonIdx = key.lastIndexOf(':');
|
|
5
|
+
if (colonIdx !== -1) {
|
|
6
|
+
const path = key.slice(0, colonIdx);
|
|
7
|
+
const op = key.slice(colonIdx + 1);
|
|
8
|
+
if (!op) {
|
|
9
|
+
return { path: key, op: 'eq' };
|
|
10
|
+
}
|
|
11
|
+
return isWhereOperator(op) ? { path, op } : { path, op: null };
|
|
12
|
+
}
|
|
13
|
+
// Compatibility with v0.17 operator style (e.g. _lt, _gt)
|
|
14
|
+
const underscoreMatch = key.match(/^(.*)_([a-z]+)$/);
|
|
15
|
+
if (underscoreMatch) {
|
|
16
|
+
const path = underscoreMatch[1];
|
|
17
|
+
const op = underscoreMatch[2];
|
|
18
|
+
if (path && isWhereOperator(op)) {
|
|
19
|
+
return { path, op };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { path: key, op: 'eq' };
|
|
23
|
+
}
|
|
24
|
+
function setPathOp(root, path, op, value) {
|
|
25
|
+
const fullPath = `${path}.${op}`;
|
|
26
|
+
setProperty(root, fullPath, coerceValue(value));
|
|
27
|
+
}
|
|
28
|
+
function coerceValue(value) {
|
|
29
|
+
if (value === 'true')
|
|
30
|
+
return true;
|
|
31
|
+
if (value === 'false')
|
|
32
|
+
return false;
|
|
33
|
+
if (value === 'null')
|
|
34
|
+
return null;
|
|
35
|
+
if (value.trim() === '')
|
|
36
|
+
return value;
|
|
37
|
+
const num = Number(value);
|
|
38
|
+
if (Number.isFinite(num))
|
|
39
|
+
return num;
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
export function parseWhere(query) {
|
|
43
|
+
const out = {};
|
|
44
|
+
const params = new URLSearchParams(query);
|
|
45
|
+
for (const [rawKey, rawValue] of params.entries()) {
|
|
46
|
+
const { path, op } = splitKey(rawKey);
|
|
47
|
+
if (op === null)
|
|
48
|
+
continue;
|
|
49
|
+
setPathOp(out, path, op, rawValue);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
package/lib/service.js
CHANGED
|
@@ -1,28 +1,18 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { getProperty } from 'dot-prop';
|
|
3
2
|
import inflection from 'inflection';
|
|
4
3
|
import { Low } from 'lowdb';
|
|
5
4
|
import sortOn from 'sort-on';
|
|
5
|
+
import { matchesWhere } from "./matches-where.js";
|
|
6
|
+
import { paginate } from "./paginate.js";
|
|
6
7
|
export function isItem(obj) {
|
|
7
|
-
return typeof obj === 'object' && obj !== null;
|
|
8
|
+
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
|
|
8
9
|
}
|
|
9
10
|
export function isData(obj) {
|
|
10
11
|
if (typeof obj !== 'object' || obj === null) {
|
|
11
12
|
return false;
|
|
12
13
|
}
|
|
13
14
|
const data = obj;
|
|
14
|
-
return Object.values(data).every((value) => Array.isArray(value)
|
|
15
|
-
}
|
|
16
|
-
const Condition = {
|
|
17
|
-
lt: 'lt',
|
|
18
|
-
lte: 'lte',
|
|
19
|
-
gt: 'gt',
|
|
20
|
-
gte: 'gte',
|
|
21
|
-
ne: 'ne',
|
|
22
|
-
default: '',
|
|
23
|
-
};
|
|
24
|
-
function isCondition(value) {
|
|
25
|
-
return Object.values(Condition).includes(value);
|
|
15
|
+
return Object.values(data).every((value) => Array.isArray(value) ? value.every(isItem) : isItem(value));
|
|
26
16
|
}
|
|
27
17
|
function ensureArray(arg = []) {
|
|
28
18
|
return Array.isArray(arg) ? arg : [arg];
|
|
@@ -120,148 +110,24 @@ export class Service {
|
|
|
120
110
|
}
|
|
121
111
|
return;
|
|
122
112
|
}
|
|
123
|
-
find(name,
|
|
124
|
-
|
|
113
|
+
find(name, opts) {
|
|
114
|
+
const items = this.#get(name);
|
|
125
115
|
if (!Array.isArray(items)) {
|
|
126
116
|
return items;
|
|
127
117
|
}
|
|
118
|
+
let results = items;
|
|
128
119
|
// Include
|
|
129
|
-
ensureArray(
|
|
130
|
-
|
|
131
|
-
items = items.map((item) => embed(this.#db, name, item, related));
|
|
132
|
-
}
|
|
120
|
+
ensureArray(opts.embed).forEach((related) => {
|
|
121
|
+
results = results.map((item) => embed(this.#db, name, item, related));
|
|
133
122
|
});
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
// Convert query params to conditions
|
|
139
|
-
const conds = [];
|
|
140
|
-
for (const [key, value] of Object.entries(query)) {
|
|
141
|
-
if (value === undefined || typeof value !== 'string') {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
const re = /_(lt|lte|gt|gte|ne)$/;
|
|
145
|
-
const reArr = re.exec(key);
|
|
146
|
-
const op = reArr?.at(1);
|
|
147
|
-
if (op && isCondition(op)) {
|
|
148
|
-
const field = key.replace(re, '');
|
|
149
|
-
conds.push([field, op, value]);
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
if (['_embed', '_sort', '_start', '_end', '_limit', '_page', '_per_page'].includes(key)) {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
conds.push([key, Condition.default, value]);
|
|
156
|
-
}
|
|
157
|
-
// Loop through conditions and filter items
|
|
158
|
-
let filtered = items;
|
|
159
|
-
for (const [key, op, paramValue] of conds) {
|
|
160
|
-
filtered = filtered.filter((item) => {
|
|
161
|
-
if (paramValue && !Array.isArray(paramValue)) {
|
|
162
|
-
// https://github.com/sindresorhus/dot-prop/issues/95
|
|
163
|
-
const itemValue = getProperty(item, key);
|
|
164
|
-
switch (op) {
|
|
165
|
-
// item_gt=value
|
|
166
|
-
case Condition.gt: {
|
|
167
|
-
if (!(typeof itemValue === 'number' && itemValue > parseInt(paramValue))) {
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
// item_gte=value
|
|
173
|
-
case Condition.gte: {
|
|
174
|
-
if (!(typeof itemValue === 'number' && itemValue >= parseInt(paramValue))) {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
// item_lt=value
|
|
180
|
-
case Condition.lt: {
|
|
181
|
-
if (!(typeof itemValue === 'number' && itemValue < parseInt(paramValue))) {
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
186
|
-
// item_lte=value
|
|
187
|
-
case Condition.lte: {
|
|
188
|
-
if (!(typeof itemValue === 'number' && itemValue <= parseInt(paramValue))) {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
// item_ne=value
|
|
194
|
-
case Condition.ne: {
|
|
195
|
-
switch (typeof itemValue) {
|
|
196
|
-
case 'number':
|
|
197
|
-
return itemValue !== parseInt(paramValue);
|
|
198
|
-
case 'string':
|
|
199
|
-
return itemValue !== paramValue;
|
|
200
|
-
case 'boolean':
|
|
201
|
-
return itemValue !== (paramValue === 'true');
|
|
202
|
-
}
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
// item=value
|
|
206
|
-
case Condition.default: {
|
|
207
|
-
switch (typeof itemValue) {
|
|
208
|
-
case 'number':
|
|
209
|
-
return itemValue === parseInt(paramValue);
|
|
210
|
-
case 'string':
|
|
211
|
-
return itemValue === paramValue;
|
|
212
|
-
case 'boolean':
|
|
213
|
-
return itemValue === (paramValue === 'true');
|
|
214
|
-
case 'undefined':
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return true;
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
// Sort
|
|
224
|
-
const sort = query._sort || '';
|
|
225
|
-
const sorted = sortOn(filtered, sort.split(','));
|
|
226
|
-
// Slice
|
|
227
|
-
const start = query._start;
|
|
228
|
-
const end = query._end;
|
|
229
|
-
const limit = query._limit;
|
|
230
|
-
if (start !== undefined) {
|
|
231
|
-
if (end !== undefined) {
|
|
232
|
-
return sorted.slice(start, end);
|
|
233
|
-
}
|
|
234
|
-
return sorted.slice(start, start + (limit || 0));
|
|
235
|
-
}
|
|
236
|
-
if (limit !== undefined) {
|
|
237
|
-
return sorted.slice(0, limit);
|
|
123
|
+
results = results.filter((item) => matchesWhere(item, opts.where));
|
|
124
|
+
if (opts.sort) {
|
|
125
|
+
results = sortOn(results, opts.sort.split(','));
|
|
238
126
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const perPage = query._per_page || 10;
|
|
242
|
-
if (page) {
|
|
243
|
-
const items = sorted.length;
|
|
244
|
-
const pages = Math.ceil(items / perPage);
|
|
245
|
-
// Ensure page is within the valid range
|
|
246
|
-
page = Math.max(1, Math.min(page, pages));
|
|
247
|
-
const first = 1;
|
|
248
|
-
const prev = page > 1 ? page - 1 : null;
|
|
249
|
-
const next = page < pages ? page + 1 : null;
|
|
250
|
-
const last = pages;
|
|
251
|
-
const start = (page - 1) * perPage;
|
|
252
|
-
const end = start + perPage;
|
|
253
|
-
const data = sorted.slice(start, end);
|
|
254
|
-
return {
|
|
255
|
-
first,
|
|
256
|
-
prev,
|
|
257
|
-
next,
|
|
258
|
-
last,
|
|
259
|
-
pages,
|
|
260
|
-
items,
|
|
261
|
-
data,
|
|
262
|
-
};
|
|
127
|
+
if (opts.page !== undefined) {
|
|
128
|
+
return paginate(results, opts.page, opts.perPage ?? 10);
|
|
263
129
|
}
|
|
264
|
-
return
|
|
130
|
+
return results;
|
|
265
131
|
}
|
|
266
132
|
async create(name, data = {}) {
|
|
267
133
|
const items = this.#get(name);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "json-server",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.6",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"license": "SEE LICENSE IN ./LICENSE",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"lib",
|
|
17
|
-
"views"
|
|
17
|
+
"views",
|
|
18
|
+
"schema.json"
|
|
18
19
|
],
|
|
19
20
|
"type": "module",
|
|
20
21
|
"dependencies": {
|