@kollors/deep-json-server 0.9.0 → 1.0.0-alpha.1

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.
Files changed (67) hide show
  1. package/README.md +286 -410
  2. package/README.ru.md +278 -404
  3. package/dist/index.d.ts +2 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/cli.js +50 -63
  6. package/dist/src/cli.js.map +1 -1
  7. package/dist/src/config.d.ts +16 -2
  8. package/dist/src/config.js +24 -8
  9. package/dist/src/config.js.map +1 -1
  10. package/dist/src/constants.d.ts +1 -1
  11. package/dist/src/constants.js +1 -1
  12. package/dist/src/constants.js.map +1 -1
  13. package/dist/src/database.d.ts +4 -3
  14. package/dist/src/database.js +32 -19
  15. package/dist/src/database.js.map +1 -1
  16. package/dist/src/engine.d.ts +47 -0
  17. package/dist/src/engine.js +438 -0
  18. package/dist/src/engine.js.map +1 -0
  19. package/dist/src/files/disk-store.js +13 -2
  20. package/dist/src/files/disk-store.js.map +1 -1
  21. package/dist/src/files/openapi.d.ts +3 -0
  22. package/dist/src/files/openapi.js +86 -0
  23. package/dist/src/files/openapi.js.map +1 -0
  24. package/dist/src/graphql.d.ts +4 -0
  25. package/dist/src/graphql.js +223 -0
  26. package/dist/src/graphql.js.map +1 -0
  27. package/dist/src/model.d.ts +67 -0
  28. package/dist/src/model.js +319 -0
  29. package/dist/src/model.js.map +1 -0
  30. package/dist/src/openapi/document.d.ts +7 -7
  31. package/dist/src/openapi/document.js +234 -320
  32. package/dist/src/openapi/document.js.map +1 -1
  33. package/dist/src/openapi/index.d.ts +3 -6
  34. package/dist/src/openapi/index.js +2 -3
  35. package/dist/src/openapi/index.js.map +1 -1
  36. package/dist/src/query/filter.d.ts +0 -5
  37. package/dist/src/query/filter.js +6 -170
  38. package/dist/src/query/filter.js.map +1 -1
  39. package/dist/src/query/options.d.ts +31 -0
  40. package/dist/src/query/options.js +135 -0
  41. package/dist/src/query/options.js.map +1 -0
  42. package/dist/src/server.d.ts +4 -3
  43. package/dist/src/server.js +113 -135
  44. package/dist/src/server.js.map +1 -1
  45. package/dist/src/types.d.ts +1 -3
  46. package/dist/src/utils.d.ts +0 -1
  47. package/dist/src/utils.js +0 -1
  48. package/dist/src/utils.js.map +1 -1
  49. package/package.json +9 -4
  50. package/dist/src/openapi/config.d.ts +0 -11
  51. package/dist/src/openapi/config.js +0 -204
  52. package/dist/src/openapi/config.js.map +0 -1
  53. package/dist/src/openapi/inference.d.ts +0 -14
  54. package/dist/src/openapi/inference.js +0 -145
  55. package/dist/src/openapi/inference.js.map +0 -1
  56. package/dist/src/query/index.d.ts +0 -3
  57. package/dist/src/query/index.js +0 -4
  58. package/dist/src/query/index.js.map +0 -1
  59. package/dist/src/query/pagination.d.ts +0 -11
  60. package/dist/src/query/pagination.js +0 -28
  61. package/dist/src/query/pagination.js.map +0 -1
  62. package/dist/src/query/sort.d.ts +0 -1
  63. package/dist/src/query/sort.js +0 -54
  64. package/dist/src/query/sort.js.map +0 -1
  65. package/dist/src/relations.d.ts +0 -12
  66. package/dist/src/relations.js +0 -155
  67. package/dist/src/relations.js.map +0 -1
package/README.md CHANGED
@@ -1,206 +1,359 @@
1
1
  # Deep JSON Server
2
2
 
3
- [English](README.md) | [Русский](README.ru.md)
3
+ [Русский](README.ru.md)
4
4
 
