@kollors/deep-json-server 0.3.2 → 0.5.0

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 CHANGED
@@ -14,24 +14,67 @@ Node.js 20 or newer is required.
14
14
  npm install --save-dev @kollors/deep-json-server
15
15
  ```
16
16
 
17
- Add a script to `package.json`:
17
+ ## Configuration and startup
18
+
19
+ Create the ESM module `server.config.js`. The example below enables every feature:
20
+
21
+ ```js
22
+ import process from 'node:process';
23
+
24
+ export default {
25
+ database: {
26
+ path: process.env.DATABASE_PATH ?? 'mock/database.json',
27
+ schema: 'mock/database-schema.json',
28
+ },
29
+ files: {
30
+ directory: 'mock/files',
31
+ metadata: 'mock/files/_database.json',
32
+ },
33
+ openapi: {
34
+ path: 'mock/openapi-schema.yaml',
35
+ },
36
+ server: {
37
+ host: '127.0.0.1',
38
+ port: 4001,
39
+ },
40
+ };
41
+ ```
42
+
43
+ Configuration keys:
44
+
45
+ | Key | Required | Purpose |
46
+ | --- | --- | --- |
47
+ | `database.path` | Always | Existing JSON database file |
48
+ | `database.schema` | No | JSON overrides for request validation and OpenAPI schemas |
49
+ | `files.directory` | With `--files` | Directory for binary contents |
50
+ | `files.metadata` | With `--files` | JSON file containing uploaded-file metadata |
51
+ | `openapi.path` | With `--openapi` | Generated OpenAPI YAML file |
52
+ | `server.host` | No | Listening host; falls back to `HOST`, then `127.0.0.1` |
53
+ | `server.port` | No | Listening port; falls back to `PORT`, then `4001` |
54
+
55
+ All relative paths are resolved from the directory containing `server.config.js`, not from the current working directory. Unknown keys, empty paths and invalid value types are rejected before startup. The config is executable JavaScript, so it can read environment variables, import other modules and calculate values before exporting the object. A `.js` config with `export default` requires an ESM project (`"type": "module"`); in a CommonJS project, use the same contents in `server.config.mjs`.
56
+
57
+ Add the commands you need to `package.json`:
18
58
 
19
59
  ```json
20
60
  {
21
61
  "scripts": {
22
- "mock": "deep-json-server mock/database.json --port 4001",
23
- "openapi": "deep-json-server mock/database.json --generate mock/database-schema.json mock/openapi-schema.yaml"
62
+ "mock": "deep-json-server --files server.config.js",
63
+ "openapi": "deep-json-server --openapi --files server.config.js"
24
64
  }
25
65
  }
26
66
  ```
27
67
 
28
- Then run:
68
+ CLI modes:
29
69
 
30
- ```bash
31
- npm run mock
32
- ```
70
+ | Command | Behavior |
71
+ | --- | --- |
72
+ | `deep-json-server server.config.js` | Starts the CRUD server without file routes |
73
+ | `deep-json-server --files server.config.js` | Starts the CRUD server with file routes |
74
+ | `deep-json-server --openapi server.config.js` | Generates OpenAPI and exits |
75
+ | `deep-json-server --openapi --files server.config.js` | Generates OpenAPI with file routes and exits |
33
76
 
34
- The default address is `http://127.0.0.1:4001`. You can also pass `--host` and `--port`, or set the `HOST` and `PORT` environment variables.
77
+ `--openapi` never starts the HTTP server. `--files` is independent: without it, file routes are neither registered nor added to OpenAPI. Run `deep-json-server --help` to print the CLI summary.
35
78
 
36
79
  ## Example database
37
80
 
@@ -106,7 +149,15 @@ PATCH /movies/:id
106
149
  DELETE /movies/:id
107
150
  ```
108
151
 
109
- `POST` generates a string ID, while `PUT` and `PATCH` preserve the stored ID type. All write operations — `POST`, `PUT`, `PATCH` and `DELETE` — persist their changes in the JSON file. The database file must exist before startup and contain a JSON object whose every top-level property is an array of resource records.
152
+ `POST` generates a string ID. `PUT` completely replaces the selected record, while `PATCH` updates only supplied fields; both preserve the existing ID and its type. An `id` supplied in any request body cannot override the server-controlled ID. All write operations — `POST`, `PUT`, `PATCH` and `DELETE` — are serialized and persisted in the JSON file.
153
+
154
+ The database file must exist before startup. Resource names may contain Latin letters, numbers, `_` and `-`, and must start with a letter. Every resource is an array of JSON objects. Every record must have a non-empty string or finite numeric `id`; IDs must be unique within a resource when compared as strings, so `1` and `"1"` cannot coexist. The server rereads the file before every GET and write operation, so valid external edits become visible without a restart.
155
+
156
+ Successful writes return the created, replaced, updated or deleted record. Errors use an appropriate HTTP status and this JSON shape:
157
+
158
+ ```json
159
+ { "error": "Human-readable message" }
160
+ ```
110
161
 
111
162
  ## Pagination and sorting
112
163
 
@@ -128,9 +179,9 @@ A GET collection always returns a page object. `_page` defaults to `1`, and `_pe
128
179
  }
