@kollors/deep-json-server 0.4.0 → 0.6.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
@@ -4,7 +4,7 @@
4
4
 
5
5
  [GitHub](https://github.com/kollors/deep-json-server) | [npm](https://www.npmjs.com/package/@kollors/deep-json-server)
6
6
 
7
- A small JSON REST mock server with CRUD, pagination, deep filters and recursive relationship embedding. It keeps the database in one readable JSON file and infers soft relations from conventional keys such as `countryId`, `genreIds` and `publisherIds`.
7
+ A small REST mock server with CRUD, pagination, deep filtering, recursive relationship embedding, binary files, and OpenAPI generation. Data can be stored in JSON files or memory, and relations are inferred from conventional keys such as `countryId`, `genreIds`, and `publisherIds`.
8
8
 
9
9
  ## Installation
10
10
 
@@ -14,38 +14,149 @@ 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
+ ## Quick start
18
+
19
+ Create the database file `mock/database.json` before startup:
18
20
 
19
21
  ```json
20
22
  {
21
- "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"
24
- }
23
+ "movies": [
24
+ { "id": "1", "title": "Shadows of Ardenia" }
25
+ ]
25
26
  }
26
27
  ```
27
28
 
28
- Then run:
29
+ Create the ESM module `server.config.js` next to `package.json`:
30
+
31
+ ```js
32
+ export default {
33
+ database: {
34
+ path: 'mock/database.json',
35
+ },
36
+ };
37
+ ```
38
+
39
+ Start the server:
29
40
 
30
41
  ```bash
31
- npm run mock
42
+ npx deep-json-server server.config.js
43
+ ```
44
+
45
+ The API is available at `http://127.0.0.1:4001` by default. For example, `GET http://127.0.0.1:4001/movies` returns this page:
46
+
47
+ ```json
48
+ {
49
+ "data": [{ "id": "1", "title": "Shadows of Ardenia" }],
50
+ "total": 1
51
+ }
52
+ ```
53
+
54
+ ## Configuration and startup
55
+
56
+ Create the ESM module `server.config.js`. The example below enables every feature:
57
+
58
+ ```js
59
+ import process from 'node:process';
60
+
61
+ export default {
62
+ database: {
63
+ path: process.env.DATABASE_PATH ?? 'mock/database.json',
64
+ schema: 'mock/database-schema.json',
65
+ },
66
+ files: {
67
+ directory: 'mock/files',
68
+ metadata: 'mock/files/_database.json',
69
+ },
70
+ openapi: {
71
+ path: 'mock/openapi-schema.yaml',
72
+ },
73
+ server: {
74
+ host: '127.0.0.1',
75
+ logger: true,
76
+ maxFileSize: 100 * 1024 * 1024,
77
+ maxPageSize: 1000,
78
+ port: 4001,
79
+ },
80
+ };
32
81
  ```
33
82
 
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.
83
+ Configuration keys:
84
+
85
+ | Key | Condition | Purpose |
86
+ | --- | --- | --- |
87
+ | `database.path` | Exactly one of `path` or `data` is required | Existing JSON database file |
88
+ | `database.data` | Exactly one of `path` or `data` is required | Database object stored in memory |
89
+ | `database.schema` | No | Path to JSON overrides or an object with request-validation and OpenAPI settings |
90
+ | `files.directory` | Together with `files.metadata` | Directory for binary contents on disk |
91
+ | `files.metadata` | Together with `files.directory` | JSON file containing file metadata on disk |
92
+ | `files.data` | Instead of the `directory` and `metadata` pair | In-memory files with `Uint8Array` contents |
93
+ | `openapi.path` | Required by the `--openapi` and `--openapi-only` CLI flags | Generated OpenAPI YAML file; the programmatic API can return a document without this path |
94
+ | `server.host` | No | Host used by the CLI, `server.openapi()`, and argument-less `server.fastify().listen()`; defaults to `127.0.0.1` |
95
+ | `server.logger` | No | Fastify logger settings; defaults to `true` |
96
+ | `server.maxFileSize` | No | Maximum uploaded-file size in bytes when file routes are enabled; defaults to 100 MiB |
97
+ | `server.maxPageSize` | No | Maximum allowed `_perPage` in the API and OpenAPI; defaults to `1000` |
98
+ | `server.port` | No | Port used by the CLI, `server.openapi()`, and argument-less `server.fastify().listen()`; defaults to `4001` |
99
+
100
+ `server.port` must be an integer from `0` to `65535`. The value `0` lets Fastify select an available port at runtime, but cannot be used to generate an OpenAPI server URL, which requires a port from `1` to `65535`. Both `server.maxFileSize` and `server.maxPageSize` must be positive integers.
101
+
102
+ 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`.
103
+
104
+ The same config may keep everything in memory. `database.path` and `database.data` are mutually exclusive; `database.schema` accepts either a path or an object. Likewise, `files.data` cannot be combined with `files.directory` or `files.metadata`:
105
+
106
+ ```js
107
+ export default {
108
+ database: {
109
+ data: { movies: [{ id: '1', title: 'Shadows of Ardenia' }] },
110
+ schema: { $info: { title: 'Movie API', version: '1.0.0' } },
111
+ },
112
+ files: {
113
+ data: [{ content: new Uint8Array([1, 2, 3]), id: 'file-1', mimeType: 'application/octet-stream', name: 'example.bin' }],
114
+ },
115
+ };
116
+ ```
117
+
118
+ In-memory values are cloned during initialization. CRUD and file operations therefore do not mutate the exported config object, and their results disappear when the process exits.
119
+
120
+ Add the commands you need to `package.json`. Here, `mock:openapi:files` updates OpenAPI first and then keeps the server running with file routes:
121
+
122
+ ```json
123
+ {
124
+ "scripts": {
125
+ "mock": "deep-json-server server.config.js",
126
+ "mock:files": "deep-json-server --files server.config.js",
127
+ "mock:openapi:files": "deep-json-server --files --openapi server.config.js",
128
+ "openapi": "deep-json-server --openapi-only server.config.js",
129
+ "openapi:files": "deep-json-server --files --openapi-only server.config.js"
130
+ }
131
+ }
132
+ ```
133
+
134
+ CLI modes:
135
+
136
+ | Command | Behavior |
137
+ | --- | --- |
138
+ | `deep-json-server server.config.js` | Starts the CRUD server without file routes |
139
+ | `deep-json-server --files server.config.js` | Starts the CRUD server with file routes |
140
+ | `deep-json-server --openapi server.config.js` | Generates OpenAPI and starts the CRUD server |
141
+ | `deep-json-server --files --openapi server.config.js` | Generates OpenAPI with file routes and starts the server with them |
142
+ | `deep-json-server --openapi-only server.config.js` | Generates OpenAPI and exits |
143
+ | `deep-json-server --files --openapi-only server.config.js` | Generates OpenAPI with file routes and exits |
144
+
145
+ `--files` is independent: without it, file routes are neither registered nor added to OpenAPI, even when the config contains a `files` section. The `--openapi` and `--openapi-only` flags are mutually exclusive. Run `deep-json-server --help` to print the CLI summary.
35
146
 
36
147
  ## Example database
37
148
 
38
- This example is based on a movie catalog. `Gangster film` demonstrates a relationship with a parent genre.
149
+ Below is an example movie catalog with sample data. `Gangster` is linked to its parent genre, `Crime`.
39
150
 
40
151
  ```json
41
152
  {
42
153
  "countries": [
43
- { "id": "1", "isArchived": false, "name": "Russia" },
44
- { "id": "2", "isArchived": false, "name": "United States" }
154
+ { "id": "1", "isArchived": false, "name": "Ardenia" },
155
+ { "id": "2", "isArchived": false, "name": "Veloria" }
45
156
  ],
46
157
  "genres": [
47
158
  { "id": "1", "isArchived": false, "name": "Crime", "parentIds": [] },
48
- { "id": "2", "isArchived": false, "name": "Gangster film", "parentIds": ["1"] },
159
+ { "id": "2", "isArchived": false, "name": "Gangster", "parentIds": ["1"] },
49
160
  { "id": "3", "isArchived": false, "name": "Drama", "parentIds": [] },
50
161
  { "id": "4", "isArchived": false, "name": "Comedy", "parentIds": [] }
51
162
  ],
@@ -55,39 +166,39 @@ This example is based on a movie catalog. `Gangster film` demonstrates a relatio
55
166
  { "genreIds": ["2", "3"], "id": "movie-1-actor-1", "userId": "1" },
56
167
  { "genreIds": ["3"], "id": "movie-1-actor-2", "userId": "2" }
57
168
  ],
58
- "coverSrc": "https://image.tmdb.org/t/p/w500/3bhkrj58Vtu7enYsRolD1fZdja1.jpg",
59
- "description": "The story of the Corleone family and the transfer of power from one generation to the next.",
169
+ "coverSrc": "https://example.com/covers/shadows-of-ardenia.jpg",
170
+ "description": "The heir to a port city uncovers a conspiracy between two rival families.",
60
171
  "id": "1",
61
172
  "isArchived": false,
62
173
  "publisherIds": ["2"],
63
- "title": "The Godfather"
174
+ "title": "Shadows of Ardenia"
64
175
  },