5
- [GitHub](https://github.com/kollors/deep-json-server) | [npm](https://www.npmjs.com/package/@kollors/deep-json-server)
5
+ A JSON-backed mock server with REST, GraphQL, nested queries, binary files and schema exports. Requires Node.js 22 or newer.
6
6
 
7
- A small REST API mock server with CRUD, pagination, nested data filtering, relation embedding through `_embed`, binary files, and OpenAPI generation. Data can be stored in JSON files or memory, and relations are inferred from field names such as `countryId`, `genreIds`, and `publisherIds`.
7
+ **1.0.0-alpha.1 is a breaking prerelease.** The old `$schema`/`$info` model format and `_where`, `_sort`, `_embed`, `_page`, `_perPage` query parameters are no longer supported.
8
8
 
9
9
  ## Installation
10
10
 
11
- Node.js 22 or newer is required.
12
-
13
- ```bash
14
- npm install --save-dev @kollors/deep-json-server
11
+ ```sh
12
+ npm install @kollors/deep-json-server@alpha
15
13
  ```
16
14
 
15
+ The `alpha` npm channel is separate from `latest`. Install an exact version with `@1.0.0-alpha.1`.
16
+
17
17
  ## Quick start
18
18
 
19
- Create the database file `mock/database.json` before startup:
19
+ `server.config.js`:
20
+
21
+ ```js
22
+ export default {
23
+ database: { path: './database.json', schema: './schema.json' },
24
+ graphql: { enabled: true, path: './generated/schema.graphql' },
25
+ openapi: { path: './generated/openapi.yaml' },
26
+ server: { host: '127.0.0.1', port: 4001, pageSize: 10, maxPageSize: 100 },
27
+ };
28
+ ```
29
+
30
+ ```sh
31
+ npx deep-json-server server.config.js
32
+ npx deep-json-server --openapi-only --graphql-only server.config.js
33
+ ```
34
+
35
+ The second command exports both schemas without listening on a port. Starting the server alone does not write schema files.
36
+
37
+ Complete catalog examples: [database](examples/database.json), [model schema](examples/schema.json), [configuration](examples/server.config.js).
38
+
39
+ ## Configuration
40
+
41
+ | Setting | Meaning |
42
+ |---|---|
43
+ | `database.path` / `database.data` | Exactly one: JSON file or in-memory collection object |
44
+ | `database.schema` | Model object or JSON schema-file path; optional for REST |
45
+ | `openapi.path` | YAML export destination |
46
+ | `openapi.info` | Optional `title`, `version`, `description` |
47
+ | `graphql.enabled` | Enable GraphQL HTTP endpoint; default `false` |
48
+ | `graphql.endpoint` | Endpoint path; default `/graphql` |
49
+ | `graphql.path` | GraphQL SDL export destination |
50
+ | `server.host`, `server.port` | Defaults `127.0.0.1`, `4001`; CLI also reads `HOST`/`PORT` |
51
+ | `server.pageSize`, `server.maxPageSize` | Defaults 10 and 100; default size is capped by the maximum |
52
+ | `server.cors`, `server.logger` | Default `true`; logger also accepts Fastify logger options |
53
+ | `server.maxFileSize` | Default 100 MiB |
54
+ | `files.data` | In-memory binary files |
55
+ | `files.directory`, `files.metadata` | Disk storage directory and metadata JSON file; both required |
56
+
57
+ Configuration-file paths resolve relative to that file. Direct `createServer()` paths resolve relative to the working directory. In-memory input is copied.
58
+
59
+ CLI flags:
60
+
61
+ | Flag | Action |
62
+ |---|---|
63
+ | `--files` | Enable binary-file routes |
64
+ | `--graphql` | Enable GraphQL endpoint |
65
+ | `--openapi` | Export OpenAPI and start |
66
+ | `--openapi-only` | Export OpenAPI without starting |
67
+ | `--graphql-schema` | Export GraphQL SDL and start |
68
+ | `--graphql-only` | Export GraphQL SDL without starting |
69
+ | `--help` | Show usage |
70
+
71
+ Both exporters can be combined. Any `--*-only` flag prevents startup. CLI file routes require `--files`; programmatic use enables them when `files` is configured unless overridden through the second `createServer()` argument.
72
+
73
+ ## Model schema
20
74
 
21
75
  ```json
22
76
  {
23
- "movies": [
24
- { "id": "1", "title": "Shadows of Ardenia" }
25
- ]
77
+ "Country": {
78
+ "collection": "countries",
79
+ "api": ["openapi", "graphql"],
80
+ "fields": {
81
+ "id": { "type": "string", "primary": true, "generated": "uuid" },
82
+ "name": { "type": "string", "required": true },
83
+ "users": { "type": "User[]", "target": "countryId" }
84
+ }
85
+ },
86
+ "User": {
87
+ "collection": "users",
88
+ "api": ["openapi", "graphql"],
89
+ "fields": {
90
+ "id": { "type": "string", "primary": true, "generated": "uuid" },
91
+ "fullName": { "type": "string", "required": true },
92
+ "country": { "type": "Country", "source": "countryId" }
93
+ }
94
+ }
26
95
  }
27
96
  ```
28
97
 
29
- Create the ESM module `server.config.js` next to `package.json`:
98
+ `api` defaults to `["openapi", "graphql"]`. `[]` excludes the model from both exports and GraphQL, while REST remains available. Related models must enable the same export format. Names must be valid identifiers and generated type/operation names must not collide. `and`, `or`, `not` are reserved filter names.
30
99
 
31
- ```js
32
- export default {
33
- database: {
34
- path: 'mock/database.json',
35
- },
36
- };
100
+ | Capability | With schema | Without schema |
101
+ |---|---|---|
102
+ | REST CRUD | Model validation | JSON/body/identifier validation only |
103
+ | Relations | Explicit model fields | `countryId`, `genreIds`, etc. naming conventions |
104
+ | OpenAPI 3.0.3 export | Available | Error when requested |
105
+ | GraphQL SDL / API | Available | Error when requested |
106
+
107
+ Explicit schemas are strict: undeclared fields and collections are rejected, except storage keys inferred from relations. Existing data is validated on startup. Schemas can be exported for empty collections or an empty database object. REST without a schema retains the standard generated `id` behavior.
108
+
109
+ ### Fields
110
+
111
+ Types: `string`, `number`, `boolean`, `object`, or a model name. Append `[]` for an array. No `integer`, `relation`, `items`, or multidimensional type strings. Nested fields use full dotted paths, for example `actors.fullName`. Objects inside arrays may contain their own arrays.
112
+
113
+ | Properties | Meaning |
114
+ |---|---|
115
+ | `type` | Required type |
116
+ | `description`, `example` | Documentation and example value |
117
+ | `required`, `nullable` | Defaults `false`; presence and explicit null are separate |
118
+ | `default` | Value when omitted on create/replace; PATCH does not insert defaults |
119
+ | `enum` | Allowed values; for arrays, allowed element values |
120
+ | `primary` | Root primary key; mandatory, unique, non-null and immutable |
121
+ | `generated` | `uuid` for strings, `increment` for numbers; server supplies the value |
122
+ | `readOnly`, `writeOnly` | Output-only or input-only; mutually exclusive |
123
+ | `minLength`, `maxLength`, `pattern` | String constraints |
124
+ | `format` | `date`, `date-time`, `email`, `uri`, `uuid` |
125
+ | `minimum`, `maximum` | Inclusive numeric bounds |
126
+ | `source`, `target`, `onDelete` | Relation metadata |
127
+
128
+ String/numeric constraints on `string[]`/`number[]` apply to every element. `required`/`nullable` apply to the entire array, and `default`/`example` contain a complete array. Array elements are non-null. There are no item-count or uniqueness constraints. A required ordinary array may be empty.
129
+
130
+ Primary keys can be named `username`, `code`, etc.; exactly one root string/number primary key is required. Without `generated`, the client supplies it during creation. Generated fields are root fields, absent from create/replace/update input; they cannot have `default`. Replace preserves generated and read-only root values.
131
+
132
+ For example, a `LocalUser` with primary `username` and `password: {"type":"string","required":true,"writeOnly":true}` has `localUser(username: ...)` and `/localUsers/{username}`. `writeOnly` excludes passwords from responses, scope, filters and ordering. It does not implement hashing or authentication.
133
+
134
+ ### Relations
135
+
136
+ ```json
137
+ "actors.genres": {
138
+ "type": "Genre[]",
139
+ "source": "actors.genreIds",
140
+ "required": true
141
+ }
37
142
  ```
38
143
 
39
- Start the server:
144
+ `Genre` produces an object; `Genre[]` produces a list. `source` defaults to the current model's primary key, `target` to the target model's primary key. Both paths are rooted at their respective records. Within `actors`, `actors.genreIds` reads the current actor's IDs. An omitted `source` still means the root model key, not `actors.id`.
40
145
 
41
- ```bash
42
- npx deep-json-server server.config.js
146
+ Storage keys remain in the database and are included among own fields. Their types can be inferred from the target key. For an undeclared source pointing to a target primary key, a list relation implies an array of keys; a single relation implies a scalar key. Declare storage fields explicitly when the mapping is ambiguous. Generation never guesses from the first database record.
147
+
148
+ Reverse example: `User.movies = {"type":"Movie[]","target":"actors.userId"}`. A movie is returned once even if several actors match. A single relation resolving to multiple targets is invalid.
149
+
150
+ Every supplied direct reference must resolve. `required: true` on a relation requires at least one target before response filtering/pagination. Reverse views with a primary source may be empty unless required. Missing single relations return `null`.
151
+
152
+ `onDelete` describes what happens **when a target record is deleted**:
153
+
154
+ - `restrict` (default): refuse deletion while a surviving owner refers to the target.
155
+ - `cascade`: delete the referring owner. For `User.country`, deleting the country deletes its users. For `Movie.actors.user`, deleting the user removes matching actor elements and retains the movie.
156
+
157
+ Deletion computes the cascade closure, handles cycles, checks restrictions and validates remaining data before committing. A failure cancels the complete operation. Policies also apply to explicitly declared reverse relation views; configure both directions deliberately when both are present.
158
+
159
+ ## Queries and responses
160
+
161
+ Collections and lists of objects, including embedded `object[]` fields, return:
162
+
163
+ ```json
164
+ { "data": [], "total": 0 }
43
165
  ```
44
166
 
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:
167
+ Primitive arrays remain plain arrays. Every object list accepts optional `where`, `order`, `pager`. Processing order is filter sort → pagination. `total` is the filtered count before pagination. Page numbers start at 1; default pagination applies even when omitted. Exceeding `maxPageSize`, fractional values and nonpositive values are errors. Out-of-range pages return empty `data` with the correct `total`.
168
+
169
+ `where` uses field operators `eq`, `ne`, `in`, string `contains`/`startsWith`/`endsWith`, and comparisons `gt`, `gte`, `lt`, `lte`. Logical composition uses `and`, `or`, `not`. Arrays support `some`, `every`, `none`; primitive arrays also support `contains`, `in`. String matching is case-insensitive. Field filters use operator objects, not shorthand scalar values.
46
170
 
47
171
  ```json
48
172
  {
49
- "data": [{ "id": "1", "title": "Shadows of Ardenia" }],
50
- "total": 1
173
+ "movies": {
174
+ "some": {
175
+ "actors": {
176
+ "some": {
177
+ "genres": { "some": { "id": { "in": ["2", "3"] } } }
178
+ }
179
+ }
180
+ }
181
+ }
51
182
  }
52
183
  ```
53
184
 
54
- ## Configuration and startup
185
+ Root filters choose parents. Filters inside a selected relation only trim that relation; they do not remove the parent. Each parent's child list is processed independently. Filtering does not require a relation to be included in the response.
55
186
 
56
- Create the ESM module `server.config.js`. The example below shows settings for every feature:
57
-
58
- ```js
59
- import process from 'node:process';
187
+ `order` is an array of `{ "field": "fullName", "direction": "ASC" }` rules. Earlier rules have priority; complete ties retain storage order. Null and missing values compare equally. REST uses dotted field paths; GraphQL uses generated enums (`profile_name` for `profile.name`). Ambiguous enum names cause a generation error. Sorting parents by a relation or an array is unsupported; sorting inside the relation is supported.
60
188
 
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
- cors: true,
75
- host: '127.0.0.1',
76
- logger: true,
77
- maxFileSize: 100 * 1024 * 1024,
78
- maxPageSize: 1000,
79
- port: 4001,
80
- },
81
- };
82
- ```
189
+ ### REST
83
190
 
84
- Configuration keys:
191
+ | Method | Path | Operation |
192
+ |---|---|---|
193
+ | GET | `/users` | `userList` |
194
+ | GET | `/users/{id}` | `user` |
195
+ | POST | `/users` | `userCreate` |
196
+ | PUT | `/users/{id}` | `userReplace` |
197
+ | PATCH | `/users/{id}` | `userUpdate` |
198
+ | DELETE | `/users/{id}` | `userDelete` |
85
199
 
86
- | Key | Condition | Purpose |
87
- | --- | --- | --- |
88
- | `database.path` | Exactly one of `path` or `data` is required | Existing JSON database file |
89
- | `database.data` | Exactly one of `path` or `data` is required | Database object stored in memory |
90
- | `database.schema` | No | Path to JSON overrides or an object with request-validation and OpenAPI settings |
91
- | `files.directory` | Together with `files.metadata` | Directory for binary contents on disk |
92
- | `files.metadata` | Together with `files.directory` | JSON file containing file metadata on disk |
93
- | `files.data` | Instead of the `directory` and `metadata` pair | In-memory files with `Uint8Array` contents |
94
- | `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 |
95
- | `server.cors` | No | Enables permissive CORS headers and `OPTIONS` routes; defaults to `true` |
96
- | `server.host` | No | Host used by the CLI, `server.openapi()`, and argument-less `server.fastify().listen()`; defaults to `127.0.0.1` |
97
- | `server.logger` | No | Fastify logger settings; defaults to `true` |
98
- | `server.maxFileSize` | No | Maximum uploaded-file size in bytes when file routes are enabled; defaults to 100 MiB |
99
- | `server.maxPageSize` | No | Maximum allowed `_perPage` in the API and OpenAPI; defaults to `1000` |
100
- | `server.port` | No | Port used by the CLI, `server.openapi()`, and argument-less `server.fastify().listen()`; defaults to `4001` |
200
+ The path key name follows the primary key. POST/PUT/PATCH receive raw record objects. PUT replaces the record while retaining its key and server-owned root values. PATCH shallowly merges supplied fields; supplied nested objects are full replacements. Create/replace enforce required fields. Update validates supplied values and the final record. Missing targets return 404; conflicts return 409. DELETE returns the deleted record.
101
201
 