129
180
  ```
130
181
 
131
- Both pagination parameters must be positive integers. Invalid values return `400` instead of being silently corrected.
182
+ Both pagination parameters must be positive integers. `_perPage` cannot exceed `1000` by default; use the programmatic `maxPageSize` option to change that limit. Invalid values return `400` instead of being silently corrected. A page beyond the last page returns an empty `data` array and points `prev` to the last available page instead of silently clamping the request.
132
183
 
133
- Prefix a sort field with `-` for descending order.
184
+ `_sort` accepts comma-separated field paths. Rules are applied from left to right; prefix a field with `-` for descending order. Dot paths can address nested object fields, including fields added by `_embed`, for example `GET /users?_embed=country&_sort=country.name,-id`. Unknown or unsafe sort fields return `400`.
134
185
 
135
186
  ## Filters
136
187
 
@@ -149,18 +200,33 @@ Nested objects and arrays can be filtered at any depth. Conditions in one object
149
200
  }
150
201
  ```
151
202
 
152
- Logical operators are also available:
203
+ Use `and`, `or` and `not` for explicit logical groups:
153
204
 
154
205
  ```json
155
206
  {
156
- "or": [
157
- { "title": { "contains": "father" } },
158
- { "actors": { "some": { "userId": { "eq": "2" } } } }
207
+ "and": [
208
+ {
209
+ "or": [
210
+ { "title": { "contains": "father" } },
211
+ { "actors": { "some": { "userId": { "eq": "2" } } } }
212
+ ]
213
+ },
214
+ { "not": { "isArchived": { "eq": true } } }
159
215
  ]
160
216
  }
161
217
  ```
162
218
 
163
- Supported field operators: `contains`, `endsWith`, `eq`, `every`, `gt`, `gte`, `in`, `lt`, `lte`, `ne`, `none`, `not`, `some` and `startsWith`.
219
+ Field operators:
220
+
221
+ | Operator | Behavior |
222
+ | --- | --- |
223
+ | `eq`, `ne` | Equality or inequality |
224
+ | `contains` | Case-insensitive substring for strings, or matching member for arrays |
225
+ | `startsWith`, `endsWith` | Case-insensitive string prefix or suffix |
226
+ | `gt`, `gte`, `lt`, `lte` | Ordered comparison; ISO date strings can be compared lexically |
227
+ | `in` | Matches a scalar or array member against the supplied values |
228
+ | `some`, `every`, `none` | Applies a nested condition to array elements |
229
+ | `not` | Negates a nested field condition |
164
230
 
165
231
  Simple query parameters are supported too:
166
232
 
@@ -170,42 +236,84 @@ GET /movies?title:contains=father
170
236
 
171
237
  Simple filter values recognize JSON primitives: numbers, `true`, `false` and `null`. Values with leading zeroes, such as `001`, remain strings. Unknown operators, invalid logical conditions and filter paths that do not exist in a non-empty resource return `400`.
172
238
 
239
+ For a simple `in` filter, separate values with commas: `GET /movies?id:in=1,2`. If `_where` is present, it is the complete filter and other simple filter parameters are ignored. The examples show readable JSON; an HTTP client must URL-encode `_where` when constructing the URL manually.
240
+
241
+ Filtering is performed after `_embed`. This means a filter can address fields added by an embedded relation when the same request includes that `_embed`; stored `...Id` and `...Ids` fields can always be filtered directly.
242
+
173
243
  ## Relationships
174
244
 
175
- Use `_embed` to replace IDs with related records:
245
+ Use `_embed` to add related records to the response:
176
246
 
177
247
  ```http
178
248
  GET /movies/1?_embed=actors.user.country&_embed=actors.genres&_embed=publishers
179
249
  ```
180
250
 
