@kollors/deep-json-server 0.4.0 → 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 --schema mock/database-schema.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,9 +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.
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.
110
153
 
111
- 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.
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
+ ```
112
161
 
113
162
  ## Pagination and sorting
114
163
 
@@ -132,7 +181,7 @@ A GET collection always returns a page object. `_page` defaults to `1`, and `_pe
132
181
 
133
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.
134
183
 
135
- Prefix a sort field with `-` for descending order. Unknown or unsafe sort fields return `400`.
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`.
136
185
 
137
186
  ## Filters
138
187
 
@@ -151,18 +200,33 @@ Nested objects and arrays can be filtered at any depth. Conditions in one object
151
200
  }
152
201
  ```
153
202
 
154
- Logical operators are also available:
203
+ Use `and`, `or` and `not` for explicit logical groups:
155
204
 
156
205
  ```json
157
206
  {
158
- "or": [
159
- { "title": { "contains": "father" } },
160
- { "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 } } }
161
215
  ]
162
216
  }
163
217
  ```
164
218
 
165
- 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 |
166
230
 
167
231
  Simple query parameters are supported too:
168
232
 
@@ -172,15 +236,19 @@ GET /movies?title:contains=father
172
236
 
173
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`.
174
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
+
175
243
  ## Relationships
176
244
 
177
- Use `_embed` to replace IDs with related records:
245
+ Use `_embed` to add related records to the response:
178
246
 
179
247
  ```http
180
248
  GET /movies/1?_embed=actors.user.country&_embed=actors.genres&_embed=publishers
181
249
  ```
182
250
 
183
- 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:
184
252
 
185
253
  ```http
186
254
  GET /movies/1?_embed=actors.user.country
@@ -189,27 +257,63 @@ GET /genres/2?_embed=parents.parents
189
257
 
190
258
  Unknown or malformed `_embed` paths return `400`.
191
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
+
192
262
  Reverse relationships work as well:
193
263
 
194
264
  ```http
195
265
  GET /countries/1?_embed=users
196
266
  ```
197
267
 
198
- 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:
199
269
 
200
270
  - `countryId` points to `countries`;
201
271
  - `userId` points to `users` when the requested relation is `user`;
202
272
  - `genreIds` points to `genres`;
203
273
  - `publisherIds` points to `publishers`;
204
- - `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.
205
275
 
206
- 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.
207
277
 
208
- 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.
209
279
 
210
- ## OpenAPI generation
280
+ ## Files
211
281
 
212
- 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:
213
317
 
214
318
  ```json
215
319
  {
@@ -235,13 +339,23 @@ Create a small configuration file next to the database, for example `mock/databa
235
339
  }
236
340
  ```
237
341
 
238
- 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:
239
353
 
240
354
  ```bash
241
- 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
242
356
  ```
243
357
 
244
- 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.
245
359
 
246
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.
247
361
 
@@ -261,9 +375,9 @@ Use `properties` to describe fields that cannot be inferred, particularly for an
261
375
  }
262
376
  ```
263
377
 
264
- 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.
265
379
 
266
- `$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`.
267
381
 
268
382
  Use `name` when a resource needs an explicit schema name instead of the automatically singularized name:
269
383
 
@@ -277,24 +391,73 @@ Use `name` when a resource needs an explicit schema name instead of the automati
277
391
  }
278
392
  ```
279
393
 
280
- The generated document describes CRUD endpoints, pagination, sorting, deep filters, `_embed`, and both direct and reverse response relations inferred from `...Id` and `...Ids` fields. 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 `--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.
281
395
 
282
- During normal startup, request bodies are validated against the inferred resource schemas. Pass `--schema mock/database-schema.json` to apply the same explicit `required`, `formats` and `properties` constraints at runtime. Invalid `POST`, `PUT` and `PATCH` bodies return `400`.
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.
283
401
 
284
402
  ## Programmatic API
285
403
 
286
404
  ```js
287
- import { createServer, generateOpenApi, startServer } from '@kollors/deep-json-server';
288
-
289
- const server = await createServer({ databasePath: 'mock/database.json', logger: false, maxPageSize: 1000, schemaPath: 'mock/database-schema.json' });
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
+ });
290
417
 
291
418
  const response = await server.inject({ method: 'GET', url: '/movies' });
292
419
 
293
420
  await server.close();
294
421
 
295
- await startServer({ databasePath: 'mock/database.json', host: '127.0.0.1', port: 4001, schemaPath: 'mock/database-schema.json' });
296
-
297
- 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
+ );
298
448
  ```
299
449
 
300
- `createServer()` is useful for tests because it returns a Fastify instance without opening a network port. The package includes generated TypeScript declarations for all exported functions.
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.