json-server 1.0.0-beta.1 → 1.0.0-beta.12
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 +21 -44
- package/README.md +140 -88
- package/lib/adapters/normalized-adapter.js +31 -0
- package/lib/app.js +81 -51
- package/lib/bin.js +54 -50
- package/lib/matches-where.js +87 -0
- package/lib/paginate.js +24 -0
- package/lib/parse-where.js +56 -0
- package/lib/random-id.js +4 -0
- package/lib/service.js +18 -190
- package/lib/where-operators.js +15 -0
- package/package.json +58 -44
- package/schema.json +10 -0
- package/views/index.html +73 -75
- package/lib/app.d.ts +0 -8
- package/lib/bin.d.ts +0 -2
- package/lib/observer.d.ts +0 -11
- package/lib/service.d.ts +0 -38
- /package/lib/{observer.js → adapters/observer.js} +0 -0
package/lib/bin.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from
|
|
3
|
-
import { extname } from
|
|
4
|
-
import { parseArgs } from
|
|
5
|
-
import chalk from
|
|
6
|
-
import { watch } from
|
|
7
|
-
import JSON5 from
|
|
8
|
-
import { Low } from
|
|
9
|
-
import { DataFile, JSONFile } from
|
|
10
|
-
import { fileURLToPath } from
|
|
11
|
-
import {
|
|
12
|
-
import { Observer } from
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { extname } from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { watch } from "chokidar";
|
|
7
|
+
import JSON5 from "json5";
|
|
8
|
+
import { Low } from "lowdb";
|
|
9
|
+
import { DataFile, JSONFile } from "lowdb/node";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { NormalizedAdapter } from "./adapters/normalized-adapter.js";
|
|
12
|
+
import { Observer } from "./adapters/observer.js";
|
|
13
|
+
import { createApp } from "./app.js";
|
|
13
14
|
function help() {
|
|
14
15
|
console.log(`Usage: json-server [options] <file>
|
|
15
16
|
|
|
@@ -27,44 +28,44 @@ function args() {
|
|
|
27
28
|
const { values, positionals } = parseArgs({
|
|
28
29
|
options: {
|
|
29
30
|
port: {
|
|
30
|
-
type:
|
|
31
|
-
short:
|
|
32
|
-
default: process.env[
|
|
31
|
+
type: "string",
|
|
32
|
+
short: "p",
|
|
33
|
+
default: process.env["PORT"] ?? "3000",
|
|
33
34
|
},
|
|
34
35
|
host: {
|
|
35
|
-
type:
|
|
36
|
-
short:
|
|
37
|
-
default: process.env[
|
|
36
|
+
type: "string",
|
|
37
|
+
short: "h",
|
|
38
|
+
default: process.env["HOST"] ?? "localhost",
|
|
38
39
|
},
|
|
39
40
|
static: {
|
|
40
|
-
type:
|
|
41
|
-
short:
|
|
41
|
+
type: "string",
|
|
42
|
+
short: "s",
|
|
42
43
|
multiple: true,
|
|
43
44
|
default: [],
|
|
44
45
|
},
|
|
45
46
|
help: {
|
|
46
|
-
type:
|
|
47
|
+
type: "boolean",
|
|
47
48
|
},
|
|
48
49
|
version: {
|
|
49
|
-
type:
|
|
50
|
+
type: "boolean",
|
|
50
51
|
},
|
|
51
52
|
// Deprecated
|
|
52
53
|
watch: {
|
|
53
|
-
type:
|
|
54
|
-
short:
|
|
54
|
+
type: "boolean",
|
|
55
|
+
short: "w",
|
|
55
56
|
},
|
|
56
57
|
},
|
|
57
58
|
allowPositionals: true,
|
|
58
59
|
});
|
|
59
60
|
// --version
|
|
60
61
|
if (values.version) {
|
|
61
|
-
const pkg = JSON.parse(readFileSync(fileURLToPath(new URL(
|
|
62
|
+
const pkg = JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf-8"));
|
|
62
63
|
console.log(pkg.version);
|
|
63
64
|
process.exit();
|
|
64
65
|
}
|
|
65
66
|
// Handle --watch
|
|
66
67
|
if (values.watch) {
|
|
67
|
-
console.log(chalk.yellow(
|
|
68
|
+
console.log(chalk.yellow("--watch/-w can be omitted, JSON Server 1+ watches for file changes by default"));
|
|
68
69
|
}
|
|
69
70
|
if (values.help || positionals.length === 0) {
|
|
70
71
|
help();
|
|
@@ -72,15 +73,15 @@ function args() {
|
|
|
72
73
|
}
|
|
73
74
|
// App args and options
|
|
74
75
|
return {
|
|
75
|
-
file: positionals[0] ??
|
|
76
|
+
file: positionals[0] ?? "",
|
|
76
77
|
port: parseInt(values.port),
|
|
77
78
|
host: values.host,
|
|
78
79
|
static: values.static,
|
|
79
80
|
};
|
|
80
81
|
}
|
|
81
82
|
catch (e) {
|
|
82
|
-
if (e.code ===
|
|
83
|
-
console.log(chalk.red(e.message.split(
|
|
83
|
+
if (e.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
|
|
84
|
+
console.log(chalk.red(e.message.split(".")[0]));
|
|
84
85
|
help();
|
|
85
86
|
process.exit(1);
|
|
86
87
|
}
|
|
@@ -95,12 +96,12 @@ if (!existsSync(file)) {
|
|
|
95
96
|
process.exit(1);
|
|
96
97
|
}
|
|
97
98
|
// Handle empty string JSON file
|
|
98
|
-
if (readFileSync(file,
|
|
99
|
-
writeFileSync(file,
|
|
99
|
+
if (readFileSync(file, "utf-8").trim() === "") {
|
|
100
|
+
writeFileSync(file, "{}");
|
|
100
101
|
}
|
|
101
102
|
// Set up database
|
|
102
103
|
let adapter;
|
|
103
|
-
if (extname(file) ===
|
|
104
|
+
if (extname(file) === ".json5") {
|
|
104
105
|
adapter = new DataFile(file, {
|
|
105
106
|
parse: JSON5.parse,
|
|
106
107
|
stringify: JSON5.stringify,
|
|
@@ -109,47 +110,48 @@ 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
|
|
116
117
|
const app = createApp(db, { logger: false, static: staticArr });
|
|
117
118
|
function logRoutes(data) {
|
|
118
|
-
console.log(chalk.bold(
|
|
119
|
+
console.log(chalk.bold("Endpoints:"));
|
|
119
120
|
if (Object.keys(data).length === 0) {
|
|
120
121
|
console.log(chalk.gray(`No endpoints found, try adding some data to ${file}`));
|
|
121
122
|
return;
|
|
122
123
|
}
|
|
123
124
|
console.log(Object.keys(data)
|
|
124
125
|
.map((key) => `${chalk.gray(`http://${host}:${port}/`)}${chalk.blue(key)}`)
|
|
125
|
-
.join(
|
|
126
|
+
.join("\n"));
|
|
126
127
|
}
|
|
127
|
-
const kaomojis = [
|
|
128
|
+
const kaomojis = ["♡⸜(˶˃ ᵕ ˂˶)⸝♡", "♡( ◡‿◡ )", "( ˶ˆ ᗜ ˆ˵ )", "(˶ᵔ ᵕ ᵔ˶)"];
|
|
128
129
|
function randomItem(items) {
|
|
129
130
|
const index = Math.floor(Math.random() * items.length);
|
|
130
|
-
return items.at(index) ??
|
|
131
|
+
return items.at(index) ?? "";
|
|
131
132
|
}
|
|
132
133
|
app.listen(port, () => {
|
|
133
134
|
console.log([
|
|
134
135
|
chalk.bold(`JSON Server started on PORT :${port}`),
|
|
135
|
-
chalk.gray(
|
|
136
|
+
chalk.gray("Press CTRL-C to stop"),
|
|
136
137
|
chalk.gray(`Watching ${file}...`),
|
|
137
|
-
|
|
138
|
+
"",
|
|
138
139
|
chalk.magenta(randomItem(kaomojis)),
|
|
139
|
-
|
|
140
|
-
chalk.bold(
|
|
140
|
+
"",
|
|
141
|
+
chalk.bold("Index:"),
|
|
141
142
|
chalk.gray(`http://localhost:${port}/`),
|
|
142
|
-
|
|
143
|
-
chalk.bold(
|
|
144
|
-
chalk.gray(
|
|
145
|
-
|
|
146
|
-
].join(
|
|
143
|
+
"",
|
|
144
|
+
chalk.bold("Static files:"),
|
|
145
|
+
chalk.gray("Serving ./public directory if it exists"),
|
|
146
|
+
"",
|
|
147
|
+
].join("\n"));
|
|
147
148
|
logRoutes(db.data);
|
|
148
149
|
});
|
|
149
150
|
// Watch file for changes
|
|
150
|
-
if (process.env[
|
|
151
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
151
152
|
let writing = false; // true if the file is being written to by the app
|
|
152
|
-
let
|
|
153
|
+
let hadReadError = false;
|
|
154
|
+
let prevEndpoints = "";
|
|
153
155
|
observer.onWriteStart = () => {
|
|
154
156
|
writing = true;
|
|
155
157
|
};
|
|
@@ -164,17 +166,19 @@ if (process.env['NODE_ENV'] !== 'production') {
|
|
|
164
166
|
return;
|
|
165
167
|
}
|
|
166
168
|
const nextEndpoints = JSON.stringify(Object.keys(data).sort());
|
|
167
|
-
if (prevEndpoints !== nextEndpoints) {
|
|
169
|
+
if (hadReadError || prevEndpoints !== nextEndpoints) {
|
|
168
170
|
console.log();
|
|
169
171
|
logRoutes(data);
|
|
170
172
|
}
|
|
173
|
+
hadReadError = false;
|
|
171
174
|
};
|
|
172
|
-
watch(file).on(
|
|
175
|
+
watch(file).on("change", () => {
|
|
173
176
|
// Do no reload if the file is being written to by the app
|
|
174
177
|
if (!writing) {
|
|
175
178
|
db.read().catch((e) => {
|
|
176
179
|
if (e instanceof SyntaxError) {
|
|
177
|
-
|
|
180
|
+
hadReadError = true;
|
|
181
|
+
return console.log(chalk.red(["", `Error parsing ${file}`, e.message].join("\n")));
|
|
178
182
|
}
|
|
179
183
|
console.log(e);
|
|
180
184
|
});
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
if (knownOps.includes('in')) {
|
|
52
|
+
const inValues = Array.isArray(op.in) ? op.in : [op.in];
|
|
53
|
+
if (!inValues.some((v) => field === v))
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (knownOps.includes('contains')) {
|
|
57
|
+
if (typeof field !== 'string')
|
|
58
|
+
return false;
|
|
59
|
+
if (!field.toLowerCase().includes(String(op.contains).toLowerCase()))
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (knownOps.includes('startsWith')) {
|
|
63
|
+
if (typeof field !== 'string')
|
|
64
|
+
return false;
|
|
65
|
+
if (!field.toLowerCase().startsWith(String(op.startsWith).toLowerCase()))
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (knownOps.includes('endsWith')) {
|
|
69
|
+
if (typeof field !== 'string')
|
|
70
|
+
return false;
|
|
71
|
+
if (!field.toLowerCase().endsWith(String(op.endsWith).toLowerCase()))
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (isJSONObject(field)) {
|
|
77
|
+
if (!matchesWhere(field, value))
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (field === undefined)
|
|
83
|
+
return false;
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
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,56 @@
|
|
|
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
|
+
if (op === 'in') {
|
|
27
|
+
setProperty(root, fullPath, value.split(',').map((part) => coerceValue(part.trim())));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
setProperty(root, fullPath, coerceValue(value));
|
|
31
|
+
}
|
|
32
|
+
function coerceValue(value) {
|
|
33
|
+
if (value === 'true')
|
|
34
|
+
return true;
|
|
35
|
+
if (value === 'false')
|
|
36
|
+
return false;
|
|
37
|
+
if (value === 'null')
|
|
38
|
+
return null;
|
|
39
|
+
if (value.trim() === '')
|
|
40
|
+
return value;
|
|
41
|
+
const num = Number(value);
|
|
42
|
+
if (Number.isFinite(num))
|
|
43
|
+
return num;
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
export function parseWhere(query) {
|
|
47
|
+
const out = {};
|
|
48
|
+
const params = new URLSearchParams(query);
|
|
49
|
+
for (const [rawKey, rawValue] of params.entries()) {
|
|
50
|
+
const { path, op } = splitKey(rawKey);
|
|
51
|
+
if (op === null)
|
|
52
|
+
continue;
|
|
53
|
+
setPathOp(out, path, op, rawValue);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
package/lib/random-id.js
ADDED
package/lib/service.js
CHANGED
|
@@ -1,28 +1,11 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { getProperty } from 'dot-prop';
|
|
3
1
|
import inflection from 'inflection';
|
|
2
|
+
import { Low } from 'lowdb';
|
|
4
3
|
import sortOn from 'sort-on';
|
|
4
|
+
import { matchesWhere } from "./matches-where.js";
|
|
5
|
+
import { paginate } from "./paginate.js";
|
|
6
|
+
import { randomId } from "./random-id.js";
|
|
5
7
|
export function isItem(obj) {
|
|
6
|
-
return typeof obj === 'object' && obj !== null;
|
|
7
|
-
}
|
|
8
|
-
export function isData(obj) {
|
|
9
|
-
if (typeof obj !== 'object' || obj === null) {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
const data = obj;
|
|
13
|
-
return Object.values(data).every((value) => Array.isArray(value) && value.every(isItem));
|
|
14
|
-
}
|
|
15
|
-
var Condition;
|
|
16
|
-
(function (Condition) {
|
|
17
|
-
Condition["lt"] = "lt";
|
|
18
|
-
Condition["lte"] = "lte";
|
|
19
|
-
Condition["gt"] = "gt";
|
|
20
|
-
Condition["gte"] = "gte";
|
|
21
|
-
Condition["ne"] = "ne";
|
|
22
|
-
Condition["default"] = "";
|
|
23
|
-
})(Condition || (Condition = {}));
|
|
24
|
-
function isCondition(value) {
|
|
25
|
-
return Object.values(Condition).includes(value);
|
|
8
|
+
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
|
|
26
9
|
}
|
|
27
10
|
function ensureArray(arg = []) {
|
|
28
11
|
return Array.isArray(arg) ? arg : [arg];
|
|
@@ -75,31 +58,9 @@ function deleteDependents(db, name, dependents) {
|
|
|
75
58
|
}
|
|
76
59
|
});
|
|
77
60
|
}
|
|
78
|
-
function randomId() {
|
|
79
|
-
return randomBytes(2).toString('hex');
|
|
80
|
-
}
|
|
81
|
-
function fixItemsIds(items) {
|
|
82
|
-
items.forEach((item) => {
|
|
83
|
-
if (typeof item['id'] === 'number') {
|
|
84
|
-
item['id'] = item['id'].toString();
|
|
85
|
-
}
|
|
86
|
-
if (item['id'] === undefined) {
|
|
87
|
-
item['id'] = randomId();
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
// Ensure all items have an id
|
|
92
|
-
function fixAllItemsIds(data) {
|
|
93
|
-
Object.values(data).forEach((value) => {
|
|
94
|
-
if (Array.isArray(value)) {
|
|
95
|
-
fixItemsIds(value);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
61
|
export class Service {
|
|
100
62
|
#db;
|
|
101
63
|
constructor(db) {
|
|
102
|
-
fixAllItemsIds(db.data);
|
|
103
64
|
this.#db = db;
|
|
104
65
|
}
|
|
105
66
|
#get(name) {
|
|
@@ -120,157 +81,24 @@ export class Service {
|
|
|
120
81
|
}
|
|
121
82
|
return;
|
|
122
83
|
}
|
|
123
|
-
find(name,
|
|
124
|
-
|
|
84
|
+
find(name, opts) {
|
|
85
|
+
const items = this.#get(name);
|
|
125
86
|
if (!Array.isArray(items)) {
|
|
126
87
|
return items;
|
|
127
88
|
}
|
|
89
|
+
let results = items;
|
|
128
90
|
// Include
|
|
129
|
-
ensureArray(
|
|
130
|
-
|
|
131
|
-
items = items.map((item) => embed(this.#db, name, item, related));
|
|
132
|
-
}
|
|
91
|
+
ensureArray(opts.embed).forEach((related) => {
|
|
92
|
+
results = results.map((item) => embed(this.#db, name, item, related));
|
|
133
93
|
});
|
|
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[field] = [op, value];
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
if ([
|
|
153
|
-
'_embed',
|
|
154
|
-
'_sort',
|
|
155
|
-
'_start',
|
|
156
|
-
'_end',
|
|
157
|
-
'_limit',
|
|
158
|
-
'_page',
|
|
159
|
-
'_per_page',
|
|
160
|
-
].includes(key)) {
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
conds[key] = [Condition.default, value];
|
|
164
|
-
}
|
|
165
|
-
// Loop through conditions and filter items
|
|
166
|
-
const res = items.filter((item) => {
|
|
167
|
-
for (const [key, [op, paramValue]] of Object.entries(conds)) {
|
|
168
|
-
if (paramValue && !Array.isArray(paramValue)) {
|
|
169
|
-
// https://github.com/sindresorhus/dot-prop/issues/95
|
|
170
|
-
const itemValue = getProperty(item, key);
|
|
171
|
-
switch (op) {
|
|
172
|
-
// item_gt=value
|
|
173
|
-
case Condition.gt: {
|
|
174
|
-
if (!(typeof itemValue === 'number' &&
|
|
175
|
-
itemValue > parseInt(paramValue))) {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
// item_gte=value
|
|
181
|
-
case Condition.gte: {
|
|
182
|
-
if (!(typeof itemValue === 'number' &&
|
|
183
|
-
itemValue >= parseInt(paramValue))) {
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
// item_lt=value
|
|
189
|
-
case Condition.lt: {
|
|
190
|
-
if (!(typeof itemValue === 'number' &&
|
|
191
|
-
itemValue < parseInt(paramValue))) {
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
// item_lte=value
|
|
197
|
-
case Condition.lte: {
|
|
198
|
-
if (!(typeof itemValue === 'number' &&
|
|
199
|
-
itemValue <= parseInt(paramValue))) {
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
// item_ne=value
|
|
205
|
-
case Condition.ne: {
|
|
206
|
-
switch (typeof itemValue) {
|
|
207
|
-
case 'number':
|
|
208
|
-
return itemValue !== parseInt(paramValue);
|
|
209
|
-
case 'string':
|
|
210
|
-
return itemValue !== paramValue;
|
|
211
|
-
case 'boolean':
|
|
212
|
-
return itemValue !== (paramValue === 'true');
|
|
213
|
-
}
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
// item=value
|
|
217
|
-
case Condition.default: {
|
|
218
|
-
switch (typeof itemValue) {
|
|
219
|
-
case 'number':
|
|
220
|
-
return itemValue === parseInt(paramValue);
|
|
221
|
-
case 'string':
|
|
222
|
-
return itemValue === paramValue;
|
|
223
|
-
case 'boolean':
|
|
224
|
-
return itemValue === (paramValue === 'true');
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return true;
|
|
231
|
-
});
|
|
232
|
-
// Sort
|
|
233
|
-
const sort = query._sort || '';
|
|
234
|
-
const sorted = sortOn(res, sort.split(','));
|
|
235
|
-
// Slice
|
|
236
|
-
const start = query._start;
|
|
237
|
-
const end = query._end;
|
|
238
|
-
const limit = query._limit;
|
|
239
|
-
if (start !== undefined) {
|
|
240
|
-
if (end !== undefined) {
|
|
241
|
-
return sorted.slice(start, end);
|
|
242
|
-
}
|
|
243
|
-
return sorted.slice(start, start + (limit || 0));
|
|
244
|
-
}
|
|
245
|
-
if (limit !== undefined) {
|
|
246
|
-
return sorted.slice(0, limit);
|
|
94
|
+
results = results.filter((item) => matchesWhere(item, opts.where));
|
|
95
|
+
if (opts.sort) {
|
|
96
|
+
results = sortOn(results, opts.sort.split(','));
|
|
247
97
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const perPage = query._per_page || 10;
|
|
251
|
-
if (page) {
|
|
252
|
-
const items = sorted.length;
|
|
253
|
-
const pages = Math.ceil(items / perPage);
|
|
254
|
-
// Ensure page is within the valid range
|
|
255
|
-
page = Math.max(1, Math.min(page, pages));
|
|
256
|
-
const first = 1;
|
|
257
|
-
const prev = page > 1 ? page - 1 : null;
|
|
258
|
-
const next = page < pages ? page + 1 : null;
|
|
259
|
-
const last = pages;
|
|
260
|
-
const start = (page - 1) * perPage;
|
|
261
|
-
const end = start + perPage;
|
|
262
|
-
const data = sorted.slice(start, end);
|
|
263
|
-
return {
|
|
264
|
-
first,
|
|
265
|
-
prev,
|
|
266
|
-
next,
|
|
267
|
-
last,
|
|
268
|
-
pages,
|
|
269
|
-
items,
|
|
270
|
-
data,
|
|
271
|
-
};
|
|
98
|
+
if (opts.page !== undefined) {
|
|
99
|
+
return paginate(results, opts.page, opts.perPage ?? 10);
|
|
272
100
|
}
|
|
273
|
-
return
|
|
101
|
+
return results;
|
|
274
102
|
}
|
|
275
103
|
async create(name, data = {}) {
|
|
276
104
|
const items = this.#get(name);
|
|
@@ -285,7 +113,7 @@ export class Service {
|
|
|
285
113
|
const item = this.#get(name);
|
|
286
114
|
if (item === undefined || Array.isArray(item))
|
|
287
115
|
return;
|
|
288
|
-
const nextItem = (this.#db.data[name] = isPatch ? { item, ...body } : body);
|
|
116
|
+
const nextItem = (this.#db.data[name] = isPatch ? { ...item, ...body } : body);
|
|
289
117
|
await this.#db.write();
|
|
290
118
|
return nextItem;
|
|
291
119
|
}
|
|
@@ -322,7 +150,7 @@ export class Service {
|
|
|
322
150
|
if (item === undefined)
|
|
323
151
|
return;
|
|
324
152
|
const index = items.indexOf(item);
|
|
325
|
-
items.splice(index, 1)
|
|
153
|
+
items.splice(index, 1);
|
|
326
154
|
nullifyForeignKey(this.#db, name, id);
|
|
327
155
|
const dependents = ensureArray(dependent);
|
|
328
156
|
deleteDependents(this.#db, name, dependents);
|