102
- `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.
202
+ Query parameters `where`, `order`, `pager`, `nested` contain JSON. `scope` is a selection string. Example shown before URL encoding:
103
203
 
104
- 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`.
204
+ ```text
205
+ GET /users?where={"fullName":{"contains":"Мира"}}&order=[{"field":"fullName","direction":"ASC"}]&pager={"page":1,"pageSize":20}
206
+ ```
105
207
 
106
- 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`:
208
+ Construct encoded URLs with `URLSearchParams`:
107
209
 
108
210
  ```js
109
- export default {
110
- database: {
111
- data: { movies: [{ id: '1', title: 'Shadows of Ardenia' }] },
112
- schema: { $info: { title: 'Movie API', version: '1.0.0' } },
113
- },
114
- files: {
115
- data: [{ content: new Uint8Array([1, 2, 3]), directory: 'examples', mimeType: 'application/octet-stream', name: 'example.bin' }],
116
- },
117
- };
211
+ const params = new URLSearchParams({
212
+ scope: 'id,fullName,movies(id,title)',
213
+ nested: JSON.stringify({ movies: { order: [{ field: 'title', direction: 'ASC' }], pager: { page: 1, pageSize: 5 } } }),
214
+ });
215
+ const response = await fetch(`/users?${params}`);
118
216
  ```
119
217
 
120
- 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.
218
+ `scope=*,actors(user(id,fullName),genres(*))` selects own fields and explicit relations. `*` selects only own fields of the current object, including inferred storage keys and excluding `writeOnly`. It never recursively expands relations. Without scope, own fields are selected. Wrappers `data`/`total` remain present.
121
219
 
122
- Add the commands you need to `package.json`. Here, `mock:openapi:files` updates OpenAPI first and then keeps the server running with file routes:
220
+ `nested` maps full response paths to list options:
123
221
 
124
222
  ```json
