json-server 1.0.0-beta.1 → 1.0.0-beta.10

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/bin.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
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 { createApp } from './app.js';
12
- import { Observer } from './observer.js';
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 { createApp } from "./app.js";
12
+ import { Observer } from "./observer.js";
13
13
  function help() {
14
14
  console.log(`Usage: json-server [options] <file>
15
15
 
@@ -27,44 +27,44 @@ function args() {
27
27
  const { values, positionals } = parseArgs({
28
28
  options: {
29
29
  port: {
30
- type: 'string',
31
- short: 'p',
32
- default: process.env['PORT'] ?? '3000',
30
+ type: "string",
31
+ short: "p",
32
+ default: process.env["PORT"] ?? "3000",
33
33
  },
34
34
  host: {
35
- type: 'string',
36
- short: 'h',
37
- default: process.env['HOST'] ?? 'localhost',
35
+ type: "string",
36
+ short: "h",
37
+ default: process.env["HOST"] ?? "localhost",
38
38
  },
39
39
  static: {
40
- type: 'string',
41
- short: 's',
40
+ type: "string",
41
+ short: "s",
42
42
  multiple: true,
43
43
  default: [],
44
44
  },
45
45
  help: {
46
- type: 'boolean',
46
+ type: "boolean",
47
47
  },
48
48
  version: {
49
- type: 'boolean',
49
+ type: "boolean",
50
50
  },
51
51
  // Deprecated
52
52
  watch: {
53
- type: 'boolean',
54
- short: 'w',
53
+ type: "boolean",
54
+ short: "w",
55
55
  },
56
56
  },
57
57
  allowPositionals: true,
58
58
  });
59
59
  // --version
60
60
  if (values.version) {
61
- const pkg = JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf-8'));
61
+ const pkg = JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf-8"));
62
62
  console.log(pkg.version);
63
63
  process.exit();
64
64
  }
65
65
  // Handle --watch
66
66
  if (values.watch) {
67
- console.log(chalk.yellow('--watch/-w can be omitted, JSON Server 1+ watches for file changes by default'));
67
+ console.log(chalk.yellow("--watch/-w can be omitted, JSON Server 1+ watches for file changes by default"));
68
68
  }
69
69
  if (values.help || positionals.length === 0) {
70
70
  help();
@@ -72,15 +72,15 @@ function args() {
72
72
  }
73
73
  // App args and options
74
74
  return {
75
- file: positionals[0] ?? '',
75
+ file: positionals[0] ?? "",
76
76
  port: parseInt(values.port),
77
77
  host: values.host,
78
78
  static: values.static,
79
79
  };
80
80
  }
81
81
  catch (e) {
82
- if (e.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') {
83
- console.log(chalk.red(e.message.split('.')[0]));
82
+ if (e.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
83
+ console.log(chalk.red(e.message.split(".")[0]));
84
84
  help();
85
85
  process.exit(1);
86
86
  }
@@ -95,12 +95,12 @@ if (!existsSync(file)) {
95
95
  process.exit(1);
96
96
  }
97
97
  // Handle empty string JSON file
98
- if (readFileSync(file, 'utf-8').trim() === '') {
99
- writeFileSync(file, '{}');
98
+ if (readFileSync(file, "utf-8").trim() === "") {
99
+ writeFileSync(file, "{}");
100
100
  }
101
101
  // Set up database
102
102
  let adapter;
103
- if (extname(file) === '.json5') {
103
+ if (extname(file) === ".json5") {
104
104
  adapter = new DataFile(file, {
105
105
  parse: JSON5.parse,
106
106
  stringify: JSON5.stringify,
@@ -115,41 +115,42 @@ await db.read();
115
115
  // Create app
116
116
  const app = createApp(db, { logger: false, static: staticArr });
117
117
  function logRoutes(data) {
118
- console.log(chalk.bold('Endpoints:'));
118
+ console.log(chalk.bold("Endpoints:"));
119
119
  if (Object.keys(data).length === 0) {
120
120
  console.log(chalk.gray(`No endpoints found, try adding some data to ${file}`));
121
121
  return;
122
122
  }
123
123
  console.log(Object.keys(data)
124
124
  .map((key) => `${chalk.gray(`http://${host}:${port}/`)}${chalk.blue(key)}`)
125
- .join('\n'));
125
+ .join("\n"));
126
126
  }
127
- const kaomojis = ['♡⸜(˶˃ ᵕ ˂˶)⸝♡', '♡( ◡‿◡ )', '( ˶ˆ ᗜ ˆ˵ )', '(˶ᵔ ᵕ ᵔ˶)'];
127
+ const kaomojis = ["♡⸜(˶˃ ᵕ ˂˶)⸝♡", "♡( ◡‿◡ )", "( ˶ˆ ᗜ ˆ˵ )", "(˶ᵔ ᵕ ᵔ˶)"];
128
128
  function randomItem(items) {
129
129
  const index = Math.floor(Math.random() * items.length);
130
- return items.at(index) ?? '';
130
+ return items.at(index) ?? "";
131
131
  }
132
132
  app.listen(port, () => {
133
133
  console.log([
134
134
  chalk.bold(`JSON Server started on PORT :${port}`),
135
- chalk.gray('Press CTRL-C to stop'),
135
+ chalk.gray("Press CTRL-C to stop"),
136
136
  chalk.gray(`Watching ${file}...`),
137
- '',
137
+ "",
138
138
  chalk.magenta(randomItem(kaomojis)),
139
- '',
140
- chalk.bold('Index:'),
139
+ "",
140
+ chalk.bold("Index:"),
141
141
  chalk.gray(`http://localhost:${port}/`),
142
- '',
143
- chalk.bold('Static files:'),
144
- chalk.gray('Serving ./public directory if it exists'),
145
- '',
146
- ].join('\n'));
142
+ "",
143
+ chalk.bold("Static files:"),
144
+ chalk.gray("Serving ./public directory if it exists"),
145
+ "",
146
+ ].join("\n"));
147
147
  logRoutes(db.data);
