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/lib/bin.js CHANGED
@@ -1,15 +1,16 @@
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 { 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: 'string',
31
- short: 'p',
32
- default: process.env['PORT'] ?? '3000',
31
+ type: "string",
32
+ short: "p",
33
+ default: process.env["PORT"] ?? "3000",
33
34
  },
34
35
  host: {
35
- type: 'string',
36
- short: 'h',
37
- default: process.env['HOST'] ?? 'localhost',
36
+ type: "string",
37
+ short: "h",
38
+ default: process.env["HOST"] ?? "localhost",
38
39
  },
39
40
  static: {
40
- type: 'string',
41
- short: 's',
41
+ type: "string",
42
+ short: "s",
42
43
  multiple: true,
43
44
  default: [],
44
45
  },
45
46
  help: {
46
- type: 'boolean',
47
+ type: "boolean",
47
48
  },
48
49
  version: {
49
- type: 'boolean',
50
+ type: "boolean",
50
51
  },
51
52
  // Deprecated
52
53
  watch: {
53
- type: 'boolean',
54
- short: 'w',
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('../package.json', import.meta.url)), 'utf-8'));
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('--watch/-w can be omitted, JSON Server 1+ watches for file changes by default'));
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 === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') {
83
- console.log(chalk.red(e.message.split('.')[0]));
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, 'utf-8').trim() === '') {
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) === '.json5') {
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('Endpoints:'));
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('\n'));
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('Press CTRL-C to stop'),
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('Index:'),
140
+ "",
141
+ chalk.bold("Index:"),
141
142
  chalk.gray(`http://localhost:${port}/`),
142
- '',
143
- chalk.bold('Static files:'),
144
- chalk.gray('Serving ./public directory if it exists'),
145
- '',
146
- ].join('\n'));
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['NODE_ENV'] !== 'production') {
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 prevEndpoints = '';
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('change', () => {
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
- return console.log(chalk.red(['', `Error parsing ${file}`, e.message].join('\n')));
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
+ }
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ export function randomId() {
3
+ return randomBytes(2).toString('hex');
4
+ }
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, query = {}) {
124
- let items = this.#get(name);
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(query._embed).forEach((related) => {
130
- if (items !== undefined && Array.isArray(items)) {
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
- // 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;
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
- // 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
- };
98
+ if (opts.page !== undefined) {
99
+ return paginate(results, opts.page, opts.perPage ?? 10);
272
100
  }
273
- return sorted.slice(start, end);
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)[0];
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);
@@ -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
+ }