json-server 1.0.0-beta.10 → 1.0.0-beta.13
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/lib/adapters/normalized-adapter.js +31 -0
- package/lib/app.js +8 -4
- package/lib/bin.js +3 -2
- package/lib/random-id.js +4 -0
- package/lib/service.js +1 -30
- package/package.json +1 -1
- package/views/index.html +119 -67
- /package/lib/{observer.js → adapters/observer.js} +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { randomId } from "../random-id.js";
|
|
2
|
+
export const DEFAULT_SCHEMA_PATH = './node_modules/json-server/schema.json';
|
|
3
|
+
export class NormalizedAdapter {
|
|
4
|
+
#adapter;
|
|
5
|
+
constructor(adapter) {
|
|
6
|
+
this.#adapter = adapter;
|
|
7
|
+
}
|
|
8
|
+
async read() {
|
|
9
|
+
const data = await this.#adapter.read();
|
|
10
|
+
if (data === null) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
delete data['$schema'];
|
|
14
|
+
for (const value of Object.values(data)) {
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
for (const item of value) {
|
|
17
|
+
if (typeof item['id'] === 'number') {
|
|
18
|
+
item['id'] = item['id'].toString();
|
|
19
|
+
}
|
|
20
|
+
if (item['id'] === undefined) {
|
|
21
|
+
item['id'] = randomId();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
async write(data) {
|
|
29
|
+
await this.#adapter.write({ ...data, $schema: DEFAULT_SCHEMA_PATH });
|
|
30
|
+
}
|
|
31
|
+
}
|
package/lib/app.js
CHANGED
|
@@ -52,18 +52,22 @@ function parseListParams(req) {
|
|
|
52
52
|
function withBody(action) {
|
|
53
53
|
return async (req, res, next) => {
|
|
54
54
|
const { name = '' } = req.params;
|
|
55
|
-
if (isItem(req.body)) {
|
|
56
|
-
res.
|
|
55
|
+
if (!isItem(req.body)) {
|
|
56
|
+
res.status(400).json({ error: 'Body must be a JSON object' });
|
|
57
|
+
return;
|
|
57
58
|
}
|
|
59
|
+
res.locals['data'] = await action(name, req.body);
|
|
58
60
|
next?.();
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
63
|
function withIdAndBody(action) {
|
|
62
64
|
return async (req, res, next) => {
|
|
63
65
|
const { name = '', id = '' } = req.params;
|
|
64
|
-
if (isItem(req.body)) {
|
|
65
|
-
res.
|
|
66
|
+
if (!isItem(req.body)) {
|
|
67
|
+
res.status(400).json({ error: 'Body must be a JSON object' });
|
|
68
|
+
return;
|
|
66
69
|
}
|
|
70
|
+
res.locals['data'] = await action(name, id, req.body);
|
|
67
71
|
next?.();
|
|
68
72
|
};
|
|
69
73
|
}
|
package/lib/bin.js
CHANGED
|
@@ -8,8 +8,9 @@ import JSON5 from "json5";
|
|
|
8
8
|
import { Low } from "lowdb";
|
|
9
9
|
import { DataFile, JSONFile } from "lowdb/node";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { NormalizedAdapter } from "./adapters/normalized-adapter.js";
|
|
12
|
+
import { Observer } from "./adapters/observer.js";
|
|
11
13
|
import { createApp } from "./app.js";
|
|
12
|
-
import { Observer } from "./observer.js";
|
|
13
14
|
function help() {
|
|
14
15
|
console.log(`Usage: json-server [options] <file>
|
|
15
16
|
|
|
@@ -109,7 +110,7 @@ if (extname(file) === ".json5") {
|
|
|
109
110
|
else {
|
|
110
111
|
adapter = new JSONFile(file);
|
|
111
112
|
}
|
|
112
|
-
const observer = new Observer(adapter);
|
|
113
|
+
const observer = new Observer(new NormalizedAdapter(adapter));
|
|
113
114
|
const db = new Low(observer, {});
|
|
114
115
|
await db.read();
|
|
115
116
|
// Create app
|
package/lib/random-id.js
ADDED
package/lib/service.js
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
2
1
|
import inflection from 'inflection';
|
|
3
2
|
import { Low } from 'lowdb';
|
|
4
3
|
import sortOn from 'sort-on';
|
|
5
4
|
import { matchesWhere } from "./matches-where.js";
|
|
6
5
|
import { paginate } from "./paginate.js";
|
|
6
|
+
import { randomId } from "./random-id.js";
|
|
7
7
|
export function isItem(obj) {
|
|
8
8
|
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
|
|
9
9
|
}
|
|
10
|
-
export function isData(obj) {
|
|
11
|
-
if (typeof obj !== 'object' || obj === null) {
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
const data = obj;
|
|
15
|
-
return Object.values(data).every((value) => Array.isArray(value) ? value.every(isItem) : isItem(value));
|
|
16
|
-
}
|
|
17
10
|
function ensureArray(arg = []) {
|
|
18
11
|
return Array.isArray(arg) ? arg : [arg];
|
|
19
12
|
}
|
|
@@ -65,31 +58,9 @@ function deleteDependents(db, name, dependents) {
|
|
|
65
58
|
}
|
|
66
59
|
});
|
|
67
60
|
}
|
|
68
|
-
function randomId() {
|
|
69
|
-
return randomBytes(2).toString('hex');
|
|
70
|
-
}
|
|
71
|
-
function fixItemsIds(items) {
|
|
72
|
-
items.forEach((item) => {
|
|
73
|
-
if (typeof item['id'] === 'number') {
|
|
74
|
-
item['id'] = item['id'].toString();
|
|
75
|
-
}
|
|
76
|
-
if (item['id'] === undefined) {
|
|
77
|
-
item['id'] = randomId();
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
// Ensure all items have an id
|
|
82
|
-
function fixAllItemsIds(data) {
|
|
83
|
-
Object.values(data).forEach((value) => {
|
|
84
|
-
if (Array.isArray(value)) {
|
|
85
|
-
fixItemsIds(value);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
61
|
export class Service {
|
|
90
62
|
#db;
|
|
91
63
|
constructor(db) {
|
|
92
|
-
fixAllItemsIds(db.data);
|
|
93
64
|
this.#db = db;
|
|
94
65
|
}
|
|
95
66
|
#get(name) {
|
package/package.json
CHANGED
package/views/index.html
CHANGED
|
@@ -1,95 +1,147 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html>
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>JSON Server</title>
|
|
6
7
|
<style>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: light dark;
|
|
10
|
+
--fg: light-dark(#111, #e8e8e8);
|
|
11
|
+
--bg: light-dark(#fff, #111214);
|
|
12
|
+
--muted: light-dark(#999, #9aa0a6);
|
|
13
|
+
--line: light-dark(#e5e5e5, #2c2f34);
|
|
14
|
+
--accent: light-dark(#6366f1, #8b90ff);
|
|
15
|
+
--chip-bg: light-dark(#111, #f5f5f5);
|
|
16
|
+
--chip-fg: light-dark(#fff, #111214);
|
|
12
17
|
}
|
|
13
|
-
|
|
14
18
|
body {
|
|
15
|
-
margin:
|
|
16
|
-
max-width:
|
|
17
|
-
padding: 0
|
|
18
|
-
font-family:
|
|
19
|
+
margin: 40px auto;
|
|
20
|
+
max-width: 400px;
|
|
21
|
+
padding: 0 24px;
|
|
22
|
+
font-family:
|
|
23
|
+
ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
24
|
+
background-color: var(--bg);
|
|
25
|
+
color: var(--fg);
|
|
26
|
+
line-height: 1.5;
|
|
27
|
+
font-size: 14px;
|
|
28
|
+
transition:
|
|
29
|
+
background-color 0.2s ease,
|
|
30
|
+
color 0.2s ease,
|
|
31
|
+
border-color 0.2s ease;
|
|
19
32
|
}
|
|
20
|
-
|
|
21
33
|
a {
|
|
22
|
-
color:
|
|
23
|
-
text-decoration: none;
|
|
34
|
+
color: inherit;
|
|
24
35
|
}
|
|
25
|
-
|
|
26
36
|
header {
|
|
27
|
-
margin-bottom:
|
|
28
|
-
|
|
37
|
+
margin-bottom: 40px;
|
|
38
|
+
color: var(--muted);
|
|
29
39
|
}
|
|
30
|
-
|
|
31
|
-
nav {
|
|
40
|
+
.topbar {
|
|
32
41
|
display: flex;
|
|
33
42
|
justify-content: space-between;
|
|
43
|
+
align-items: baseline;
|
|
44
|
+
margin-bottom: 12px;
|
|
45
|
+
padding-top: 12px;
|
|
46
|
+
border-top: 1px solid var(--line);
|
|
34
47
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
.topbar a {
|
|
49
|
+
text-decoration: none;
|
|
50
|
+
transition: color 0.1s ease;
|
|
38
51
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
margin: 0;
|
|
42
|
-
padding: 0;
|
|
43
|
-
list-style: none;
|
|
52
|
+
.topbar a:first-child {
|
|
53
|
+
color: var(--fg);
|
|
44
54
|
}
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
.topbar a:hover {
|
|
56
|
+
color: var(--accent);
|
|
57
|
+
}
|
|
58
|
+
.section-title {
|
|
47
59
|
margin-bottom: 8px;
|
|
60
|
+
color: var(--muted);
|
|
48
61
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
.list {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
gap: 4px;
|
|
66
|
+
}
|
|
67
|
+
.list a {
|
|
68
|
+
display: flex;
|
|
69
|
+
justify-content: space-between;
|
|
70
|
+
align-items: center;
|
|
71
|
+
padding: 8px 0;
|
|
72
|
+
text-decoration: none;
|
|
73
|
+
transition: color 0.1s ease;
|
|
74
|
+
}
|
|
75
|
+
.list a:hover {
|
|
76
|
+
color: var(--accent);
|
|
77
|
+
}
|
|
78
|
+
.meta {
|
|
79
|
+
color: var(--muted);
|
|
80
|
+
font-size: 13px;
|
|
81
|
+
}
|
|
82
|
+
.empty {
|
|
83
|
+
color: var(--muted);
|
|
84
|
+
}
|
|
85
|
+
footer {
|
|
86
|
+
margin-top: 48px;
|
|
87
|
+
padding-top: 12px;
|
|
88
|
+
border-top: 1px solid var(--line);
|
|
89
|
+
color: var(--muted);
|
|
90
|
+
font-size: 13px;
|
|
91
|
+
}
|
|
92
|
+
.heart {
|
|
93
|
+
position: fixed;
|
|
94
|
+
bottom: 20px;
|
|
95
|
+
right: 24px;
|
|
96
|
+
display: inline-flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
width: 32px;
|
|
100
|
+
height: 32px;
|
|
101
|
+
border-radius: 50%;
|
|
102
|
+
color: var(--chip-fg);
|
|
103
|
+
background-color: var(--chip-bg);
|
|
104
|
+
text-decoration: none;
|
|
105
|
+
transition: transform 0.15s ease;
|
|
106
|
+
}
|
|
107
|
+
.heart:hover {
|
|
108
|
+
transform: scale(1.1);
|
|
59
109
|
}
|
|
60
110
|
</style>
|
|
61
111
|
</head>
|
|
62
112
|
|
|
63
113
|
<body>
|
|
114
|
+
<% const resources = Object.entries(it.data ?? {}); %>
|
|
115
|
+
|
|
64
116
|
<header>
|
|
65
|
-
<
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
</div>
|
|
71
|
-
</nav>
|
|
117
|
+
<div class="topbar">
|
|
118
|
+
<a href="https://github.com/typicode/json-server" target="_blank">json-server</a>
|
|
119
|
+
<a href="https://github.com/typicode/json-server" target="_blank">README</a>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="intro">Available REST resources from db.json.</div>
|
|
72
122
|
</header>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
<%
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
123
|
+
|
|
124
|
+
<div class="section-title">Resources</div>
|
|
125
|
+
|
|
126
|
+
<div class="list">
|
|
127
|
+
<% if (resources.length === 0) { %>
|
|
128
|
+
<span class="empty">No resources in db.json.</span>
|
|
129
|
+
<% } else { %> <% resources.forEach(function([name, value]) { const isCollection =
|
|
130
|
+
Array.isArray(value); %>
|
|
131
|
+
<a href="/<%= name %>">
|
|
132
|
+
<span>/<%= name %></span>
|
|
133
|
+
<span class="meta">
|
|
134
|
+
<% if (isCollection) { %> <%= value.length %> items <% } else { %> object <% } %>
|
|
135
|
+
</span>
|
|
136
|
+
</a>
|
|
137
|
+
<% }) %> <% } %>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<footer>
|
|
141
|
+
<span>To replace this page, create public/index.html.</span>
|
|
142
|
+
</footer>
|
|
143
|
+
|
|
144
|
+
<a href="https://github.com/sponsors/typicode" target="_blank" class="heart">❤</a>
|
|
145
|
+
|
|
94
146
|
</body>
|
|
95
147
|
</html>
|
|
File without changes
|