181
- The response contains actors, each actor's user and genres, the user's country, and publishers. Embedding can follow any number of levels:
251
+ The response contains actors, each actor's user and genres, the user's country, and publishers. Original ID fields remain in the response, and the database file is not modified. Embedding can follow any number of levels:
182
252
 
183
253
  ```http
184
254
  GET /movies/1?_embed=actors.user.country
185
255
  GET /genres/2?_embed=parents.parents
186
256
  ```
187
257
 
258
+ Unknown or malformed `_embed` paths return `400`.
259
+
260
+ Pass `_embed` more than once, as above, or provide a comma-separated list in one parameter. Pagination applies only to the requested root collection; embedded related records are returned in full.
261
+
188
262
  Reverse relationships work as well:
189
263
 
190
264
  ```http
191
265
  GET /countries/1?_embed=users
192
266
  ```
193
267
 
194
- Relations are inferred by convention:
268
+ Relations are inferred by convention. A field named `<relation>Id` creates a single relation, while `<relation>Ids` creates a collection relation. The relation name is matched to a top-level resource directly or through its singular form. For example:
195
269
 
196
270
  - `countryId` points to `countries`;
197
271
  - `userId` points to `users` when the requested relation is `user`;
198
272
  - `genreIds` points to `genres`;
199
273
  - `publisherIds` points to `publishers`;
200
- - `parentIds` points back to the current resource when `_embed=parents` is requested.
274
+ - `parentIds` points back to the current resource when `_embed=parents` is requested; `_embed=children` resolves the reverse self-relation.
201
275
 
202
- They are soft references: the server resolves them when requested but does not enforce referential integrity when data is written.
276
+ Reverse relations use the source resource name. For example, `_embed=users` on a country finds users whose nested data contains the corresponding `countryId`. They are soft references: the server resolves them when requested but does not enforce referential integrity when data is written.
203
277
 
204
- An explicit `...Id` or `...Ids` field is the source of truth. If a record also contains an outdated embedded value, `_embed` replaces it with the current related record. Relationship lookups use lazy per-request ID indexes, so each referenced resource is indexed only when needed.
278
+ An explicit `...Id` or `...Ids` field is the source of truth. If a record also contains an outdated embedded value, `_embed` replaces that response property with the current related record. A missing target becomes `null` for a single relation or is omitted from the resulting array for a collection relation. Relationship lookups use lazy per-request ID indexes, so each referenced resource is indexed only when needed.
205
279
 
206
- ## OpenAPI generation
280
+ ## Files
207
281
 
208
- Create a small configuration file next to the database, for example `mock/database-schema.json`:
282
+ Add `files.directory` and `files.metadata` to the server config, then pass `--files` to enable raw binary uploads:
283
+
284
+ ```bash
285
+ deep-json-server --files server.config.js
286
+ ```
287
+
288
+ Upload one file as the request body. Both headers are required: `Content-Name` contains the relative logical name encoded with `encodeURIComponent`, and `Content-Type` contains the file MIME type:
289
+
290
+ ```http
291
+ POST /_files
292
+ Content-Name: posters%2Fthe-godfather.jpg
293
+ Content-Type: image/jpeg
294
+
295
+ <binary body>
296
+ ```
297
+
298
+ The response contains metadata and a stable URL:
299
+
300
+ ```json
301
+ {
302
+ "id": "generated-id",
303
+ "mimeType": "image/jpeg",
304
+ "name": "posters/the-godfather.jpg",
305
+ "size": 182340,
306
+ "url": "/_files/generated-id"
307
+ }
308
+ ```
309
+
310
+ Use `GET /_files/:id` to download the original bytes and `DELETE /_files/:id` to delete both the binary contents and their metadata. The returned `url` is relative to the mock-server origin. The server creates the configured directories automatically, stores binary contents under generated IDs without relying on the original file name, and keeps the logical names and other metadata in `files.metadata`. The metadata file may be absent initially and is created on the first upload. Do not edit it while the server is running.
311
+
312
+ The upload is raw binary rather than `multipart/form-data`, so `XMLHttpRequest.upload.onprogress` can report progress while the browser sends a `File` directly with `xhr.send(file)`. The default maximum size is 100 MiB and can be changed through the programmatic `maxFileSize` option. Unsafe or absolute `Content-Name` paths return `400`, an exceeded limit returns `413`, and a malformed or unsupported `Content-Type` returns `400` or `415`.
313
+
314
+ ## Database schema and OpenAPI generation
315
+
316
+ The optional JSON file referenced by `database.schema`, for example `mock/database-schema.json`, customizes inferred schemas. It is read both during normal server startup and during OpenAPI generation:
209
317
 