125
223
  {
126
- "scripts": {
127
- "mock": "deep-json-server server.config.js",
128
- "mock:files": "deep-json-server --files server.config.js",
129
- "mock:openapi:files": "deep-json-server --files --openapi server.config.js",
130
- "openapi": "deep-json-server --openapi-only server.config.js",
131
- "openapi:files": "deep-json-server --files --openapi-only server.config.js"
224
+ "actors": { "pager": { "pageSize": 5 } },
225
+ "actors.genres": {
226
+ "where": { "id": { "in": ["2", "3"] } },
227
+ "order": [{ "field": "name", "direction": "ASC" }]
132
228
  }
133
229
  }
134
230
  ```
135
231
 
136
- CLI modes:
232
+ A nested path must be selected by scope and must address an object list. Single-record routes and mutations accept `scope` and `nested`; root list parameters only apply to collection GET. Invalid names and unsafe paths return 400.
233
+
234
+ ### GraphQL
235
+
236
+ ```graphql
237
+ query {
238
+ userList(
239
+ order: [{ field: fullName, direction: ASC }]
240
+ pager: { page: 1, pageSize: 20 }
241
+ ) {
242
+ total
243
+ data {
244
+ id
245
+ fullName
246
+ movies(order: [{ field: title, direction: ASC }], pager: { pageSize: 5 }) {
247
+ total
248
+ data { id title }
249
+ }
250
+ }
251
+ }
252
+ }
253
+ ```
137
254
 
138
- | Command | Behavior |
139
- | --- | --- |
140
- | `deep-json-server server.config.js` | Starts the CRUD server without file routes |
141
- | `deep-json-server --files server.config.js` | Starts the CRUD server with file routes |
142
- | `deep-json-server --openapi server.config.js` | Generates OpenAPI and starts the CRUD server |
143
- | `deep-json-server --files --openapi server.config.js` | Generates OpenAPI with file routes and starts the server with them |
144
- | `deep-json-server --openapi-only server.config.js` | Generates OpenAPI and exits |
145
- | `deep-json-server --files --openapi-only server.config.js` | Generates OpenAPI with file routes and exits |
255
+ Single queries are `user(id: ...)`, with no `ById`; missing records yield null. Mutation names are `userCreate(data: ...)`, `userReplace(id: ..., data: ...)`, `userUpdate(id: ..., data: ...)`, `userDelete(id: ...)`. A generated-only model creates records without a `data` argument. Mutations use the same validation and storage operations as REST. Selecting relations in mutation results shapes the response only.
146
256
 
147
- `--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.
257
+ String primary keys use GraphQL `ID`; ordinary strings use `String`, numbers use `Float`, pagination uses `Int`. Schema enums preserve valid string labels; other values receive `VALUE_0`, `VALUE_1`, etc. Schema constraints such as string length and formats are enforced by the shared runtime validator; SDL alone cannot express all constraints. Introspection and ordinary query/mutation execution are supported; this release does not add subscriptions or bulk mutations.
148
258
 
149
- ## Example database
150
259
 
151
- Below is an example movie catalog with sample data. `Gangster` is linked to its parent genre, `Crime`.
260
+ ## Example database
152
261
 
153
262
  ```json
154
263
  {
155
264
  "countries": [
156
- { "id": "1", "isArchived": false, "name": "Ardenia" },
157
- { "id": "2", "isArchived": false, "name": "Veloria" }
265
+ {
266
+ "id": "1",
267
+ "isArchived": false,
268
+ "name": "Ардения"
269
+ },
270
+ {
271
+ "id": "2",
272
+ "isArchived": false,
273
+ "name": "Велория"
274
+ }
158
275
  ],
159
276
  "genres": [
160
- { "id": "1", "isArchived": false, "name": "Crime", "parentIds": [] },
161
- { "id": "2", "isArchived": false, "name": "Gangster", "parentIds": ["1"] },
162
- { "id": "3", "isArchived": false, "name": "Drama", "parentIds": [] },
163
- { "id": "4", "isArchived": false, "name": "Comedy", "parentIds": [] }
277
+ {
278
+ "id": "1",
279
+ "isArchived": false,
280
+ "name": "Криминал",
281
+ "parentIds": []
282
+ },
283
+ {
284
+ "id": "2",
285
+ "isArchived": false,
286
+ "name": "Гангстер",
287
+ "parentIds": ["1"]
288
+ },
289
+ {
290
+ "id": "3",
291
+ "isArchived": false,
292
+ "name": "Драма",
293
+ "parentIds": []
294
+ },
295
+ {
296
+ "id": "4",
297
+ "isArchived": false,
298
+ "name": "Комедия",
299
+ "parentIds": []
300
+ }
164
301
  ],
165
302
  "movies": [
166
303
  {
167
304
  "actors": [
168
- { "genreIds": ["2", "3"], "id": "movie-1-actor-1", "userId": "1" },
169
- { "genreIds": ["3"], "id": "movie-1-actor-2", "userId": "2" }
305
+ {
306
+ "genreIds": ["2", "3"],
307
+ "id": "movie-1-actor-1",
308
+ "userId": "1"
309
+ },
310
+ {
311
+ "genreIds": ["3"],
312
+ "id": "movie-1-actor-2",
313
+ "userId": "2"
314
+ }
170
315
  ],
171
316
  "coverSrc": "https://example.com/covers/shadows-of-ardenia.jpg",
172
- "description": "The heir to a port city uncovers a conspiracy between two rival families.",
317
+ "description": "Наследница портового города раскрывает заговор двух соперничающих семей.",
173
318
  "id": "1",
174
319
  "isArchived": false,
175
320
  "publisherIds": ["2"],
176
- "title": "Shadows of Ardenia"
321
+ "title": "Тени Ардении"
177
322
  },
178
323
  {
179
324
  "actors": [],
180
325
  "coverSrc": "https://example.com/covers/northern-star.jpg",
181
- "description": "A night manager at an old hotel is drawn into the search for a missing painting.",
326
+ "description": "Ночной администратор старого отеля случайно становится участником поисков пропавшей картины.",
182
327
  "id": "2",
183
328
  "isArchived": false,
184
329
  "publisherIds": ["1"],
185
- "title": "Midnight at the Northern Star"
330
+ "title": "Полночь в «Северной звезде»"
186
331
  }
187
332
  ],
188
333
  "publishers": [
189
- { "id": "1", "isArchived": false, "name": "Northlight Studio" },
190
- { "id": "2", "isArchived": false, "name": "Aurora Pictures" }
334
+ {
335
+ "id": "1",
336
+ "isArchived": false,
337
+ "name": "Northlight Studio"
338
+ },
339
+ {
340
+ "id": "2",
341
+ "isArchived": false,
342
+ "name": "Aurora Pictures"
343
+ }
191
344
  ],
192
345
  "users": [
193
346
  {
194
347
  "bornAt": "1988-03-14",
195
348
  "countryId": "1",
196
- "fullName": "Mira Volkova",
349
+ "fullName": "Мира Волкова",
197
350
  "id": "1",
198
351
  "isArchived": false
199
352
  },
200
353
  {
201
354
  "bornAt": "1991-11-02",
202
355
  "countryId": "2",
203
- "fullName": "Leon Vetrov",
356
+ "fullName": "Леон Ветров",
204
357
  "id": "2",
205
358
  "isArchived": false
206
359
  }
@@ -208,144 +361,6 @@ Below is an example movie catalog with sample data. `Gangster` is linked to its
208
361
  }
209
362
  ```
210
363
 
211
- Every top-level array becomes a REST resource:
212
-
213
- ```text
214
- GET /movies
215
- GET /movies/:id
216
- POST /movies
217
- PUT /movies/:id
218
- PATCH /movies/:id
219
- DELETE /movies/:id
220
- ```
221
-
222
- `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.
223
-
224
- 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. All nested values must be JSON-compatible: finite numbers, strings, booleans, `null`, arrays, and plain objects. The server rereads the file before every resource GET and write operation, so valid edits to existing resources become visible immediately. Resource names and routes are discovered at startup; restart the server after adding, removing, or renaming a top-level resource.
225
-
226
- Successful writes return the created, replaced, updated or deleted record. Errors use an appropriate HTTP status and this JSON shape:
227
-
228
- ```json
229
- { "error": "..." }
230
- ```
231
-
232
- ## Pagination and sorting
233
-
234
- ```http
235
- GET /movies?_page=1&_perPage=10&_sort=-id,title
236
- ```
237
-
238
- 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`:
239
-
240
- ```json
241
- {
242
- "data": [],
243
- "total": 0
244
- }
245
- ```
246
-
247
- `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))`.
248
-
249
- 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.
250
-
251
- `_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`.
252
-
253
- ## Filters
254
-
255
- Pass a JSON object through `_where`:
256
-
257
- ```http
258
- GET /movies?_where={"title":{"contains":"ardenia"}}
259
- ```
260
-
261
- Nested objects and arrays can be filtered at any depth. Conditions in one object use `AND` by default:
262
-
263
- ```json
264
- {
265
- "actors": { "some": { "userId": { "eq": "1" } } },
266
- "title": { "contains": "ardenia" }
267
- }
268
- ```
269
-
270
- Use `and`, `or` and `not` for explicit logical groups:
271
-
272
- ```json
273
- {
274
- "and": [
275
- {
276
- "or": [
277
- { "title": { "contains": "ardenia" } },
278
- { "actors": { "some": { "userId": { "eq": "2" } } } }
279
- ]
280
- },
281
- { "not": { "isArchived": { "eq": true } } }
282
- ]
283
- }
284
- ```
285
-
286
- Field operators:
287
-
288
- | Operator | Behavior |
289
- | --- | --- |
290
- | `eq`, `ne` | Equality or inequality |
291
- | `contains` | Case-insensitive substring for strings, or matching member for arrays |
292
- | `startsWith`, `endsWith` | Case-insensitive string prefix or suffix |
293
- | `gt`, `gte`, `lt`, `lte` | Ordered comparison; ISO date strings can be compared lexically |
294
- | `in` | Matches a scalar or array member against the supplied values |
295
- | `some`, `every`, `none` | Applies a nested condition to array elements |
296
- | `not` | Negates a nested field condition |
297
-
298
- Simple query parameters are supported too:
299
-
300
- ```http
301
- GET /movies?title:contains=ardenia
302
- ```
303
-
304
- 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`.
305
-
306
- Different simple query filters are combined with `AND`. Repeating the same equality filter selects any of its values, so `GET /movies?id=1&id=2` is equivalent to `GET /movies?id:in=1,2`. For an `in` filter, values are separated with commas. 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`.
307
-
308
- 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))`.
309
-
310
- 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.
311
-
312
- ## Relationships
313
-
314
- Use `_embed` to add related records to the response:
315
-
316
- ```http
317
- GET /movies/1?_embed=actors.user.country&_embed=actors.genres&_embed=publishers
318
- ```
319
-
320
- 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:
321
-
322
- ```http
323
- GET /movies/1?_embed=actors.user.country
324
- GET /genres/2?_embed=parents.parents
325
- ```
326
-
327
- Unknown or malformed `_embed` paths return `400`.
328
-
329
- 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.
330
-
331
- Reverse relationships work as well:
332
-
333
- ```http
334
- GET /countries/1?_embed=users
335
- ```
336
-
337
- 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:
338
-
339
- - `countryId` points to `countries`;
340
- - `userId` points to `users` when the requested relation is `user`;
341
- - `genreIds` points to `genres`;
342
- - `publisherIds` points to `publishers`;
343
- - `parentIds` points back to the current resource when `_embed=parents` is requested; `_embed=children` resolves the reverse self-relation.
344
-
345
- Reverse relations use the source resource name. For example, `_embed=users` on a country finds users whose nested data contains the corresponding `countryId`. Relations are resolved when requested, but referential integrity is not enforced when data is written.
346
-
347
- Explicit `...Id` and `...Ids` fields determine relations. If a record also contains an outdated embedded value, `_embed` replaces that response property with the current related record. A missing related record becomes `null` for a single relation or is omitted from the resulting array for a collection relation. Per-request ID indexes are created only for resources used by the current relation lookup.
348
-
349
364
  ## Files
350
365
 
351
366
  Add `files.directory` and `files.metadata` to the server config, then pass `--files` to enable raw binary uploads:
@@ -411,172 +426,33 @@ In disk mode, the binary is stored at `<files.directory>/<directory>/<name>`. Th
411
426
 
412
427
  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.
413
428
 
414
- ## Database schema and OpenAPI generation
415
-
416
- 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:
417
-
418
- ```json
419
- {
420
- "$info": {
421
- "title": "Movie Catalog API",
422
- "version": "1.0.0"
423
- },
424
- "$schema": {
425
- "movies": {
426
- "required": ["actors", "actors.genreIds", "actors.userId", "publisherIds", "title"],
427
- "formats": {
428
- "coverSrc": "uri"
429
- }
430
- },
431
- "users": {
432
- "required": ["bornAt", "fullName"],
433
- "formats": {
434
- "avatarSrc": "uri",
435
- "bornAt": "date"
436
- }
437
- }
438
- }
439
- }
440
- ```
441
-
442
- Schema configuration:
443
-
444
- | Key | Purpose |
445
- | --- | --- |
446
- | `$info` | OpenAPI `info`; when present, non-empty `title` and `version` are required |
447
- | `$schema.<resource>.name` | Explicit component name when automatic singularization is unsuitable or collides |
448
- | `$schema.<resource>.required` | Required field paths; nested paths use dots, such as `actors.userId` |
449
- | `$schema.<resource>.formats` | OpenAPI formats for inferred or explicit string fields, such as `date`, `date-time` or `uri` |
450
- | `$schema.<resource>.properties` | Recursive OpenAPI-compatible field schemas merged with inference |
451
-
452
- `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.
453
-
454
- Set `openapi.path` in the server config, then generate an OpenAPI 3.0.3 file and exit:
455
-
456
- ```bash
457
- deep-json-server --openapi-only server.config.js
458
- ```
459
-
460
- To include file routes in the document, configure the `files` section and add `--files`: `deep-json-server --files --openapi-only server.config.js`.
461
-
462
- 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.
463
-
464
- Different non-overlapping value types are inferred independently and combined through `oneOf`; mixed integers and decimal numbers are represented by one `number` schema. Configuration is validated before generation: `$info`, resource and component names, supported `properties` keywords and their value types are checked, while paths from `required` and `formats` must exist in the resulting schema. Explicit component names may contain ASCII letters, digits, dots, underscores, and hyphens.
465
-
466
- Use `properties` to describe fields that cannot be inferred, particularly for an empty resource. Explicit properties are merged with inferred properties:
467
-
468
- ```json
469
- {
470
- "$schema": {
471
- "reviews": {
472
- "properties": {
473
- "rating": { "type": "integer", "minimum": 1, "maximum": 5 },
474
- "text": { "type": "string" }
475
- },
476
- "required": ["rating"]
477
- }
478
- }
479
- }
480
- ```
481
-
482
- 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.
483
-
484
- `$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.
485
-
486
- Use `name` when a resource needs an explicit schema name instead of the automatically singularized name:
487
-
488
- ```json
489
- {
490
- "$schema": {
491
- "equipment": {
492
- "name": "Equipment"
493
- }
494
- }
495
- }
496
- ```
497
-
498
- The generated document describes CRUD endpoints, pagination, sorting, nested data 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 for arbitrary media types. A numeric database ID is described as `integer | string`, because a later `POST` creates a string ID in the same resource. Generation rejects duplicate schema names, operation IDs, and invalid schema overrides before producing a document. 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.
499
-
500
- 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`.
501
-
502
429
  ## Programmatic API