65
176
  {
66
177
  "actors": [],
67
- "coverSrc": "https://image.tmdb.org/t/p/w500/eWdyYQreja6JGCzqHWXpWHDrrPo.jpg",
68
- "description": "The adventures of a concierge and his young assistant in a famous European hotel.",
178
+ "coverSrc": "https://example.com/covers/northern-star.jpg",
179
+ "description": "A night manager at an old hotel is drawn into the search for a missing painting.",
69
180
  "id": "2",
70
181
  "isArchived": false,
71
182
  "publisherIds": ["1"],
72
- "title": "The Grand Budapest Hotel"
183
+ "title": "Midnight at the Northern Star"
73
184
  }
74
185
  ],
75
186
  "publishers": [
76
- { "id": "1", "isArchived": false, "name": "A24" },
77
- { "id": "2", "isArchived": false, "name": "Paramount Pictures" }
187
+ { "id": "1", "isArchived": false, "name": "Northlight Studio" },
188
+ { "id": "2", "isArchived": false, "name": "Aurora Pictures" }
78
189
  ],
79
190
  "users": [
80
191
  {
81
- "bornAt": "1989-01-25",
192
+ "bornAt": "1988-03-14",
82
193
  "countryId": "1",
83
- "fullName": "Alexander Petrov",
194
+ "fullName": "Mira Volkova",
84
195
  "id": "1",
85
196
  "isArchived": false
86
197
  },
87
198
  {
88
- "bornAt": "1984-09-05",
89
- "countryId": "1",
90
- "fullName": "Yulia Peresild",
199
+ "bornAt": "1991-11-02",
200
+ "countryId": "2",
201
+ "fullName": "Leon Vetrov",
91
202
  "id": "2",
92
203
  "isArchived": false
93
204
  }
@@ -106,9 +217,15 @@ PATCH /movies/:id
106
217
  DELETE /movies/:id
107
218
  ```
108
219
 
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.
220
+ `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; disk storage persists them in JSON, while memory storage retains them until the process exits.
110
221
 
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.
222
+ 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.
223
+
224
+ Successful writes return the created, replaced, updated or deleted record. Errors use an appropriate HTTP status and this JSON shape:
225
+
226
+ ```json
227
+ { "error": "..." }
228
+ ```
112
229
 
113
230
  ## Pagination and sorting
114
231
 
@@ -116,30 +233,27 @@ The database file must exist before startup. Resource names may contain Latin le
116
233
  GET /movies?_page=1&_perPage=10&_sort=-id,title
117
234
  ```
118
235
 
119
- A GET collection always returns a page object. `_page` defaults to `1`, and `_perPage` defaults to `10`:
236
+ A collection GET always returns the current page data and the total number of records after filtering. `_page` defaults to `1`, and `_perPage` defaults to `10`:
120
237
 
121
238
  ```json
122
239
  {
123
240
  "data": [],
124
- "first": 1,
125
- "items": 0,
126
- "last": 1,
127
- "next": null,
128
- "pages": 1,
129
- "prev": null
241
+ "total": 0
130
242
  }
131
243
  ```
132
244
 
133
- 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.
245
+ `data` contains only the records on the requested page. `total` is the number of all records matching the filter before pagination is applied. When needed, a client can calculate the last page as `Math.max(1, Math.ceil(total / pageSize))`.
246
+
247
+ Both pagination parameters must be positive integers. `_perPage` cannot exceed `1000` by default; change the limit through `server.maxPageSize` in the config passed to either the CLI or `createServer()`. Invalid values return `400` instead of being silently corrected. A page beyond the last page returns an empty `data` array while preserving the actual `total` value.
134
248
 
135
- Prefix a sort field with `-` for descending order. Unknown or unsafe sort fields return `400`.
249
+ `_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
250
 
137
251
  ## Filters
138
252
 
139
253
  Pass a JSON object through `_where`:
140
254
 
141
255
  ```http
142
- GET /movies?_where={"title":{"contains":"father"}}
256
+ GET /movies?_where={"title":{"contains":"ardenia"}}
143
257
  ```
144
258
 
145
259
  Nested objects and arrays can be filtered at any depth. Conditions in one object use `AND` by default:
@@ -147,40 +261,61 @@ Nested objects and arrays can be filtered at any depth. Conditions in one object
147
261
  ```json
148
262
  {
149
263
  "actors": { "some": { "userId": { "eq": "1" } } },
150
- "title": { "contains": "father" }
264
+ "title": { "contains": "ardenia" }
151
265
  }
152
266
  ```
153
267
 
154
- Logical operators are also available:
268
+ Use `and`, `or` and `not` for explicit logical groups:
155
269
 
156
270
  ```json
157
271
  {
158
- "or": [
159
- { "title": { "contains": "father" } },
160
- { "actors": { "some": { "userId": { "eq": "2" } } } }
272
+ "and": [
273
+ {
274
+ "or": [
275
+ { "title": { "contains": "ardenia" } },
276
+ { "actors": { "some": { "userId": { "eq": "2" } } } }
277
+ ]
278
+ },
279
+ { "not": { "isArchived": { "eq": true } } }
161
280
  ]
162
281
  }
163
282
  ```
164
283
 
165
- Supported field operators: `contains`, `endsWith`, `eq`, `every`, `gt`, `gte`, `in`, `lt`, `lte`, `ne`, `none`, `not`, `some` and `startsWith`.
284
+ Field operators:
285
+
286
+ | Operator | Behavior |
287
+ | --- | --- |
288
+ | `eq`, `ne` | Equality or inequality |
289
+ | `contains` | Case-insensitive substring for strings, or matching member for arrays |
290
+ | `startsWith`, `endsWith` | Case-insensitive string prefix or suffix |
291
+ | `gt`, `gte`, `lt`, `lte` | Ordered comparison; ISO date strings can be compared lexically |
292
+ | `in` | Matches a scalar or array member against the supplied values |
293
+ | `some`, `every`, `none` | Applies a nested condition to array elements |
294
+ | `not` | Negates a nested field condition |
166
295
 
167
296
  Simple query parameters are supported too:
168
297
 
169
298
  ```http
170
- GET /movies?title:contains=father
299
+ GET /movies?title:contains=ardenia
171
300
  ```
172
301
 
173
302
  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
303
 
304
+ Multiple simple query filters are combined with `AND`. For an `in` filter, separate values with commas: `GET /movies?id:in=1,2`. On an array field, `in` means that at least one field element matches at least one supplied value. `every` returns `true` for an empty array, while `some` returns `false`.
305
+
306
+ 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, for example with `encodeURIComponent(JSON.stringify(where))`.
307
+
308
+ 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.
309
+
175
310
  ## Relationships
176
311
 
177
- Use `_embed` to replace IDs with related records:
312
+ Use `_embed` to add related records to the response:
178
313
 
179
314
  ```http
180
315
  GET /movies/1?_embed=actors.user.country&_embed=actors.genres&_embed=publishers
181
316
  ```
182
317
 
183
- The response contains actors, each actor's user and genres, the user's country, and publishers. Embedding can follow any number of levels:
318
+ 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. The server imposes no fixed depth limit, but every required level must be written explicitly in the finite `_embed` path:
184
319
 
185
320
  ```http
186
321
  GET /movies/1?_embed=actors.user.country
@@ -189,27 +324,65 @@ GET /genres/2?_embed=parents.parents
189
324
 
190
325
  Unknown or malformed `_embed` paths return `400`.
191
326
 
327
+ 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.
328
+
192
329
  Reverse relationships work as well:
193
330
 
194
331
  ```http
195
332
  GET /countries/1?_embed=users
196
333
  ```
197
334
 
198
- Relations are inferred by convention:
335
+ 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
336
 
200
337
  - `countryId` points to `countries`;
201
338
  - `userId` points to `users` when the requested relation is `user`;
202
339
  - `genreIds` points to `genres`;
203
340
  - `publisherIds` points to `publishers`;
204
- - `parentIds` points back to the current resource when `_embed=parents` is requested.
341
+ - `parentIds` points back to the current resource when `_embed=parents` is requested; `_embed=children` resolves the reverse self-relation.
342
+
343
+ 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.
344
+
345
+ 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.
346
+
347
+ ## Files
348
+
349
+ Add `files.directory` and `files.metadata` to the server config, then pass `--files` to enable raw binary uploads:
350
+
351
+ ```bash
352
+ deep-json-server --files server.config.js
353
+ ```
354
+
355
+ For temporary tests, use `files.data` instead. Each initial record contains `id`, `name`, `mimeType`, and binary `content` as a `Uint8Array`; `size` and `url` are derived automatically. Uploaded files then remain in memory until the process exits.
356
+
357
+ 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:
358
+
359
+ ```http
360
+ POST /_files
361
+ Content-Name: posters%2Fshadows-of-ardenia.jpg
362
+ Content-Type: image/jpeg
363
+
364
+ <binary body>
365
+ ```
366
+
367
+ A successful upload returns status `201`, metadata, and a stable URL:
368
+
369
+ ```json
370
+ {
371
+ "id": "generated-id",
372
+ "mimeType": "image/jpeg",
373
+ "name": "posters/shadows-of-ardenia.jpg",
374
+ "size": 182340,
375
+ "url": "/_files/generated-id"
376
+ }
377
+ ```
205
378
 
206
- They are soft references: the server resolves them when requested but does not enforce referential integrity when data is written.
379
+ `GET /_files/:id` returns the original bytes with status `200`. `DELETE /_files/:id` removes both the binary contents and metadata, and returns the deleted metadata with status `200`. 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.
207
380
 
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.
381
+ 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 `server.maxFileSize`. A missing or unsafe `Content-Name` returns `400`, an exceeded limit returns `413`, and a missing, malformed, or Fastify-unsupported `Content-Type` returns `400` or `415`, depending on which validation stage rejects it.
209
382
 
210
- ## OpenAPI generation
383
+ ## Database schema and OpenAPI generation
211
384
 
212
- Create a small configuration file next to the database, for example `mock/database-schema.json`:
385
+ The optional path or object in `database.schema` customizes inferred schemas. This is a Deep JSON Server configuration format, not a standard JSON Schema document: `$schema` is an object containing resource settings. For example, `mock/database-schema.json` may contain:
213
386
 
214
387
  ```json
215
388
  {
@@ -235,13 +408,27 @@ Create a small configuration file next to the database, for example `mock/databa
235
408
  }
236
409
  ```
237
410
 
238
- Generate an OpenAPI 3.0.3 file and exit:
411
+ Schema configuration:
412
+
413
+ | Key | Purpose |
414
+ | --- | --- |
415
+ | `$info` | OpenAPI `info`; when present, non-empty `title` and `version` are required |
416
+ | `$schema.<resource>.name` | Explicit component name when automatic singularization is unsuitable or collides |
417
+ | `$schema.<resource>.required` | Required field paths; nested paths use dots, such as `actors.userId` |
418
+ | `$schema.<resource>.formats` | OpenAPI formats for inferred or explicit string fields, such as `date`, `date-time` or `uri` |
419
+ | `$schema.<resource>.properties` | Recursive OpenAPI-compatible field schemas merged with inference |
420
+
421
+ `formats` is shorthand for assigning `format` to an existing string field. `properties` can fully describe a field—including its `type`, `format`, constraints, and nested properties—or add a field that is absent from the data. If both mechanisms assign a format to the same field, the value from `formats` is applied last.
422
+
423
+ Set `openapi.path` in the server config, then generate an OpenAPI 3.0.3 file and exit:
239
424
 
240
425
  ```bash
241
- deep-json-server mock/database.json --generate mock/database-schema.json mock/openapi-schema.yaml --host 127.0.0.1 --port 4001
426
+ deep-json-server --openapi-only server.config.js
242
427
  ```
243
428
 
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`.
429
+ To include file routes in the document, configure the `files` section and add `--files`: `deep-json-server --files --openapi-only server.config.js`.
430
+
431
+ 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
432
 
246
433
  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
434
 
@@ -261,9 +448,9 @@ Use `properties` to describe fields that cannot be inferred, particularly for an
261
448
  }
262
449
  ```
263
450
 
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.
451
+ 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
452
 
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`.
453
+ `$info` becomes the OpenAPI `info` object, while resource settings live under `$schema`. In the CLI, the OpenAPI `servers` entry uses `server.host` and `server.port`, then the `HOST` and `PORT` environment-variable fallbacks, and finally `http://127.0.0.1:4001`. A direct `createServer()` call does not read those environment variables automatically: `server.openapi()` uses the config values or the same default URL.
267
454
 
268
455
  Use `name` when a resource needs an explicit schema name instead of the automatically singularized name:
269
456
 
@@ -277,24 +464,87 @@ Use `name` when a resource needs an explicit schema name instead of the automati
277
464
  }
278
465
  ```
279
466
 
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.
467
+ 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 with `--openapi` or `--openapi-only`; normal server startup does not rewrite the file.
281
468
 
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`.
469
+ 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`.
283
470
 
284
471
  ## Programmatic API
285
472
 
286
473
  ```js
287
- import { createServer, generateOpenApi, startServer } from '@kollors/deep-json-server';
474
+ import { createServer } from '@kollors/deep-json-server';
475
+
476
+ const config = {
477
+ database: {
478
+ path: 'mock/database.json',
479
+ schema: 'mock/database-schema.json',
480
+ },
481
+ files: {
482
+ directory: 'mock/files',
483
+ metadata: 'mock/files/_database.json',
484
+ },
485
+ openapi: {
486
+ path: 'mock/openapi-schema.yaml',
487
+ },
488
+ server: {
489
+ host: '127.0.0.1',
490
+ logger: false,
491
+ maxFileSize: 100 * 1024 * 1024,
492
+ maxPageSize: 1000,
493
+ port: 4001,
494
+ },
495
+ };
496
+
497
+ // Make a request without opening a network port—useful in automated tests.
498
+ const server = await createServer(config);
499
+ const fastify = server.fastify();
500
+ const response = await fastify.inject({ method: 'GET', url: '/movies' });
288
501
 
289
- const server = await createServer({ databasePath: 'mock/database.json', logger: false, maxPageSize: 1000, schemaPath: 'mock/database-schema.json' });
502
+ console.log(response.json());
290
503
 
291
- const response = await server.inject({ method: 'GET', url: '/movies' });
504
+ // Return the document and write it to config.openapi.path.
505
+ const document = await server.openapi();
292
506
 
293
- await server.close();
507
+ await fastify.close();
294
508
 
295
- await startServer({ databasePath: 'mock/database.json', host: '127.0.0.1', port: 4001, schemaPath: 'mock/database-schema.json' });
509
+ // Start a network server. With no arguments, listen uses server.host and server.port.
510
+ const runningServer = await createServer(config);
511
+ const runningFastify = runningServer.fastify();
296
512
 
297
- await generateOpenApi({ databasePath: 'mock/database.json', host: '127.0.0.1', port: 4001, schemaPath: 'mock/database-schema.json', outputPath: 'mock/openapi-schema.yaml' });
513
+ await runningFastify.listen();
514
+
515
+ // Later, during application shutdown:
516
+ await runningFastify.close();
517
+
518
+ // Keep the database, schema, and files entirely in memory.
519
+ const memoryServer = await createServer({
520
+ database: {
521
+ data: { movies: [{ id: '1', title: 'Shadows of Ardenia' }] },
522
+ schema: { $info: { title: 'Movie API', version: '1.0.0' } },
523
+ },
524
+ files: {
525
+ data: [{ content: new Uint8Array([1, 2, 3]), id: 'file-1', mimeType: 'application/octet-stream', name: 'example.bin' }],
526
+ },
527
+ });
528
+
529
+ const memoryFastify = memoryServer.fastify();
530
+ const memoryResponse = await memoryFastify.inject({ method: 'GET', url: '/movies/1' });
531
+
532
+ console.log(memoryResponse.json());
533
+
534
+ await memoryFastify.close();
298
535
  ```
299
536
 
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.
537
+ `createServer()` accepts exactly the same config shape as `server.config.js`. It loads and clones the configured sources, then returns a facade with two operations:
538
+
539
+ | Member | Meaning |
540
+ | --- | --- | --- |
541
+ | `server.fastify()` | Lazily creates and caches the real Fastify instance; every native method remains available, and argument-less `listen()` uses `server.host` and `server.port` |
542
+ | `server.openapi()` | Returns an OpenAPI document and also writes it when `openapi.path` is configured |
543
+
544
+ File routes are enabled programmatically when a `files` section is present. The second argument has the shape `{ files?: boolean }`: pass `{ files: false }` to keep a configured store disabled, or `{ files: true }` to require a `files` section and enable the routes. `server.openapi()` uses the same feature state as `server.fastify()`.
545
+
546
+ An argument-less `server.fastify().listen()` uses `server.host` and `server.port`, falling back to `127.0.0.1:4001`. Explicit `listen(options)` values take precedence. Relative paths passed directly to `createServer()` resolve from the current working directory; paths loaded from `server.config.js` resolve from the config directory. The package includes generated TypeScript declarations for the facade and every config variant.
547
+
548
+ ## Scope and security
549
+
550
+ 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 when disk storage is configured 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.