@kollors/deep-json-server 0.5.0 → 0.7.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 +229 -113
- package/README.ru.md +229 -113
- package/index.js +12 -2
- package/package.json +9 -2
- package/src/cli.js +45 -44
- package/src/config.js +141 -35
- package/src/constants.js +1 -1
- package/src/database.js +40 -20
- package/src/files.js +445 -90
- package/src/openapi/config.js +15 -1
- package/src/openapi/document.js +124 -61
- package/src/openapi/index.js +29 -18
- package/src/query/filter.js +11 -10
- package/src/query/pagination.js +3 -9
- package/src/relation-metadata.js +1 -0
- package/src/server.js +104 -92
- package/src/utils.js +30 -0
- package/types/index.d.ts +21 -2
- package/types/src/config.d.ts +73 -0
- package/types/src/constants.d.ts +1 -1
- package/types/src/database.d.ts +28 -3
- package/types/src/files.d.ts +63 -4
- package/types/src/openapi/config.d.ts +1 -1
- package/types/src/openapi/document.d.ts +6 -7
- package/types/src/openapi/index.d.ts +9 -14
- package/types/src/query/pagination.d.ts +1 -6
- package/types/src/server.d.ts +12 -28
- package/types/src/utils.d.ts +8 -0
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
|
|
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,6 +14,43 @@ Node.js 20 or newer is required.
|
|
|
14
14
|
npm install --save-dev @kollors/deep-json-server
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
Create the database file `mock/database.json` before startup:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"movies": [
|
|
24
|
+
{ "id": "1", "title": "Shadows of Ardenia" }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
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:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
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
|
+
|
|
17
54
|
## Configuration and startup
|
|
18
55
|
|
|
19
56
|
Create the ESM module `server.config.js`. The example below enables every feature:
|
|
@@ -35,6 +72,9 @@ export default {
|
|
|
35
72
|
},
|
|
36
73
|
server: {
|
|
37
74
|
host: '127.0.0.1',
|
|
75
|
+
logger: true,
|
|
76
|
+
maxFileSize: 100 * 1024 * 1024,
|
|
77
|
+
maxPageSize: 1000,
|
|
38
78
|
port: 4001,
|
|
39
79
|
},
|
|
40
80
|
};
|
|
@@ -42,25 +82,51 @@ export default {
|
|
|
42
82
|
|
|
43
83
|
Configuration keys:
|
|
44
84
|
|
|
45
|
-
| Key |
|
|
85
|
+
| Key | Condition | Purpose |
|
|
46
86
|
| --- | --- | --- |
|
|
47
|
-
| `database.path` |
|
|
48
|
-
| `database.
|
|
49
|
-
| `
|
|
50
|
-
| `files.
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
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.
|
|
54
101
|
|
|
55
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`.
|
|
56
103
|
|
|
57
|
-
|
|
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]), directory: 'examples', 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:
|
|
58
121
|
|
|
59
122
|
```json
|
|
60
123
|
{
|
|
61
124
|
"scripts": {
|
|
62
|
-
"mock": "deep-json-server
|
|
63
|
-
"
|
|
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"
|
|
64
130
|
}
|
|
65
131
|
}
|
|
66
132
|
```
|
|
@@ -71,24 +137,26 @@ CLI modes:
|
|
|
71
137
|
| --- | --- |
|
|
72
138
|
| `deep-json-server server.config.js` | Starts the CRUD server without file routes |
|
|
73
139
|
| `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
|
|
75
|
-
| `deep-json-server --
|
|
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 |
|
|
76
144
|
|
|
77
|
-
`--
|
|
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.
|
|
78
146
|
|
|
79
147
|
## Example database
|
|
80
148
|
|
|
81
|
-
|
|
149
|
+
Below is an example movie catalog with sample data. `Gangster` is linked to its parent genre, `Crime`.
|
|
82
150
|
|
|
83
151
|
```json
|
|
84
152
|
{
|
|
85
153
|
"countries": [
|
|
86
|
-
{ "id": "1", "isArchived": false, "name": "
|
|
87
|
-
{ "id": "2", "isArchived": false, "name": "
|
|
154
|
+
{ "id": "1", "isArchived": false, "name": "Ardenia" },
|
|
155
|
+
{ "id": "2", "isArchived": false, "name": "Veloria" }
|
|
88
156
|
],
|
|
89
157
|
"genres": [
|
|
90
158
|
{ "id": "1", "isArchived": false, "name": "Crime", "parentIds": [] },
|
|
91
|
-
{ "id": "2", "isArchived": false, "name": "Gangster
|
|
159
|
+
{ "id": "2", "isArchived": false, "name": "Gangster", "parentIds": ["1"] },
|
|
92
160
|
{ "id": "3", "isArchived": false, "name": "Drama", "parentIds": [] },
|
|
93
161
|
{ "id": "4", "isArchived": false, "name": "Comedy", "parentIds": [] }
|
|
94
162
|
],
|
|
@@ -98,39 +166,39 @@ This example is based on a movie catalog. `Gangster film` demonstrates a relatio
|
|
|
98
166
|
{ "genreIds": ["2", "3"], "id": "movie-1-actor-1", "userId": "1" },
|
|
99
167
|
{ "genreIds": ["3"], "id": "movie-1-actor-2", "userId": "2" }
|
|
100
168
|
],
|
|
101
|
-
"coverSrc": "https://
|
|
102
|
-
"description": "The
|
|
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.",
|
|
103
171
|
"id": "1",
|
|
104
172
|
"isArchived": false,
|
|
105
173
|
"publisherIds": ["2"],
|
|
106
|
-
"title": "
|
|
174
|
+
"title": "Shadows of Ardenia"
|
|
107
175
|
},
|
|
108
176
|
{
|
|
109
177
|
"actors": [],
|
|
110
|
-
"coverSrc": "https://
|
|
111
|
-
"description": "
|
|
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.",
|
|
112
180
|
"id": "2",
|
|
113
181
|
"isArchived": false,
|
|
114
182
|
"publisherIds": ["1"],
|
|
115
|
-
"title": "
|
|
183
|
+
"title": "Midnight at the Northern Star"
|
|
116
184
|
}
|
|
117
185
|
],
|
|
118
186
|
"publishers": [
|
|
119
|
-
{ "id": "1", "isArchived": false, "name": "
|
|
120
|
-
{ "id": "2", "isArchived": false, "name": "
|
|
187
|
+
{ "id": "1", "isArchived": false, "name": "Northlight Studio" },
|
|
188
|
+
{ "id": "2", "isArchived": false, "name": "Aurora Pictures" }
|
|
121
189
|
],
|
|
122
190
|
"users": [
|
|
123
191
|
{
|
|
124
|
-
"bornAt": "
|
|
192
|
+
"bornAt": "1988-03-14",
|
|
125
193
|
"countryId": "1",
|
|
126
|
-
"fullName": "
|
|
194
|
+
"fullName": "Mira Volkova",
|
|
127
195
|
"id": "1",
|
|
128
196
|
"isArchived": false
|
|
129
197
|
},
|
|
130
198
|
{
|
|
131
|
-
"bornAt": "
|
|
132
|
-
"countryId": "
|
|
133
|
-
"fullName": "
|
|
199
|
+
"bornAt": "1991-11-02",
|
|
200
|
+
"countryId": "2",
|
|
201
|
+
"fullName": "Leon Vetrov",
|
|
134
202
|
"id": "2",
|
|
135
203
|
"isArchived": false
|
|
136
204
|
}
|
|
@@ -149,14 +217,14 @@ PATCH /movies/:id
|
|
|
149
217
|
DELETE /movies/:id
|
|
150
218
|
```
|
|
151
219
|
|
|
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
|
|
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.
|
|
153
221
|
|
|
154
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.
|
|
155
223
|
|
|
156
224
|
Successful writes return the created, replaced, updated or deleted record. Errors use an appropriate HTTP status and this JSON shape:
|
|
157
225
|
|
|
158
226
|
```json
|
|
159
|
-
{ "error": "
|
|
227
|
+
{ "error": "..." }
|
|
160
228
|
```
|
|
161
229
|
|
|
162
230
|
## Pagination and sorting
|
|
@@ -165,21 +233,18 @@ Successful writes return the created, replaced, updated or deleted record. Error
|
|
|
165
233
|
GET /movies?_page=1&_perPage=10&_sort=-id,title
|
|
166
234
|
```
|
|
167
235
|
|
|
168
|
-
A GET
|
|
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`:
|
|
169
237
|
|
|
170
238
|
```json
|
|
171
239
|
{
|
|
172
240
|
"data": [],
|
|
173
|
-
"
|
|
174
|
-
"items": 0,
|
|
175
|
-
"last": 1,
|
|
176
|
-
"next": null,
|
|
177
|
-
"pages": 1,
|
|
178
|
-
"prev": null
|
|
241
|
+
"total": 0
|
|
179
242
|
}
|
|
180
243
|
```
|
|
181
244
|
|
|
182
|
-
|
|
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.
|
|
183
248
|
|
|
184
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`.
|
|
185
250
|
|
|
@@ -188,7 +253,7 @@ Both pagination parameters must be positive integers. `_perPage` cannot exceed `
|
|
|
188
253
|
Pass a JSON object through `_where`:
|
|
189
254
|
|
|
190
255
|
```http
|
|
191
|
-
GET /movies?_where={"title":{"contains":"
|
|
256
|
+
GET /movies?_where={"title":{"contains":"ardenia"}}
|
|
192
257
|
```
|
|
193
258
|
|
|
194
259
|
Nested objects and arrays can be filtered at any depth. Conditions in one object use `AND` by default:
|
|
@@ -196,7 +261,7 @@ Nested objects and arrays can be filtered at any depth. Conditions in one object
|
|
|
196
261
|
```json
|
|
197
262
|
{
|
|
198
263
|
"actors": { "some": { "userId": { "eq": "1" } } },
|
|
199
|
-
"title": { "contains": "
|
|
264
|
+
"title": { "contains": "ardenia" }
|
|
200
265
|
}
|
|
201
266
|
```
|
|
202
267
|
|
|
@@ -207,7 +272,7 @@ Use `and`, `or` and `not` for explicit logical groups:
|
|
|
207
272
|
"and": [
|
|
208
273
|
{
|
|
209
274
|
"or": [
|
|
210
|
-
{ "title": { "contains": "
|
|
275
|
+
{ "title": { "contains": "ardenia" } },
|
|
211
276
|
{ "actors": { "some": { "userId": { "eq": "2" } } } }
|
|
212
277
|
]
|
|
213
278
|
},
|
|
@@ -231,12 +296,14 @@ Field operators:
|
|
|
231
296
|
Simple query parameters are supported too:
|
|
232
297
|
|
|
233
298
|
```http
|
|
234
|
-
GET /movies?title:contains=
|
|
299
|
+
GET /movies?title:contains=ardenia
|
|
235
300
|
```
|
|
236
301
|
|
|
237
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`.
|
|
238
303
|
|
|
239
|
-
|
|
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))`.
|
|
240
307
|
|
|
241
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.
|
|
242
309
|
|
|
@@ -248,7 +315,7 @@ Use `_embed` to add related records to the response:
|
|
|
248
315
|
GET /movies/1?_embed=actors.user.country&_embed=actors.genres&_embed=publishers
|
|
249
316
|
```
|
|
250
317
|
|
|
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.
|
|
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:
|
|
252
319
|
|
|
253
320
|
```http
|
|
254
321
|
GET /movies/1?_embed=actors.user.country
|
|
@@ -285,35 +352,66 @@ Add `files.directory` and `files.metadata` to the server config, then pass `--fi
|
|
|
285
352
|
deep-json-server --files server.config.js
|
|
286
353
|
```
|
|
287
354
|
|
|
288
|
-
|
|
355
|
+
For temporary tests, use `files.data` instead. Each initial record contains `name`, `mimeType`, binary `content` as a `Uint8Array`, and an optional `directory`. Uploaded files then remain in memory until the process exits.
|
|
356
|
+
|
|
357
|
+
Upload one file directly as the request body. `Content-Name` contains the URI-encoded file name, `Content-Type` contains its MIME type, and the optional `Content-Directory` contains the URI-encoded relative directory:
|
|
289
358
|
|
|
290
359
|
```http
|
|
291
|
-
POST /_files
|
|
292
|
-
Content-Name:
|
|
360
|
+
POST /_files/storage
|
|
361
|
+
Content-Name: shadows-of-ardenia.jpg
|
|
362
|
+
Content-Directory: posters
|
|
293
363
|
Content-Type: image/jpeg
|
|
294
364
|
|
|
295
365
|
<binary body>
|
|
296
366
|
```
|
|
297
367
|
|
|
298
|
-
|
|
368
|
+
A new file returns status `201` and its computed metadata:
|
|
299
369
|
|
|
300
370
|
```json
|
|
301
371
|
{
|
|
302
|
-
"
|
|
372
|
+
"directory": "posters",
|
|
373
|
+
"downloadUrl": "/_files/download/posters/shadows-of-ardenia.jpg",
|
|
374
|
+
"metadataUrl": "/_files/metadata/posters/shadows-of-ardenia.jpg",
|
|
303
375
|
"mimeType": "image/jpeg",
|
|
304
|
-
"name": "
|
|
376
|
+
"name": "shadows-of-ardenia.jpg",
|
|
305
377
|
"size": 182340,
|
|
306
|
-
"url": "/_files/
|
|
378
|
+
"url": "/_files/storage/posters/shadows-of-ardenia.jpg"
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
The combination of `directory` and `name` identifies a file. Uploading to an existing path returns `409`. Pass `Content-Override: true` to replace it; a successful replacement returns `200`. The server supports these file routes:
|
|
383
|
+
|
|
384
|
+
```text
|
|
385
|
+
POST /_files/storage Upload or replace a file
|
|
386
|
+
GET /_files/storage/* Return file contents inline
|
|
387
|
+
PATCH /_files/storage/* Rename or move a file
|
|
388
|
+
DELETE /_files/storage/* Delete a file
|
|
389
|
+
|
|
390
|
+
GET /_files/metadata/* Return file metadata as JSON
|
|
391
|
+
GET /_files/download/* Download a file as an attachment
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Rename, move, or perform both operations with a JSON body. At least one field is required:
|
|
395
|
+
|
|
396
|
+
```http
|
|
397
|
+
PATCH /_files/storage/posters/shadows-of-ardenia.jpg
|
|
398
|
+
Content-Type: application/json
|
|
399
|
+
|
|
400
|
+
{
|
|
401
|
+
"directory": "archive/posters",
|
|
402
|
+
"name": "ardenia-shadows.jpg"
|
|
307
403
|
}
|
|
308
404
|
```
|
|
309
405
|
|
|
310
|
-
|
|
406
|
+
`PATCH` returns the updated metadata with status `200`; a conflicting destination returns `409`. `DELETE` returns `204` without a response body. A missing file returns `404` on every path-based operation. File paths in URLs are relative to `files.directory`, and all returned URLs are relative to the mock-server origin.
|
|
407
|
+
|
|
408
|
+
In disk mode, the binary is stored at `<files.directory>/<directory>/<name>`. The metadata file contains only `directory`, `mimeType`, and `name`; `size` is read from the actual file, while response URLs are computed. The server creates directories automatically. The metadata file may be absent initially and is created on the first upload. Do not edit stored files or metadata while the server is running. Metadata created by versions before this path-based API is not compatible with the new format.
|
|
311
409
|
|
|
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
|
|
410
|
+
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`. Missing or unsafe headers and paths return `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.
|
|
313
411
|
|
|
314
412
|
## Database schema and OpenAPI generation
|
|
315
413
|
|
|
316
|
-
The optional
|
|
414
|
+
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:
|
|
317
415
|
|
|
318
416
|
```json
|
|
319
417
|
{
|
|
@@ -349,12 +447,16 @@ Schema configuration:
|
|
|
349
447
|
| `$schema.<resource>.formats` | OpenAPI formats for inferred or explicit string fields, such as `date`, `date-time` or `uri` |
|
|
350
448
|
| `$schema.<resource>.properties` | Recursive OpenAPI-compatible field schemas merged with inference |
|
|
351
449
|
|
|
450
|
+
`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.
|
|
451
|
+
|
|
352
452
|
Set `openapi.path` in the server config, then generate an OpenAPI 3.0.3 file and exit:
|
|
353
453
|
|
|
354
454
|
```bash
|
|
355
|
-
deep-json-server --openapi
|
|
455
|
+
deep-json-server --openapi-only server.config.js
|
|
356
456
|
```
|
|
357
457
|
|
|
458
|
+
To include file routes in the document, configure the `files` section and add `--files`: `deep-json-server --files --openapi-only server.config.js`.
|
|
459
|
+
|
|
358
460
|
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.
|
|
359
461
|
|
|
360
462
|
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.
|
|
@@ -377,7 +479,7 @@ Use `properties` to describe fields that cannot be inferred, particularly for an
|
|
|
377
479
|
|
|
378
480
|
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.
|
|
379
481
|
|
|
380
|
-
`$info` becomes the OpenAPI `info` object, while resource settings live under `$schema`.
|
|
482
|
+
`$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.
|
|
381
483
|
|
|
382
484
|
Use `name` when a resource needs an explicit schema name instead of the automatically singularized name:
|
|
383
485
|
|
|
@@ -391,73 +493,87 @@ Use `name` when a resource needs an explicit schema name instead of the automati
|
|
|
391
493
|
}
|
|
392
494
|
```
|
|
393
495
|
|
|
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
|
|
496
|
+
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.
|
|
395
497
|
|
|
396
498
|
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
499
|
|
|
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.
|
|
401
|
-
|
|
402
500
|
## Programmatic API
|
|
403
501
|
|
|
404
502
|
```js
|
|
405
|
-
import {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
503
|
+
import { createServer } from '@kollors/deep-json-server';
|
|
504
|
+
|
|
505
|
+
const config = {
|
|
506
|
+
database: {
|
|
507
|
+
path: 'mock/database.json',
|
|
508
|
+
schema: 'mock/database-schema.json',
|
|
509
|
+
},
|
|
510
|
+
files: {
|
|
511
|
+
directory: 'mock/files',
|
|
512
|
+
metadata: 'mock/files/_database.json',
|
|
513
|
+
},
|
|
514
|
+
openapi: {
|
|
515
|
+
path: 'mock/openapi-schema.yaml',
|
|
516
|
+
},
|
|
517
|
+
server: {
|
|
518
|
+
host: '127.0.0.1',
|
|
519
|
+
logger: false,
|
|
520
|
+
maxFileSize: 100 * 1024 * 1024,
|
|
521
|
+
maxPageSize: 1000,
|
|
522
|
+
port: 4001,
|
|
523
|
+
},
|
|
524
|
+
};
|
|
417
525
|
|
|
418
|
-
|
|
526
|
+
// Make a request without opening a network port—useful in automated tests.
|
|
527
|
+
const server = await createServer(config);
|
|
528
|
+
const fastify = server.fastify();
|
|
529
|
+
const response = await fastify.inject({ method: 'GET', url: '/movies' });
|
|
419
530
|
|
|
420
|
-
|
|
531
|
+
console.log(response.json());
|
|
421
532
|
|
|
422
|
-
//
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
533
|
+
// Return the document and write it to config.openapi.path.
|
|
534
|
+
const document = await server.openapi();
|
|
535
|
+
|
|
536
|
+
await fastify.close();
|
|
537
|
+
|
|
538
|
+
// Start a network server. With no arguments, listen uses server.host and server.port.
|
|
539
|
+
const runningServer = await createServer(config);
|
|
540
|
+
const runningFastify = runningServer.fastify();
|
|
541
|
+
|
|
542
|
+
await runningFastify.listen();
|
|
429
543
|
|
|
430
|
-
|
|
544
|
+
// Later, during application shutdown:
|
|
545
|
+
await runningFastify.close();
|
|
431
546
|
|
|
432
|
-
//
|
|
433
|
-
await
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
547
|
+
// Keep the database, schema, and files entirely in memory.
|
|
548
|
+
const memoryServer = await createServer({
|
|
549
|
+
database: {
|
|
550
|
+
data: { movies: [{ id: '1', title: 'Shadows of Ardenia' }] },
|
|
551
|
+
schema: { $info: { title: 'Movie API', version: '1.0.0' } },
|
|
552
|
+
},
|
|
553
|
+
files: {
|
|
554
|
+
data: [{ content: new Uint8Array([1, 2, 3]), directory: 'examples', mimeType: 'application/octet-stream', name: 'example.bin' }],
|
|
555
|
+
},
|
|
440
556
|
});
|
|
441
557
|
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
);
|
|
558
|
+
const memoryFastify = memoryServer.fastify();
|
|
559
|
+
const memoryResponse = await memoryFastify.inject({ method: 'GET', url: '/movies/1' });
|
|
560
|
+
|
|
561
|
+
console.log(memoryResponse.json());
|
|
562
|
+
|
|
563
|
+
await memoryFastify.close();
|
|
448
564
|
```
|
|
449
565
|
|
|
450
|
-
|
|
566
|
+
`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:
|
|
451
567
|
|
|
452
|
-
|
|
|
568
|
+
| Member | Meaning |
|
|
453
569
|
| --- | --- | --- |
|
|
454
|
-
| `
|
|
455
|
-
| `
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
570
|
+
| `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` |
|
|
571
|
+
| `server.openapi()` | Returns an OpenAPI document and also writes it when `openapi.path` is configured |
|
|
572
|
+
|
|
573
|
+
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()`.
|
|
574
|
+
|
|
575
|
+
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.
|
|
576
|
+
|
|
577
|
+
## Scope and security
|
|
578
|
+
|
|
579
|
+
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.
|