210
318
  ```json
211
319
  {
@@ -231,13 +339,23 @@ Create a small configuration file next to the database, for example `mock/databa
231
339
  }
232
340
  ```
233
341
 
234
- Generate an OpenAPI 3.0.3 file and exit:
342
+ Schema configuration:
343
+
344
+ | Key | Purpose |
345
+ | --- | --- |
346
+ | `$info` | OpenAPI `info`; when present, non-empty `title` and `version` are required |
347
+ | `$schema.<resource>.name` | Explicit component name when automatic singularization is unsuitable or collides |
348
+ | `$schema.<resource>.required` | Required field paths; nested paths use dots, such as `actors.userId` |
349
+ | `$schema.<resource>.formats` | OpenAPI formats for inferred or explicit string fields, such as `date`, `date-time` or `uri` |
350
+ | `$schema.<resource>.properties` | Recursive OpenAPI-compatible field schemas merged with inference |
351
+
352
+ Set `openapi.path` in the server config, then generate an OpenAPI 3.0.3 file and exit:
235
353
 
236
354
  ```bash
237
- deep-json-server mock/database.json --generate mock/database-schema.json mock/openapi-schema.yaml --host 127.0.0.1 --port 4001
355
+ deep-json-server --openapi --files server.config.js
238
356
  ```
239
357
 
240
- The generator infers resources and field types from all database records. Every inferred field is optional by default, while a top-level `id` present in the resulting resource schema is always required. Add other required fields to `required`; nested fields use dot paths such as `actors.userId`. The `formats` object adds OpenAPI formats such as `date` and `uri`.
358
+ The generator infers resources and field types from all database records. Every inferred field is optional by default, while the top-level `id` is always required in response schemas and is omitted from create and update request schemas. Add other required fields to `required`. A nested required path marks that nested property as required; it does not automatically make every parent path required, so list the parent separately when necessary.
241
359
 
242
360
  Different value types are inferred independently and combined through `oneOf`. Configuration is validated before generation: `$info`, resource and schema names, and the structure of `properties` are validated, while paths from `required` and `formats` must exist in the resulting schema.
243
361
 
@@ -257,9 +375,9 @@ Use `properties` to describe fields that cannot be inferred, particularly for an
257
375
  }
258
376
  ```
259
377
 
260
- An empty resource still receives a required string `id` property because IDs created by the server are strings. Generation stops with an actionable error when resources produce duplicate schema names or operation IDs; use an explicit `name` to resolve schema-name collisions.
378
+ An empty resource still receives a required string `id` property because IDs created by the server are strings. Generation stops with an actionable error when resources produce duplicate schema names or operation IDs; use an explicit `name` to resolve schema-name collisions. The output directory is created automatically, and the configured YAML file is replaced on every generation.
261
379
 
262
- `$info` becomes the OpenAPI `info` object, while resource settings live under `$schema`. The OpenAPI `servers` entry is generated automatically from `--host` and `--port`, their `HOST` and `PORT` environment variable equivalents, or the default `http://127.0.0.1:4001`.
380
+ `$info` becomes the OpenAPI `info` object, while resource settings live under `$schema`. The OpenAPI `servers` entry is generated automatically from `server.host` and `server.port`, their `HOST` and `PORT` environment variable fallbacks, or the default `http://127.0.0.1:4001`.
263
381
 
264
382
  Use `name` when a resource needs an explicit schema name instead of the automatically singularized name:
265
383
 
@@ -273,22 +391,73 @@ Use `name` when a resource needs an explicit schema name instead of the automati
273
391
  }
274
392
  ```
275
393
 
276
- The generated document describes CRUD endpoints, pagination, sorting, deep filters, `_embed`, and response relations inferred from `...Id` and `...Ids` fields. It can be used as input for tools such as RTK Query OpenAPI Codegen. OpenAPI is generated only when `--generate` is passed; normal server startup does not rewrite the file.
394
+ The generated document describes CRUD endpoints, pagination, sorting, deep filters, `_embed`, and both direct and reverse response relations inferred from `...Id` and `...Ids` fields. When `--files` is present, it also describes raw binary upload, download and deletion endpoints. A numeric database ID is described as `integer | string`, because a later `POST` creates a string ID in the same resource. The document can be used as input for tools such as RTK Query OpenAPI Codegen. OpenAPI is generated only when `--openapi` is passed; normal server startup does not rewrite the file.
395
+
396
+ During normal startup, request bodies are validated against the same inferred and configured schemas. `POST` and `PUT` enforce configured required fields; `PATCH` validates only fields that are actually supplied. `formats` and `properties` apply to all three methods. Unlisted additional object fields remain allowed. Invalid bodies return `400`.
397
+
398
+ ## Scope and security
399
+
400
+ Deep JSON Server is intended for local development and automated tests. It has no authentication or authorization, allows CORS from every origin, persists accepted writes directly to the configured files and does not enforce referential integrity. Keep the default loopback host unless the surrounding environment provides its own access controls; do not expose the server or file routes to an untrusted network.
277
401
 
278
402
  ## Programmatic API
279
403
 
280
404
  ```js
