@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 +317 -67
- package/README.ru.md +316 -66
- package/index.js +7 -2
- package/package.json +1 -1
- package/src/cli.js +61 -48
- package/src/config.js +194 -0
- package/src/constants.js +1 -0
- package/src/database.js +31 -2
- package/src/files.js +368 -0
- package/src/openapi/config.js +15 -1
- package/src/openapi/document.js +69 -39
- package/src/openapi/index.js +29 -18
- package/src/query/pagination.js +1 -7
- package/src/server.js +91 -58
- package/types/index.d.ts +11 -2
- package/types/src/config.d.ts +86 -0
- package/types/src/constants.d.ts +1 -0
- package/types/src/database.d.ts +5 -2
- package/types/src/files.d.ts +24 -0
- package/types/src/openapi/config.d.ts +1 -1
- package/types/src/openapi/document.d.ts +7 -7
- package/types/src/openapi/index.d.ts +9 -13
- package/types/src/query/pagination.d.ts +1 -6
- package/types/src/server.d.ts +11 -22
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,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
|
-
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
Create the database file `mock/database.json` before startup:
|
|
18
20
|
|
|
19
21
|
```json
|
|
20
22
|
{
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
}
|
|
23
|
+
"movies": [
|
|
24
|
+
{ "id": "1", "title": "Shadows of Ardenia" }
|
|
25
|
+
]
|
|
25
26
|
}
|
|
26
27
|
```
|
|
27
28
|
|
|
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:
|
|
29
40
|
|
|
30
41
|
```bash
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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": "
|
|
44
|
-
{ "id": "2", "isArchived": false, "name": "
|
|
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
|
|
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://
|
|
59
|
-
"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.",
|
|
60
171
|
"id": "1",
|
|
61
172
|
"isArchived": false,
|
|
62
173
|
"publisherIds": ["2"],
|
|
63
|
-
"title": "
|
|
174
|
+
"title": "Shadows of Ardenia"
|
|
64
175
|
},
|
|
65
176
|
{
|
|
66
177
|
"actors": [],
|
|
67
|
-
"coverSrc": "https://
|
|
68
|
-
"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.",
|
|
69
180
|
"id": "2",
|
|
70
181
|
"isArchived": false,
|
|
71
182
|
"publisherIds": ["1"],
|
|
72
|
-
"title": "
|
|
183
|
+
"title": "Midnight at the Northern Star"
|
|
73
184
|
}
|
|
74
185
|
],
|
|
75
186
|
"publishers": [
|
|
76
|
-
{ "id": "1", "isArchived": false, "name": "
|
|
77
|
-
{ "id": "2", "isArchived": false, "name": "
|
|
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": "
|
|
192
|
+
"bornAt": "1988-03-14",
|
|
82
193
|
"countryId": "1",
|
|
83
|
-
"fullName": "
|
|
194
|
+
"fullName": "Mira Volkova",
|
|
84
195
|
"id": "1",
|
|
85
196
|
"isArchived": false
|
|
86
197
|
},
|
|
87
198
|
{
|
|
88
|
-
"bornAt": "
|
|
89
|
-
"countryId": "
|
|
90
|
-
"fullName": "
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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":"
|
|
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": "
|
|
264
|
+
"title": { "contains": "ardenia" }
|
|
151
265
|
}
|
|
152
266
|
```
|
|
153
267
|
|
|
154
|
-
|
|
268
|
+
Use `and`, `or` and `not` for explicit logical groups:
|
|
155
269
|
|
|
156
270
|
```json
|
|
157
271
|
{
|
|
158
|
-
"
|
|
159
|
-
{
|
|
160
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
426
|
+
deep-json-server --openapi-only server.config.js
|
|
242
427
|
```
|
|
243
428
|
|
|
244
|
-
|
|
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`.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
502
|
+
console.log(response.json());
|
|
290
503
|
|
|
291
|
-
|
|
504
|
+
// Return the document and write it to config.openapi.path.
|
|
505
|
+
const document = await server.openapi();
|
|
292
506
|
|
|
293
|
-
await
|
|
507
|
+
await fastify.close();
|
|
294
508
|
|
|
295
|
-
|
|
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
|
|
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()`
|
|
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.
|