148
148
  });
149
149
  // Watch file for changes
150
- if (process.env['NODE_ENV'] !== 'production') {
150
+ if (process.env["NODE_ENV"] !== "production") {
151
151
  let writing = false; // true if the file is being written to by the app
152
- let prevEndpoints = '';
152
+ let hadReadError = false;
153
+ let prevEndpoints = "";
153
154
  observer.onWriteStart = () => {
154
155
  writing = true;
155
156
  };
@@ -164,17 +165,19 @@ if (process.env['NODE_ENV'] !== 'production') {
164
165
  return;
165
166
  }
166
167
  const nextEndpoints = JSON.stringify(Object.keys(data).sort());
167
- if (prevEndpoints !== nextEndpoints) {
168
+ if (hadReadError || prevEndpoints !== nextEndpoints) {
168
169
  console.log();
169
170
  logRoutes(data);
170
171
  }
172
+ hadReadError = false;
171
173
  };
172
- watch(file).on('change', () => {
174
+ watch(file).on("change", () => {
173
175
  // Do no reload if the file is being written to by the app
174
176
  if (!writing) {
175
177
  db.read().catch((e) => {
176
178
  if (e instanceof SyntaxError) {
177
- return console.log(chalk.red(['', `Error parsing ${file}`, e.message].join('\n')));
179
+ hadReadError = true;
180
+ return console.log(chalk.red(["", `Error parsing ${file}`, e.message].join("\n")));
178
181
  }
179
182
  console.log(e);
180
183
  });
@@ -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
+ }
@@ -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/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';
3
+ import { Low } from 'lowdb';
4
4
  import sortOn from 'sort-on';
5
+ import { matchesWhere } from "./matches-where.js";
6
+ import { paginate } from "./paginate.js";
5
7
  export function isItem(obj) {
6
- return typeof obj === 'object' && obj !== null;
8
+ return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
7
9
  }
8
10
  export function isData(obj) {
9
11
  if (typeof obj !== 'object' || obj === null) {
10
12
  return false;
11
13
  }
12
14
  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);
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,157 +110,24 @@ export class Service {
120
110
  }
121
111
  return;
122
112
  }
123
- find(name, query = {}) {
124
- let items = this.#get(name);
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(query._embed).forEach((related) => {
130
- if (items !== undefined && Array.isArray(items)) {
131
- items = items.map((item) => embed(this.#db, name, item, related));
132
- }
133
- });
134
- // Return list if no query params
135
- if (Object.keys(query).length === 0) {
136
- return items;
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;
120
+ ensureArray(opts.embed).forEach((related) => {
121
+ results = results.map((item) => embed(this.#db, name, item, related));
231
122
  });
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);
123
+ results = results.filter((item) => matchesWhere(item, opts.where));
124
+ if (opts.sort) {
125
+ results = sortOn(results, opts.sort.split(','));
247
126
  }
248
- // Paginate
249
- let page = query._page;
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
- };
127
+ if (opts.page !== undefined) {
128
+ return paginate(results, opts.page, opts.perPage ?? 10);
272
129
  }
273
- return sorted.slice(start, end);
130
+ return results;
274
131
  }
275
132
  async create(name, data = {}) {
276
133
  const items = this.#get(name);
@@ -285,7 +142,7 @@ export class Service {
285
142
  const item = this.#get(name);
286
143
  if (item === undefined || Array.isArray(item))
287
144
  return;
288
- const nextItem = (this.#db.data[name] = isPatch ? { item, ...body } : body);
145
+ const nextItem = (this.#db.data[name] = isPatch ? { ...item, ...body } : body);
289
146
  await this.#db.write();
290
147
  return nextItem;
291
148
  }
@@ -322,7 +179,7 @@ export class Service {
322
179
  if (item === undefined)
323
180
  return;
324
181
  const index = items.indexOf(item);
325
- items.splice(index, 1)[0];
182
+ items.splice(index, 1);
326
183
  nullifyForeignKey(this.#db, name, id);
327
184
  const dependents = ensureArray(dependent);
328
185
  deleteDependents(this.#db, name, dependents);
@@ -0,0 +1,15 @@
1
+ export const WHERE_OPERATORS = [
2
+ 'lt',
3
+ 'lte',
4
+ 'gt',
5
+ 'gte',
6
+ 'eq',
7
+ 'ne',
8
+ 'in',
9
+ 'contains',
10
+ 'startsWith',
11
+ 'endsWith',
12
+ ];
13
+ export function isWhereOperator(value) {
14
+ return WHERE_OPERATORS.includes(value);
15
+ }