503
430
 
504
431
  ```js
505
432
  import { createServer } from '@kollors/deep-json-server';
433
+ import config from './server.config.js';
434
+
435
+ const facade = await createServer(config);
436
+ const openapi = await facade.openapi();
437
+ const sdl = await facade.graphql();
438
+ const server = facade.fastify();
439
+ await server.listen();
440
+ // await server.close();
441
+ ```
506
442
 
507
- const config = {
508
- database: {
509
- path: 'mock/database.json',
510
- schema: 'mock/database-schema.json',
511
- },
512
- files: {
513
- directory: 'mock/files',
514
- metadata: 'mock/files/_database.json',
515
- },
516
- openapi: {
517
- path: 'mock/openapi-schema.yaml',
518
- },
519
- server: {
520
- cors: true,
521
- host: '127.0.0.1',
522
- logger: false,
523
- maxFileSize: 100 * 1024 * 1024,
524
- maxPageSize: 1000,
525
- port: 4001,
526
- },
527
- };
528
-
529
- // Make a request without opening a network port—useful in automated tests.
530
- const server = await createServer(config);
531
- const fastify = server.fastify();
532
- const response = await fastify.inject({ method: 'GET', url: '/movies' });
533
-
534
- console.log(response.json());
535
-
536
- // Return the document and write it to config.openapi.path.
537
- const document = await server.openapi();
538
-
539
- await fastify.close();
443
+ Accessors are lazy; exporting schemas does not listen or initialize disk file storage. Override runtime features with `createServer(config, { files: false, graphql: true })`.
540
444
 