281
- import { createServer, generateOpenApi, startServer } from '@kollors/deep-json-server';
282
-
283
- const server = await createServer({ databasePath: 'mock/database.json', logger: false });
405
+ import { createOpenApiDocument, createServer, generateOpenApi, startServer } from '@kollors/deep-json-server';
406
+
407
+ // Create an instance without opening a port, for example for tests.
408
+ const server = await createServer({
409
+ databasePath: 'mock/database.json',
410
+ filesDirectoryPath: 'mock/files',
411
+ filesMetadataPath: 'mock/files/_database.json',
412
+ logger: false,
413
+ maxFileSize: 100 * 1024 * 1024,
414
+ maxPageSize: 1000,
415
+ schemaPath: 'mock/database-schema.json',
416
+ });
284
417
 
285
418
  const response = await server.inject({ method: 'GET', url: '/movies' });
286
419
 
287
420
  await server.close();
288
421
 
289
- await startServer({ databasePath: 'mock/database.json', host: '127.0.0.1', port: 4001 });
290
-
291
- await generateOpenApi({ databasePath: 'mock/database.json', host: '127.0.0.1', port: 4001, schemaPath: 'mock/database-schema.json', outputPath: 'mock/openapi-schema.yaml' });
422
+ // Create an instance and start listening.
423
+ const listeningServer = await startServer({
424
+ databasePath: 'mock/database.json',
425
+ host: '127.0.0.1',
426
+ port: 4001,
427
+ schemaPath: 'mock/database-schema.json',
428
+ });
429
+
430
+ await listeningServer.close();
431
+
432
+ // Read the database and schema files, then write an OpenAPI YAML file.
433
+ await generateOpenApi({
434
+ databasePath: 'mock/database.json',
435
+ files: true,
436
+ host: '127.0.0.1',
437
+ outputPath: 'mock/openapi-schema.yaml',
438
+ port: 4001,
439
+ schemaPath: 'mock/database-schema.json',
440
+ });
441
+
442
+ // Build the same kind of OpenAPI document entirely in memory.
443
+ const document = createOpenApiDocument(
444
+ { movies: [{ id: '1', title: 'The Godfather' }] },
445
+ { $info: { title: 'Movie API', version: '1.0.0' } },
446
+ { files: true, host: '127.0.0.1', port: 4001 },
447
+ );
292
448
  ```
293
449
 
294
- `createServer()` is useful for tests because it returns a Fastify instance without opening a network port.
450
+ Programmatic options:
451
+
452
+ | Option | Used by | Meaning |
453
+ | --- | --- | --- |
454
+ | `databasePath` | `createServer`, `startServer`, `generateOpenApi` | Required JSON database path |
455
+ | `schemaPath` | The same three functions | Optional database-schema path |
456
+ | `filesDirectoryPath`, `filesMetadataPath` | `createServer`, `startServer` | Optional pair enabling file routes |
457
+ | `files` | `generateOpenApi`, `createOpenApiDocument` | Whether file routes appear in OpenAPI |
458
+ | `host`, `port` | `startServer`, `generateOpenApi`, `createOpenApiDocument` | Listening address or generated `servers` URL |
459
+ | `logger` | `createServer`, `startServer` | Fastify logger settings; defaults to `true` |
460
+ | `maxPageSize`, `maxFileSize` | `createServer`, `startServer` | Runtime limits; defaults are 1000 records and 100 MiB |
461
+ | `outputPath` | `generateOpenApi` | Required generated YAML path |
462
+
463
+ `createServer()` returns a Fastify instance without opening a network port, which is useful with `server.inject()` in tests. `startServer()` also starts listening. `generateOpenApi()` reads files and writes YAML, while `createOpenApiDocument()` works with in-memory database and schema objects and does not write a file. These functions do not read `server.config.js`; pass their options explicitly. The package includes generated TypeScript declarations for all exported functions.