541
- // Start a network server. With no arguments, listen uses server.host and server.port.
542
- const runningServer = await createServer(config);
543
- const runningFastify = runningServer.fastify();
445
+ ## Storage and development
544
446
 
545
- await runningFastify.listen();
447
+ Updates are serialized within one server instance and validated on a draft before persistence. Use one writer per database file. Increment counters are stored next to the database in `<database path>.counters.json`; keep that file with the database. Numbers are reserved before the data write, so a failed write can leave gaps but cannot reuse a reserved number. UUID generation uses Node's built-in crypto API.
546
448
 
547
- // Later, during application shutdown:
548
- await runningFastify.close();
449
+ This is a mock server; there is no authentication or password hashing. File routes keep their independent storage and validation.
549
450
 
550
- // Keep the database, schema, and files entirely in memory.
551
- const memoryServer = await createServer({
552
- database: {
553
- data: { movies: [{ id: '1', title: 'Shadows of Ardenia' }] },
554
- schema: { $info: { title: 'Movie API', version: '1.0.0' } },
555
- },
556
- files: {
557
- data: [{ content: new Uint8Array([1, 2, 3]), directory: 'examples', mimeType: 'application/octet-stream', name: 'example.bin' }],
558
- },
559
- });
560
-
561
- const memoryFastify = memoryServer.fastify();
562
- const memoryResponse = await memoryFastify.inject({ method: 'GET', url: '/movies/1' });
563
-
564
- console.log(memoryResponse.json());
565
-
566
- await memoryFastify.close();
451
+ ```sh
452
+ npm ci
453
+ npm run verify
567
454
  ```
568
455
 
569
- `createServer()` accepts exactly the same config shape as `server.config.js`. It loads and clones the database, schema, and file storage, then returns an object with two methods:
570
-
571
- | Method | Purpose |
572
- | --- | --- |
573
- | `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` |
574
- | `server.openapi()` | Returns an OpenAPI document and also writes it when `openapi.path` is configured |
575
-
576
- 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 file-route setting as `server.fastify()`.
577
-
578
- 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 returned object and every config variant.
579
-
580
- ## Scope and security
456
+ Verification runs type checking, lint, coverage gates and installation checks against the packed package. A new alpha version in `package.json` pushed to `main` creates its version tag and publishes through GitHub Actions trusted publishing to npm `alpha`. An existing tag skips automatic publication. Explicit version-tag pushes also publish; stable versions use `latest`. The publishing script rejects mismatched Git tags.
581
457
 
582
- Deep JSON Server is intended for local development and automated tests. It has no authentication or authorization, allows CORS from every origin by default, persists accepted writes when disk storage is configured and does not enforce referential integrity. Set `server.cors` to `false` to disable the built-in CORS headers and `OPTIONS` routes. 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.
458
+ License: MIT.