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

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 (229) hide show
  1. package/README.md +566 -347
  2. package/README.ru.md +560 -341
  3. package/dist/bin/deep-json-server.js +1 -1
  4. package/dist/bin/deep-json-server.js.map +1 -1
  5. package/dist/index.d.ts +13 -5
  6. package/dist/index.js +4 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/src/auth/contract.d.ts +31 -0
  9. package/dist/src/auth/contract.js +12 -0
  10. package/dist/src/auth/contract.js.map +1 -0
  11. package/dist/src/auth/input.d.ts +18 -0
  12. package/dist/src/auth/input.js +34 -0
  13. package/dist/src/auth/input.js.map +1 -0
  14. package/dist/src/auth/password.d.ts +17 -0
  15. package/dist/src/auth/password.js +41 -0
  16. package/dist/src/auth/password.js.map +1 -0
  17. package/dist/src/auth/public.d.ts +3 -0
  18. package/dist/src/auth/public.js +5 -0
  19. package/dist/src/auth/public.js.map +1 -0
  20. package/dist/src/auth/routes.d.ts +6 -0
  21. package/dist/src/auth/routes.js +18 -0
  22. package/dist/src/auth/routes.js.map +1 -0
  23. package/dist/src/auth/service.d.ts +78 -0
  24. package/dist/src/auth/service.js +218 -0
  25. package/dist/src/auth/service.js.map +1 -0
  26. package/dist/src/auth/store.d.ts +26 -0
  27. package/dist/src/auth/store.js +82 -0
  28. package/dist/src/auth/store.js.map +1 -0
  29. package/dist/src/cli/index.d.ts +7 -0
  30. package/dist/src/cli/index.js +122 -0
  31. package/dist/src/cli/index.js.map +1 -0
  32. package/dist/src/core/config-values.d.ts +28 -0
  33. package/dist/src/core/config-values.js +53 -0
  34. package/dist/src/core/config-values.js.map +1 -0
  35. package/dist/src/{constants.d.ts → core/constants.d.ts} +2 -1
  36. package/dist/src/{constants.js → core/constants.js} +2 -1
  37. package/dist/src/core/constants.js.map +1 -0
  38. package/dist/src/core/database.d.ts +48 -0
  39. package/dist/src/{database.js → core/database.js} +64 -29
  40. package/dist/src/core/database.js.map +1 -0
  41. package/dist/src/core/engine.d.ts +65 -0
  42. package/dist/src/core/engine.js +312 -0
  43. package/dist/src/core/engine.js.map +1 -0
  44. package/dist/src/core/errors.d.ts +9 -0
  45. package/dist/src/core/errors.js +12 -0
  46. package/dist/src/core/errors.js.map +1 -0
  47. package/dist/src/core/http-errors.d.ts +9 -0
  48. package/dist/src/core/http-errors.js +15 -0
  49. package/dist/src/core/http-errors.js.map +1 -0
  50. package/dist/src/core/lifecycle/mutation.d.ts +49 -0
  51. package/dist/src/core/lifecycle/mutation.js +170 -0
  52. package/dist/src/core/lifecycle/mutation.js.map +1 -0
  53. package/dist/src/core/lifecycle/options.d.ts +21 -0
  54. package/dist/src/core/lifecycle/options.js +19 -0
  55. package/dist/src/core/lifecycle/options.js.map +1 -0
  56. package/dist/src/core/model.d.ts +145 -0
  57. package/dist/src/core/model.js +510 -0
  58. package/dist/src/core/model.js.map +1 -0
  59. package/dist/src/core/mutations/write.d.ts +26 -0
  60. package/dist/src/core/mutations/write.js +284 -0
  61. package/dist/src/core/mutations/write.js.map +1 -0
  62. package/dist/src/core/operations.d.ts +33 -0
  63. package/dist/src/core/operations.js +10 -0
  64. package/dist/src/core/operations.js.map +1 -0
  65. package/dist/src/core/pagination.d.ts +10 -0
  66. package/dist/src/core/pagination.js +13 -0
  67. package/dist/src/core/pagination.js.map +1 -0
  68. package/dist/src/core/paths.d.ts +16 -0
  69. package/dist/src/core/paths.js +64 -0
  70. package/dist/src/core/paths.js.map +1 -0
  71. package/dist/src/core/query/contract.d.ts +6 -0
  72. package/dist/src/core/query/contract.js +22 -0
  73. package/dist/src/core/query/contract.js.map +1 -0
  74. package/dist/src/core/query/filter.d.ts +6 -0
  75. package/dist/src/core/query/filter.js +100 -0
  76. package/dist/src/core/query/filter.js.map +1 -0
  77. package/dist/src/core/query/options.d.ts +30 -0
  78. package/dist/src/core/query/options.js +44 -0
  79. package/dist/src/core/query/options.js.map +1 -0
  80. package/dist/src/core/records.d.ts +46 -0
  81. package/dist/src/core/records.js +69 -0
  82. package/dist/src/core/records.js.map +1 -0
  83. package/dist/src/core/relation-metadata.d.ts +15 -0
  84. package/dist/src/{relation-metadata.js → core/relation-metadata.js} +7 -2
  85. package/dist/src/core/relation-metadata.js.map +1 -0
  86. package/dist/src/core/storage.d.ts +2 -0
  87. package/dist/src/core/storage.js +2 -0
  88. package/dist/src/core/storage.js.map +1 -0
  89. package/dist/src/core/types.d.ts +10 -0
  90. package/dist/src/{types.js.map → core/types.js.map} +1 -1
  91. package/dist/src/core/utils.d.ts +56 -0
  92. package/dist/src/core/utils.js +102 -0
  93. package/dist/src/core/utils.js.map +1 -0
  94. package/dist/src/files/contract.d.ts +32 -30
  95. package/dist/src/files/contract.js +29 -48
  96. package/dist/src/files/contract.js.map +1 -1
  97. package/dist/src/files/disk-store.d.ts +5 -1
  98. package/dist/src/files/disk-store.js +61 -33
  99. package/dist/src/files/disk-store.js.map +1 -1
  100. package/dist/src/files/http.d.ts +36 -0
  101. package/dist/src/files/http.js +53 -0
  102. package/dist/src/files/http.js.map +1 -0
  103. package/dist/src/files/index.d.ts +5 -4
  104. package/dist/src/files/index.js +7 -2
  105. package/dist/src/files/index.js.map +1 -1
  106. package/dist/src/files/memory-store.d.ts +4 -1
  107. package/dist/src/files/memory-store.js +29 -22
  108. package/dist/src/files/memory-store.js.map +1 -1
  109. package/dist/src/files/routes.d.ts +3 -0
  110. package/dist/src/files/routes.js +27 -2
  111. package/dist/src/files/routes.js.map +1 -1
  112. package/dist/src/files/streams.d.ts +5 -0
  113. package/dist/src/files/streams.js +20 -0
  114. package/dist/src/files/streams.js.map +1 -0
  115. package/dist/src/graphql/entry.d.ts +12 -0
  116. package/dist/src/graphql/entry.js +13 -0
  117. package/dist/src/graphql/entry.js.map +1 -0
  118. package/dist/src/graphql/generate.d.ts +12 -0
  119. package/dist/src/graphql/generate.js +19 -0
  120. package/dist/src/graphql/generate.js.map +1 -0
  121. package/dist/src/graphql/preflight.d.ts +6 -0
  122. package/dist/src/graphql/preflight.js +42 -0
  123. package/dist/src/graphql/preflight.js.map +1 -0
  124. package/dist/src/graphql/resolvers.d.ts +17 -0
  125. package/dist/src/graphql/resolvers.js +44 -0
  126. package/dist/src/graphql/resolvers.js.map +1 -0
  127. package/dist/src/graphql/routes.d.ts +8 -0
  128. package/dist/src/graphql/routes.js +30 -0
  129. package/dist/src/graphql/routes.js.map +1 -0
  130. package/dist/src/graphql/schema.d.ts +6 -0
  131. package/dist/src/graphql/schema.js +230 -0
  132. package/dist/src/graphql/schema.js.map +1 -0
  133. package/dist/src/graphql/write.d.ts +4 -0
  134. package/dist/src/graphql/write.js +10 -0
  135. package/dist/src/graphql/write.js.map +1 -0
  136. package/dist/src/openapi/auth.d.ts +13 -0
  137. package/dist/src/openapi/auth.js +139 -0
  138. package/dist/src/openapi/auth.js.map +1 -0
  139. package/dist/src/openapi/document.d.ts +11 -7
  140. package/dist/src/openapi/document.js +287 -318
  141. package/dist/src/openapi/document.js.map +1 -1
  142. package/dist/src/openapi/entry.d.ts +14 -0
  143. package/dist/src/openapi/entry.js +13 -0
  144. package/dist/src/openapi/entry.js.map +1 -0
  145. package/dist/src/openapi/files.d.ts +6 -0
  146. package/dist/src/openapi/files.js +87 -0
  147. package/dist/src/openapi/files.js.map +1 -0
  148. package/dist/src/openapi/generate.d.ts +19 -0
  149. package/dist/src/openapi/generate.js +41 -0
  150. package/dist/src/openapi/generate.js.map +1 -0
  151. package/dist/src/openapi/helpers.d.ts +26 -0
  152. package/dist/src/openapi/helpers.js +13 -0
  153. package/dist/src/openapi/helpers.js.map +1 -0
  154. package/dist/src/openapi/options.d.ts +18 -0
  155. package/dist/src/openapi/options.js +13 -0
  156. package/dist/src/openapi/options.js.map +1 -0
  157. package/dist/src/{types.d.ts → openapi/types.d.ts} +2 -12
  158. package/dist/src/openapi/types.js +2 -0
  159. package/dist/src/openapi/types.js.map +1 -0
  160. package/dist/src/openapi/write.d.ts +5 -0
  161. package/dist/src/openapi/write.js +12 -0
  162. package/dist/src/openapi/write.js.map +1 -0
  163. package/dist/src/rest/options.d.ts +22 -0
  164. package/dist/src/rest/options.js +65 -0
  165. package/dist/src/rest/options.js.map +1 -0
  166. package/dist/src/rest/projection.d.ts +13 -0
  167. package/dist/src/rest/projection.js +61 -0
  168. package/dist/src/rest/projection.js.map +1 -0
  169. package/dist/src/rest/routes.d.ts +7 -0
  170. package/dist/src/rest/routes.js +49 -0
  171. package/dist/src/rest/routes.js.map +1 -0
  172. package/dist/src/server/config.d.ts +74 -0
  173. package/dist/src/server/config.js +142 -0
  174. package/dist/src/server/config.js.map +1 -0
  175. package/dist/src/server/create.d.ts +16 -0
  176. package/dist/src/server/create.js +133 -0
  177. package/dist/src/server/create.js.map +1 -0
  178. package/dist/src/server/features.d.ts +4 -0
  179. package/dist/src/server/features.js +11 -0
  180. package/dist/src/server/features.js.map +1 -0
  181. package/dist/src/server/model.d.ts +6 -0
  182. package/dist/src/server/model.js +14 -0
  183. package/dist/src/server/model.js.map +1 -0
  184. package/dist/src/server/public.d.ts +2 -0
  185. package/dist/src/server/public.js +2 -0
  186. package/dist/src/server/public.js.map +1 -0
  187. package/package.json +25 -4
  188. package/dist/src/cli.d.ts +0 -4
  189. package/dist/src/cli.js +0 -80
  190. package/dist/src/cli.js.map +0 -1
  191. package/dist/src/config.d.ts +0 -54
  192. package/dist/src/config.js +0 -145
  193. package/dist/src/config.js.map +0 -1
  194. package/dist/src/constants.js.map +0 -1
  195. package/dist/src/database.d.ts +0 -20
  196. package/dist/src/database.js.map +0 -1
  197. package/dist/src/openapi/config.d.ts +0 -11
  198. package/dist/src/openapi/config.js +0 -204
  199. package/dist/src/openapi/config.js.map +0 -1
  200. package/dist/src/openapi/index.d.ts +0 -10
  201. package/dist/src/openapi/index.js +0 -27
  202. package/dist/src/openapi/index.js.map +0 -1
  203. package/dist/src/openapi/inference.d.ts +0 -14
  204. package/dist/src/openapi/inference.js +0 -145
  205. package/dist/src/openapi/inference.js.map +0 -1
  206. package/dist/src/query/filter.d.ts +0 -6
  207. package/dist/src/query/filter.js +0 -248
  208. package/dist/src/query/filter.js.map +0 -1
  209. package/dist/src/query/index.d.ts +0 -3
  210. package/dist/src/query/index.js +0 -4
  211. package/dist/src/query/index.js.map +0 -1
  212. package/dist/src/query/pagination.d.ts +0 -11
  213. package/dist/src/query/pagination.js +0 -28
  214. package/dist/src/query/pagination.js.map +0 -1
  215. package/dist/src/query/sort.d.ts +0 -1
  216. package/dist/src/query/sort.js +0 -54
  217. package/dist/src/query/sort.js.map +0 -1
  218. package/dist/src/relation-metadata.d.ts +0 -10
  219. package/dist/src/relation-metadata.js.map +0 -1
  220. package/dist/src/relations.d.ts +0 -12
  221. package/dist/src/relations.js +0 -155
  222. package/dist/src/relations.js.map +0 -1
  223. package/dist/src/server.d.ts +0 -13
  224. package/dist/src/server.js +0 -188
  225. package/dist/src/server.js.map +0 -1
  226. package/dist/src/utils.d.ts +0 -20
  227. package/dist/src/utils.js +0 -63
  228. package/dist/src/utils.js.map +0 -1
  229. /package/dist/src/{types.js → core/types.js} +0 -0
package/README.md CHANGED
@@ -1,206 +1,314 @@
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 mock server with REST, GraphQL, related records, file uploads and schema exports. Supports user login, owner and administrator permissions, record timestamps and soft deletion. 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
+ **Breaking changes: 1.0.0-alpha.10.** Configuration and schema formats have changed. See [Configuration](#configuration) and [Model schema](#model-schema) for current examples.
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
+ To install a specific version, use `@1.0.0-alpha.10`.
16
+
17
17
  ## Quick start
18
18
 
19
- Create the database file `mock/database.json` before startup:
19
+ Create two files in the same directory.
20
+
21
+ `database.json`:
20
22
 
21
23
  ```json
22
24
  {
23
- "movies": [
24
- { "id": "1", "title": "Shadows of Ardenia" }
25
+ "users": [
26
+ { "id": "1", "fullName": "Мира Волкова" }
25
27
  ]
26
28
  }
27
29
  ```
28
30
 
29
- Create the ESM module `server.config.js` next to `package.json`:
31
+ `server.config.js`:
30
32
 
31
33
  ```js
32
- export default {
33
- database: {
34
- path: 'mock/database.json',
35
- },
36
- };
34
+ export default { storage: 'file', database: { source: './database.json' } };
37
35
  ```
38
36
 
39
- Start the server:
40
-
41
- ```bash
37
+ ```sh
42
38
  npx deep-json-server server.config.js
43
39
  ```
44
40
 
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:
41
+ The user list is available at `http://127.0.0.1:4001/users`.
42
+
43
+ Add a [model schema](#model-schema) to define relations and validation. See [queries](#queries-and-responses), [authentication](#authentication), [soft deletion](#record-dates-deletion-and-ownership), [files](#files) and the [programmatic API](#programmatic-api) for more.
44
+
45
+ ## Configuration
46
+
47
+ With `storage: 'file'`, all sources and the schema are paths. With `'memory'`, they are in-memory data. Modes cannot be mixed. The presence of `auth`, `files`, `graphql` and `openapi` enables those modules. `graphql: {}` and `openapi: {}` enable only the HTTP endpoints at their defaults; add `target` to export a schema.
48
+
49
+ ```js
50
+ export default {
51
+ storage: 'file',
52
+ database: { source: './database.json', schema: './schema.json' },
53
+ auth: { source: './users.json', expiresIn: 3600 },
54
+ files: { source: './uploads' },
55
+ graphql: { target: './generated/schema.graphql' },
56
+ openapi: { target: './generated/openapi.yaml' },
57
+ server: { host: '127.0.0.1', port: 4001 },
58
+ };
59
+ ```
60
+
61
+ | Setting | Meaning |
62
+ |---|---|
63
+ | `storage` | Required: `file` or `memory`; applies to all sources and the schema |
64
+ | `database.source` | Database JSON path or collection object |
65
+ | `database.schema` | Schema JSON path or schema object; optional for REST |
66
+ | `auth.source` | Users JSON path or user array |
67
+ | `auth.expiresIn` | Session lifetime in seconds; default 3600 |
68
+ | `files.source` | Files directory or initial file array |
69
+ | `files.metadata` | For `file` only: metadata JSON path; defaults to `.files.json` inside `files.source` |
70
+ | `graphql.endpoint` | HTTP endpoint; default `/graphql` |
71
+ | `graphql.target` | GraphQL SDL export destination |
72
+ | `openapi.endpoint` | HTTP endpoint; default `/openapi.json` |
73
+ | `openapi.target` | OpenAPI export destination |
74
+ | `openapi.info` | Metadata: required `title` and `version`, optional `description` |
75
+ | `server.host`, `server.port` | Defaults `127.0.0.1`, `4001`; CLI also reads `HOST`/`PORT` |
76
+ | `server.pageSize`, `server.maxPageSize` | Defaults 10 and 100; default size is capped by the maximum |
77
+ | `server.cors`, `server.logger` | Default `true`; logger also accepts Fastify logger options |
78
+ | `server.maxFileSize` | Default 100 MiB |
79
+
80
+ Relative paths resolve from the configuration file directory, or from the working directory with `createServer(config)`. In-memory data, including the schema, is copied. Port `0` lets the system choose an available port.
81
+
82
+ ### CLI
83
+
84
+ | Flag | Action |
85
+ |---|---|
86
+ | `--generate` | Export schemas, then start the server |
87
+ | `--generate-only` | Export schemas and exit |
88
+ | `--host <host>` | Server address |
89
+ | `--port <port>` | Server port |
90
+ | `--help, -h` | Help |
91
+ | `--version, -v` | Package version |
92
+
93
+ Address precedence: CLI → configuration → `HOST`/`PORT` → defaults. Without generation flags, only the server starts. `--generate` and `--generate-only` are mutually exclusive.
94
+
95
+ ## Model schema
96
+
97
+ Examples: [database](examples/database.json), [model schema](examples/schema.json), [configuration](examples/server.config.js).
46
98
 
47
99
  ```json
48
100
  {
49
- "data": [{ "id": "1", "title": "Shadows of Ardenia" }],
50
- "total": 1
101
+ "api": [
102
+ "openapi",
103
+ "graphql"
104
+ ],
105
+ "models": {
106
+ "Country": {
107
+ "collection": "countries",
108
+ "fields": {
109
+ "id": {
110
+ "type": "string",
111
+ "primary": true,
112
+ "generated": "uuid"
113
+ },
114
+ "name": {
115
+ "type": "string",
116
+ "required": true
117
+ },
118
+ "users": {
119
+ "type": "User[]",
120
+ "target": "countryId"
121
+ }
122
+ }
123
+ },
124
+ "User": {
125
+ "collection": "users",
126
+ "fields": {
127
+ "id": {
128
+ "type": "string",
129
+ "primary": true,
130
+ "generated": "uuid"
131
+ },
132
+ "fullName": {
133
+ "type": "string",
134
+ "required": true
135
+ },
136
+ "country": {
137
+ "type": "Country",
138
+ "source": "countryId"
139
+ }
140
+ }
141
+ }
142
+ }
51
143
  }
52
144
  ```
53
145
 
54
- ## Configuration and startup
146
+ Model definitions belong in `models`. The schema root can define `api`, `timestamps` and `softDelete`; a model can override each setting. API precedence is model → schema root → configuration sections. Arrays replace the inherited value; `[]` excludes a model from GraphQL and OpenAPI while REST remains available. An explicit list does not activate an absent module. Related models must allow the same format. Model names must be valid identifiers; type and operation collisions cause errors. `and`, `or` and `not` are reserved filter names.
55
147
 
56
- Create the ESM module `server.config.js`. The example below shows settings for every feature:
148
+ | Capability | With schema | Without schema |
149
+ |---|---|---|
150
+ | REST CRUD | Model validation | JSON/body/identifier validation only |
151
+ | Relations | Explicit model fields | `countryId`, `genreIds`, etc. naming conventions |
152
+ | OpenAPI 3.0.3 export | Available | Error when requested |
153
+ | GraphQL SDL / API | Available | Error when requested |
57
154
 
58
- ```js
59
- import process from 'node:process';
155
+ Explicit schemas are strict: undeclared fields and collections are rejected, except storage keys inferred from relations. Existing data is validated on startup. Generation uses the model definitions.
60
156
 
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
- ```
157
+ Schemaless REST generates an `id` and preserves arbitrary JSON fields. Filters and individual field selections use identifier-style names; other fields are returned through `scope=[{"*":true}]`. Fields with mixed value types can be read, but filtering, ordering and paging heterogeneous lists require an explicit schema.
83
158
 
84
- Configuration keys:
159
+ Each model requires `collection`, the database collection and REST path name, and `fields`, its field definitions. The model name (`User`) determines GraphQL type and operation names. `api` controls format availability; `timestamps` and `softDelete` override global settings for that model.
85
160
 
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` |
161
+ ### Fields
101
162
 
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.
163
+ The `type` property accepts `string`, `number`, `boolean`, `object`, or a model name. Append `[]` for an array: `string[]`, `object[]`, `Genre[]`. Use dotted paths for nested fields, such as `actors.fullName`.
103
164
 
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`.
165
+ | Properties | Meaning |
166
+ |---|---|
167
+ | `type` | Required type |
168
+ | `description`, `example` | Documentation and example value |
169
+ | `required`, `nullable` | Defaults `false`; presence and explicit null are separate |
170
+ | `default` | Value when omitted on create/replace; PATCH does not insert defaults |
171
+ | `enum` | Allowed strings, numbers or booleans; for arrays, allowed element values |
172
+ | `primary` | Root primary key; mandatory, unique, non-null and immutable |
173
+ | `generated` | `uuid` for strings, `increment` for numbers; server supplies the value |
174
+ | `readOnly`, `writeOnly` | Output-only or input-only; mutually exclusive |
175
+ | `minLength`, `maxLength`, `pattern` | String constraints |
176
+ | `format` | `date`, `date-time`, `email`, `uri`, `uuid` |
177
+ | `minimum`, `maximum` | Inclusive numeric bounds |
178
+ | `source`, `target`, `onDelete` | Relation metadata |
105
179
 
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`:
180
+ String and numeric constraints on `string[]`/`number[]` apply to every element. `required` and `nullable` apply to the entire array; `default` and `example` contain a complete array. Elements must match the array's type and be non-null. `required` requires the field to be present; an ordinary array may still be empty.
107
181
 
108
- ```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
- };
118
- ```
182
+ Each model requires exactly one primary key of type `string` or `number`, declared at the top level. The name is arbitrary: `id`, `username`, `code`. If `generated` is omitted, the client supplies the value on creation. Generated fields must be declared at the top level, are excluded from input types and cannot have `default`. Replacing a record preserves generated values and read-only fields, including nested objects. To protect fields inside an array, mark the entire array or its containing object as `readOnly`. Objects containing only server-managed fields are output-only.
183
+
184
+ For example, a `LocalUser` with primary key `username` and `password: {"type":"string","required":true,"writeOnly":true}` has `localUser(username: ...)` and `/localUsers/{username}`. A `writeOnly` field accepts input and is excluded from responses, `scope`, filters and ordering.
119
185
 
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.
186
+ Objects used in GraphQL must have at least one field visible in responses; REST also accepts empty objects.
121
187
 
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:
188
+ ### Relations
123
189
 
124
190
  ```json
125
191
  {
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"
192
+ "actors.genres": {
193
+ "type": "Genre[]",
194
+ "source": "actors.genreIds",
195
+ "required": true
132
196
  }
133
197
  }
134
198
  ```
135
199
 
136
- CLI modes:
200
+ `Genre` returns an object; `Genre[]` returns a list. `source` defaults to the current model's primary key, `target` to the target model's primary key. These defaults also apply to nested relations. Paths start at the root of their respective records: in this example, `actors.genreIds` contains the current actor's genre keys.
137
201
 
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 |
202
+ Relation keys are stored in the database and included among the record's own fields. Their types are inferred from the matched keys. A `source` field pointing to a target primary key can be omitted from the field declarations: the schema infers an array of keys for a list relation or a scalar key for a single relation. Declare the storage field explicitly when the mapping is ambiguous.
146
203
 
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.
204
+ Reverse example: `User.movies = {"type":"Movie[]","target":"actors.userId"}`. A movie is returned once even if several actors match. A single relation that matches multiple records causes an error.
148
205
 
149
- ## Example database
206
+ Every supplied direct relation key must point to an existing record. `required: true` on a relation requires at least one target before response filtering/pagination. Reverse relations using the primary key as `source` may be empty unless required. Missing single relations return `null`.
207
+
208
+ `onDelete` describes what happens **when a target record is deleted**:
209
+
210
+ - `restrict` (default): refuse deletion while a retained record refers to the target.
211
+ - `cascade`: delete the referring record. For `User.country`, deleting the country deletes its users. For `Movie.actors.user`, deleting the user removes matching actor elements and retains the movie.
150
212
 
151
- Below is an example movie catalog with sample data. `Gangster` is linked to its parent genre, `Crime`.
213
+ Cascading deletion runs as one operation, including cyclic relations. A validation failure cancels the entire operation. `onDelete` rules also apply to explicitly declared reverse relations; account for both rules when defining both directions.
214
+
215
+ ## Example database
152
216
 
153
217
  ```json
154
218
  {
155
219
  "countries": [
156
- { "id": "1", "isArchived": false, "name": "Ardenia" },
157
- { "id": "2", "isArchived": false, "name": "Veloria" }
220
+ {
221
+ "id": "1",
222
+ "isArchived": false,
223
+ "name": "Ардения"
224
+ },
225
+ {
226
+ "id": "2",
227
+ "isArchived": false,
228
+ "name": "Велория"
229
+ }
158
230
  ],
159
231
  "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": [] }
232
+ {
233
+ "id": "1",
234
+ "isArchived": false,
235
+ "name": "Криминал",
236
+ "parentIds": []
237
+ },
238
+ {
239
+ "id": "2",
240
+ "isArchived": false,
241
+ "name": "Гангстер",
242
+ "parentIds": ["1"]
243
+ },
244
+ {
245
+ "id": "3",
246
+ "isArchived": false,
247
+ "name": "Драма",
248
+ "parentIds": []
249
+ },
250
+ {
251
+ "id": "4",
252
+ "isArchived": false,
253
+ "name": "Комедия",
254
+ "parentIds": []
255
+ }
164
256
  ],
165
257
  "movies": [
166
258
  {
167
259
  "actors": [
168
- { "genreIds": ["2", "3"], "id": "movie-1-actor-1", "userId": "1" },
169
- { "genreIds": ["3"], "id": "movie-1-actor-2", "userId": "2" }
260
+ {
261
+ "genreIds": ["2", "3"],
262
+ "id": "movie-1-actor-1",
263
+ "userId": "1"
264
+ },
265
+ {
266
+ "genreIds": ["3"],
267
+ "id": "movie-1-actor-2",
268
+ "userId": "2"
269
+ }
170
270
  ],
171
271
  "coverSrc": "https://example.com/covers/shadows-of-ardenia.jpg",
172
- "description": "The heir to a port city uncovers a conspiracy between two rival families.",
272
+ "description": "Наследница портового города раскрывает заговор двух соперничающих семей.",
173
273
  "id": "1",
174
274
  "isArchived": false,
175
275
  "publisherIds": ["2"],
176
- "title": "Shadows of Ardenia"
276
+ "title": "Тени Ардении"
177
277
  },
178
278
  {
179
279
  "actors": [],
180
280
  "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.",
281
+ "description": "Ночной администратор старого отеля случайно становится участником поисков пропавшей картины.",
182
282
  "id": "2",
183
283
  "isArchived": false,
184
284
  "publisherIds": ["1"],
185
- "title": "Midnight at the Northern Star"
285
+ "title": "Полночь в «Северной звезде»"
186
286
  }
187
287
  ],
188
288
  "publishers": [
189
- { "id": "1", "isArchived": false, "name": "Northlight Studio" },
190
- { "id": "2", "isArchived": false, "name": "Aurora Pictures" }
289
+ {
290
+ "id": "1",
291
+ "isArchived": false,
292
+ "name": "Northlight Studio"
293
+ },
294
+ {
295
+ "id": "2",
296
+ "isArchived": false,
297
+ "name": "Aurora Pictures"
298
+ }
191
299
  ],
192
300
  "users": [
193
301
  {
194
302
  "bornAt": "1988-03-14",
195
303
  "countryId": "1",
196
- "fullName": "Mira Volkova",
304
+ "fullName": "Мира Волкова",
197
305
  "id": "1",
198
306
  "isArchived": false
199
307
  },
200
308
  {
201
309
  "bornAt": "1991-11-02",
202
310
  "countryId": "2",
203
- "fullName": "Leon Vetrov",
311
+ "fullName": "Леон Ветров",
204
312
  "id": "2",
205
313
  "isArchived": false
206
314
  }
@@ -208,153 +316,381 @@ Below is an example movie catalog with sample data. `Gangster` is linked to its
208
316
  }
209
317
  ```
210
318
 
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
- ```
319
+ ## Queries and responses
221
320
 
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.
321
+ The movie, actor and genre examples use the full [example schema](examples/schema.json). Run them with the [example configuration](examples/server.config.js):
223
322
 
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.
323
+ ```sh
324
+ npx deep-json-server examples/server.config.js
325
+ ```
225
326
 
226
- Successful writes return the created, replaced, updated or deleted record. Errors use an appropriate HTTP status and this JSON shape:
327
+ Collections and lists of objects, including embedded `object[]` fields, return:
227
328
 
228
329
  ```json
229
- { "error": "..." }
330
+ { "data": [], "total": 0 }
230
331
  ```
231
332
 
232
- ## Pagination and sorting
333
+ Primitive arrays are returned as plain arrays. Every object list accepts optional `where`, `order` and `pager`. Processing order is filter → sort → pagination. `total` is the filtered record count before pagination.
233
334
 
234
- ```http
235
- GET /movies?_page=1&_perPage=10&_sort=-id,title
236
- ```
335
+ Use `page` and `pageSize` in `pager`. The default is the first page with the size from `server.pageSize`. Both values must be positive integers; `pageSize` is limited by `server.maxPageSize`. Out-of-range pages return empty `data` with the total matching record count in `total`.
237
336
 
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`:
337
+ `where` uses field operators `eq`, `ne`, `in`, string `contains`/`startsWith`/`endsWith`, and comparisons `gt`, `gte`, `lt`, `lte`. Conditions in the same object must all match. `and` and `or` take arrays of conditions; `not` takes one condition and can also be used inside a field filter. Arrays support `some`, `every`, `none`; primitive arrays also support `contains`, `in`. String `contains`, `startsWith` and `endsWith` ignore case; `eq`, `ne` and `in` compare exact values. A field condition is an object containing an operator, such as `{ "id": { "eq": "1" } }`.
239
338
 
240
339
  ```json
241
340
  {
242
- "data": [],
243
- "total": 0
341
+ "movies": {
342
+ "some": {
343
+ "actors": {
344
+ "some": {
345
+ "genres": { "some": { "id": { "in": ["2", "3"] } } }
346
+ }
347
+ }
348
+ }
349
+ }
244
350
  }
245
351
  ```
246
352
 
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))`.
353
+ Root `where` selects records from the main collection. `where` inside a relation filters its elements while retaining the parent record. Each nested list is processed independently. Filtering by a relation works independently of its inclusion in the response.
248
354
 
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.
355
+ `order` is an array of `{ "field": "fullName", "direction": "ASC" }` rules. `ASC` sorts in ascending order and `DESC` in descending order. Earlier rules have priority; equal values 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 supports scalar fields of the current object, including nested fields. Related lists accept their own `order`.
250
356
 
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`.
357
+ ### REST
252
358
 
253
- ## Filters
359
+ `GET /` returns collection names: `{ "resources": ["users", "movies"] }`. Each collection has these routes:
254
360
 
255
- Pass a JSON object through `_where`:
361
+ | Method | Path | Operation |
362
+ |---|---|---|
363
+ | GET | `/users` | `userList` |
364
+ | GET | `/users/{id}` | `user` |
365
+ | POST | `/users` | `userCreate` |
366
+ | PUT | `/users/{id}` | `userReplace` |
367
+ | PATCH | `/users/{id}` | `userUpdate` |
368
+ | DELETE | `/users/{id}` | `userDelete` |
256
369
 
257
- ```http
258
- GET /movies?_where={"title":{"contains":"ardenia"}}
370
+ The path parameter name follows the primary key. POST, PUT and PATCH accept a JSON record object. PUT replaces the record while retaining its key and server-managed fields. PATCH merges fields at the top level; supplied nested objects are replaced while preserving their read-only fields. Creation and replacement require all mandatory fields. Updates validate supplied values and the final record. Missing records return `404`; conflicts return `409`.
371
+
372
+ POST returns the created record with status `201`; PUT, PATCH and DELETE return the updated or deleted record with status `200`. REST errors use `{ "error": "Error description" }`.
373
+
374
+ ### REST query parameters
375
+
376
+ REST record routes accept one query parameter, `scope`, containing a JSON array `[fields, arguments?]`. The first object selects fields; the optional second object supplies `where`, `order` and `pager` for a list. The same format applies to the root query, embedded objects and relations.
377
+
378
+ Select users and their movies with independent ordering and pagination:
379
+
380
+ ```js
381
+ const scope = [
382
+ {
383
+ id: true,
384
+ fullName: true,
385
+ movies: [
386
+ { id: true, title: true },
387
+ { order: [{ field: 'title', direction: 'ASC' }], pager: { page: 1, pageSize: 5 } },
388
+ ],
389
+ },
390
+ {
391
+ where: { fullName: { contains: 'Мира' } },
392
+ order: [{ field: 'fullName', direction: 'ASC' }],
393
+ pager: { page: 1, pageSize: 20 },
394
+ },
395
+ ];
396
+ const params = new URLSearchParams({ scope: JSON.stringify(scope) });
397
+ const response = await fetch(`/users?${params}`);
259
398
  ```
260
399
 
261
- Nested objects and arrays can be filtered at any depth. Conditions in one object use `AND` by default:
400
+ Select ordinary fields with `true` and objects or relations with their own scope arrays. Without arguments, the array contains only the fields object. `"*": true` includes own fields and stored keys, except `writeOnly` fields; select relations explicitly.
401
+
402
+ For example, select a movie's own fields, its actors' users and sorted genres:
262
403
 
263
404
  ```json
264
- {
265
- "actors": { "some": { "userId": { "eq": "1" } } },
266
- "title": { "contains": "ardenia" }
267
- }
405
+ [
406
+ {
407
+ "*": true,
408
+ "actors": [
409
+ {
410
+ "user": [{ "id": true, "fullName": true }],
411
+ "genres": [
412
+ { "*": true },
413
+ { "order": [{ "field": "name", "direction": "ASC" }] }
414
+ ]
415
+ }
416
+ ]
417
+ }
418
+ ]
268
419
  ```
269
420
 
270
- Use `and`, `or` and `not` for explicit logical groups:
421
+ Omitting `scope` returns own fields, as with `[{"*":true}]`. An empty selection `[{}]` returns an object without fields. Lists retain the `{ data, total }` response structure.
422
+
423
+ Arguments are available only on lists. Single-record queries and mutation responses can set arguments on their embedded lists. Parameters are validated even on empty data; an invalid response selection rolls back record changes. Invalid scopes return `400`. The JSON length limit is 10,000 characters; selection depth is limited to 32 levels.
424
+
425
+ ### Nested writes
426
+
427
+ Storage keys such as `genreIds: ["1"]` only set a relation. Relation fields also accept records to create or update:
428
+
429
+ ```http
430
+ PATCH /movies/1
431
+ Content-Type: application/json
271
432
 
272
- ```json
273
433
  {
274
- "and": [
434
+ "actors": [
275
435
  {
276
- "or": [
277
- { "title": { "contains": "ardenia" } },
278
- { "actors": { "some": { "userId": { "eq": "2" } } } }
436
+ "userId": "1",
437
+ "genres": [
438
+ "1",
439
+ { "id": "2", "name": "Updated genre" },
440
+ { "name": "New genre" }
279
441
  ]
280
- },
281
- { "not": { "isArchived": { "eq": true } } }
442
+ }
282
443
  ]
283
444
  }
284
445
  ```
285
446
 
286
- Field operators:
447
+ | Relation value | Behavior |
448
+ |---|---|
449
+ | A key, such as `"1"` | Link an existing record without changing it |
450
+ | An object with a primary key | PATCH updates supplied fields; PUT replaces the related record |
451
+ | An object without a primary key | Create a related record with defaults and a generated key |
287
452
 
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 |
453
+ The key name and type follow the target model. An object containing only a key still counts as an update: in PUT it must include the model's required fields. Replacement preserves primary keys, generated values and `readOnly` fields. In POST, nested objects with existing keys receive partial updates. A missing target is an error; creating a nested record without a key requires an autogenerated primary key.
297
454
 
298
- Simple query parameters are supported too:
455
+ A supplied list replaces the relation's membership. PATCH preserves omitted relations; PUT clears omitted writable links. `[]` clears a list and `null` clears a nullable single relation. Removing a link does not delete the related record. Required relations must remain populated.
299
456
 
300
- ```http
301
- GET /movies?title:contains=ardenia
457
+ Use either the relation field or its storage key in an object, for example `genres` or `genreIds`. Reverse relations update the target key. If a target path crosses an array and the server cannot identify one element to attach, provide the array with the intended keys explicitly. Protected keys cannot be changed.
458
+
459
+ All nested changes belong to the main record's transaction. A validation error, missing record or invalid response selection rolls back the entire operation. Updating a shared record affects every record linked to it.
460
+
461
+ GraphQL accepts typed objects in relation fields. To change only the links in a replace mutation, use storage keys such as `genreIds`. For example:
462
+
463
+ ```graphql
464
+ mutation {
465
+ movieUpdate(id: "1", data: {
466
+ actors: [{
467
+ userId: "1"
468
+ genres: [{ id: "2", name: "Updated genre" }, { name: "New genre" }]
469
+ }]
470
+ }) {
471
+ actors { data { genres { data { id name } } } }
472
+ }
473
+ }
302
474
  ```
303
475
 
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`.
476
+ ### GraphQL
305
477
 
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`.
478
+ Set `database.schema` and add `graphql: {}` to the configuration:
307
479
 
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))`.
480
+ ```sh
481
+ npx deep-json-server server.config.js
482
+ ```
309
483
 
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.
484
+ Send requests to `/graphql` using POST with `Content-Type: application/json` and a body of `{ "query": "…", "variables": {} }`. Change the path through `graphql.endpoint`.
485
+
486
+ ```graphql
487
+ query {
488
+ userList(
489
+ where: { fullName: { contains: "Мира" } }
490
+ order: [{ field: fullName, direction: ASC }]
491
+ pager: { page: 1, pageSize: 20 }
492
+ ) {
493
+ total
494
+ data {
495
+ id
496
+ fullName
497
+ movies(
498
+ where: { title: { contains: "Тени" } }
499
+ order: [{ field: title, direction: ASC }]
500
+ pager: { pageSize: 5 }
501
+ ) {
502
+ total
503
+ data { id title }
504
+ }
505
+ }
506
+ }
507
+ }
508
+ ```
311
509
 
312
- ## Relationships
510
+ The query `user(id: ...)` returns one record or `null` if it is missing. Mutations are `userCreate(data: ...)`, `userReplace(id: ..., data: ...)`, `userUpdate(id: ..., data: ...)`, `userDelete(id: ...)`. For models containing only generated fields, the create mutation takes no `data` argument. Writes and validation follow the same rules as REST. Relations selected in a mutation result determine the response contents.
313
511
 
314
- Use `_embed` to add related records to the response:
512
+ String primary keys use GraphQL `ID`; ordinary strings use `String`, numbers use `Float`, and pagination parameters use `Int`. Schema enums preserve valid string labels; other values receive `VALUE_0`, `VALUE_1`, etc. String lengths, formats and other model constraints are validated by the server during request execution. Introspection is available for exploring the schema. Selected list arguments are validated before executing mutations.
315
513
 
316
- ```http
317
- GET /movies/1?_embed=actors.user.country&_embed=actors.genres&_embed=publishers
514
+ Errors include `extensions.code`: `INVALID_INPUT`, `INVALID_QUERY`, `NOT_FOUND`, `CONFLICT`, `UNAUTHENTICATED`, `FORBIDDEN` or `INTERNAL_ERROR`. GraphQL syntax and type errors appear in the standard `errors` array. Query depth is limited to 32 levels.
515
+
516
+ ## OpenAPI and schema exports
517
+
518
+ Exports use OpenAPI 3.0.3. Add `openapi: {}` and `database.schema` to serve the specification at `/openapi.json`. Change the route with `openapi.endpoint`. Open the document in Swagger UI or import it into an API client.
519
+
520
+ Set output paths to save schemas:
521
+
522
+ ```js
523
+ export default {
524
+ storage: 'file',
525
+ database: { source: './database.json', schema: './schema.json' },
526
+ openapi: { target: './generated/openapi.yaml' },
527
+ graphql: { target: './generated/schema.graphql' },
528
+ };
318
529
  ```
319
530
 
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:
531
+ ```bash
532
+ npx deep-json-server server.config.js --generate-only
533
+ npx deep-json-server server.config.js --generate
534
+ ```
321
535
 
322
- ```http
323
- GET /movies/1?_embed=actors.user.country
324
- GET /genres/2?_embed=parents.parents
536
+ `--generate-only` exports and exits; `--generate` starts the server after exporting. Configuration sections select the formats. Each selected format requires its own `target`. Missing sections, missing targets or generation errors fail the command before server startup.
537
+
538
+ Export does not open the database, user records or files. Every selected `target` is checked before writing and cannot overwrite the configuration, database, schema, users, counters or file metadata.
539
+
540
+ ## Authentication
541
+
542
+ The `auth` section enables registration, login and permission checks for record changes in REST and GraphQL. Reads and all file operations remain public.
543
+
544
+ Define the first administrator in the initial data. For example, create `auth.json` with `setup-auth.mjs`:
545
+
546
+ ```js
547
+ import { writeFile } from 'node:fs/promises';
548
+ import { hashPassword } from '@kollors/deep-json-server/auth';
549
+
550
+ const password = process.env.DJS_PASSWORD;
551
+ if (!password) throw new Error('Set DJS_PASSWORD');
552
+ await writeFile(
553
+ './auth.json',
554
+ JSON.stringify(
555
+ [{ id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true }],
556
+ null,
557
+ 2,
558
+ ),
559
+ { flag: 'wx', mode: 0o600 },
560
+ );
325
561
  ```
326
562
 
327
- Unknown or malformed `_embed` paths return `400`.
563
+ Set `DJS_PASSWORD` and run `node setup-auth.mjs`. Add the file to your server configuration:
328
564
 
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.
565
+ ```js
566
+ export default {
567
+ storage: 'file',
568
+ database: { source: './database.json' },
569
+ auth: { source: './auth.json', expiresIn: 3600 },
570
+ };
571
+ ```
330
572
 
331
- Reverse relationships work as well:
573
+ Start with `npx deep-json-server server.config.js`. Each initial user needs a unique string `id`, a unique `username` and a `passwordHash` created by the helper. `isAdmin` defaults to `false`. Passwords use salted scrypt hashes.
332
574
 
333
- ```http
334
- GET /countries/1?_embed=users
575
+ With `storage: 'memory'`, pass an array in `auth.source`:
576
+
577
+ ```js
578
+ import { hashPassword } from '@kollors/deep-json-server/auth';
579
+
580
+ const password = process.env.DJS_PASSWORD;
581
+ if (!password) throw new Error('Set DJS_PASSWORD');
582
+
583
+ export default {
584
+ storage: 'memory',
585
+ database: { source: { items: [] } },
586
+ auth: {
587
+ source: [
588
+ { id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true },
589
+ ],
590
+ },
591
+ };
592
+ ```
593
+
594
+ Auth users are stored separately from the database. In file mode, changes are saved to `auth.source`; in memory mode, they disappear on restart. The supplied user array is not modified. Restart the server after editing the file manually.
595
+
596
+ | REST request | JSON body | Response |
597
+ |---|---|---|
598
+ | `POST /auth/register` | `{ "username": "anna", "password": "…" }` | `201`: `{ id, username, isAdmin: false }` |
599
+ | `POST /auth/login` | `{ "username": "anna", "password": "…" }` | `{ accessToken, expiresIn, user: { id, username, isAdmin } }` |
600
+ | `GET /auth/me` | — | `{ id, username, isAdmin }` |
601
+ | `POST /auth/logout` | — | `{ success: true }` |
602
+ | `PATCH /auth/users/:id/password` | `{ "currentPassword": "…", "newPassword": "…" }` | `{ success: true }` |
603
+ | `PATCH /auth/users/:id/admin` | `{ "isAdmin": true }` | `{ id, username, isAdmin }` |
604
+
605
+ Registration and login are public. Send `Authorization: Bearer <accessToken>` for the other methods. Registration creates an ordinary user with a generated `id`; requests cannot include `id`, `passwordHash` or `isAdmin`. Usernames are case-sensitive and unique; duplicates return `409`. A username must contain a non-whitespace character and be at most 256 characters long. Passwords must contain 1–1024 characters. Values are not trimmed. Registration does not create a session: log in afterwards.
606
+
607
+ Users can change only their own password by supplying `currentPassword` and `newPassword`. Administrators follow the same rule for their own password. An administrator can change an ordinary user's password with just `newPassword`. Changing another administrator's password returns `403`. A successful password change ends all sessions of the target user, including the current session when changing your own password; log in again. An incorrect current password returns `401` without changing sessions.
608
+
609
+ Only administrators can change `isAdmin`. They can grant or remove another user's admin status. An administrator can remove their own status only if another administrator remains; otherwise the request returns `409`. This check accounts for concurrent requests. Existing tokens use the new permissions as soon as the change is saved, including for GraphQL mutations. An administrator may demote another administrator and then change their password as an ordinary user.
610
+
611
+ Invalid or expired tokens return `401`, insufficient permissions return `403`, and an absent user for an otherwise permitted operation returns `404`. Invalid request bodies return `400`. Login, registration and password changes may return `429` when too many password computations are running; login also limits active sessions. Sessions are kept in memory and disappear on restart. Logout revokes only the supplied token.
612
+
613
+ OpenAPI describes all auth routes and their Bearer token requirements. In Swagger UI, paste a token from login into **Authorize**. For schema exports, enable auth in the configuration and run `npx deep-json-server server.config.js --generate-only`; the users file is not read during generation. Auth methods are exposed through REST. GraphQL checks the same token when changing records. GraphQL and OpenAPI require `database.schema`.
614
+
615
+ ## Record dates, deletion and ownership
616
+
617
+ Set defaults at the schema root. Here `Note` inherits soft deletion and disables timestamps:
618
+
619
+ ```json
620
+ {
621
+ "timestamps": true,
622
+ "softDelete": true,
623
+ "models": {
624
+ "Note": {
625
+ "collection": "notes",
626
+ "timestamps": false,
627
+ "fields": {
628
+ "id": {
629
+ "type": "string",
630
+ "primary": true,
631
+ "generated": "uuid"
632
+ },
633
+ "text": {
634
+ "type": "string",
635
+ "required": true
636
+ }
637
+ }
638
+ }
639
+ }
640
+ }
335
641
  ```
336
642
 
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:
643
+ Precedence: model schema root `false`. Explicit `false` disables an inherited setting. Without a schema, timestamps and soft deletion are disabled.
644
+
645
+ | Field | Enabled by | Meaning |
646
+ |---|---|---|
647
+ | `createdAt`, `updatedAt` | `timestamps` | Creation and last update time |
648
+ | `deletedAt` | `softDelete` | Deletion time, or `null` for an active record |
649
+ | `createdById`, `updatedById` | `auth` | Creator/owner and last editor |
650
+ | `deletedById` | `auth` + `softDelete` | User who deleted the record, or `null` |
338
651
 
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.
652
+ Dates are UTC ISO 8601 strings. New records receive the same creation and update time and, with auth, the authenticated user's ID as creator and editor. PUT/PATCH preserve the creator and creation time. DELETE updates the deletion fields and the enabled last-update fields. Existing records with unknown dates or authors expose `null`. These fields are read-only in REST and GraphQL; embedded plain objects do not receive their own audit fields. Previously stored audit values are retained when their features are disabled.
344
653
 
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.
654
+ With soft deletion, DELETE retains the record in the database. Repeating DELETE on an already deleted record leaves its deletion details unchanged. Fetching by primary key returns deleted records too. A successful PUT/PATCH restores the record by clearing `deletedAt` and `deletedById`. An empty PATCH restores it without replacing other fields; PUT requires all mandatory fields.
655
+
656
+ Lists return active records by default. To select deleted records, specify `deletedAt` in `where`:
657
+
658
+ ```json
659
+ [
660
+ { "id": true, "deletedAt": true },
661
+ { "where": { "deletedAt": { "ne": null } } }
662
+ ]
663
+ ```
346
664
 
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.
665
+ Use `eq: null` for active records, `ne: null` for deleted records, or combine both with `or` for all records. The same filters work in GraphQL. An explicit `deletedAt` condition, including one inside `and`, `or` or `not`, replaces the default at that level. Relation filters and relation lists apply this rule independently. Singular relations hide deleted targets; primary-key queries can still retrieve them directly.
666
+
667
+ Cascade deletion follows `onDelete` and each affected model's `softDelete`. Restoring the record that initiated a cascade also restores records deleted by that operation, excluding those deleted earlier. Physically deleted records cannot be recovered. If a cascade removed an embedded object, restoration requires the affected object field to remain unchanged since deletion. A conflict or missing required relation rejects the whole operation.
668
+
669
+ Restoration metadata is stored with the records and survives restarts; include it when backing up the database. The internal `djsDeletion` field is reserved and is not exposed by either API.
670
+
671
+ With auth, any authenticated user can create records. Updating, deleting and restoring require ownership through `createdById` or `isAdmin: true`. Only administrators can change unowned records. Administrator edits preserve the original owner. The rules cover nested writes, changes to relation storage keys, cascades and restoration. Linking an existing record without changing it does not require owning it. Each mutation is atomic: denied changes leave all affected records unchanged.
672
+
673
+ REST returns 401 for an invalid or missing token and 403 for insufficient permissions. GraphQL applies the same rules to mutations and returns `UNAUTHENTICATED` or `FORBIDDEN`; obtain the token through REST login and send `Authorization: Bearer <token>`. GraphQL reads remain public, as do OPTIONS requests and every file operation.
348
674
 
349
675
  ## Files
350
676
 
351
- Add `files.directory` and `files.metadata` to the server config, then pass `--files` to enable raw binary uploads:
677
+ Files are available through REST and documented in OpenAPI. Add storage to the configuration:
678
+
679
+ ```js
680
+ export default {
681
+ storage: 'file',
682
+ database: { source: './database.json' },
683
+ files: { source: './uploads' },
684
+ };
685
+ ```
686
+
687
+ Start the server:
352
688
 
353
689
  ```bash
354
- deep-json-server --files server.config.js
690
+ npx deep-json-server server.config.js
355
691
  ```
356
692
 
357
- 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.
693
+ For temporary tests, choose `storage: 'memory'` and pass an array in `files.source`. 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.
358
694
 
359
695
  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:
360
696
 
@@ -405,178 +741,61 @@ Content-Type: application/json
405
741
  }
406
742
  ```
407
743
 
408
- `PATCH` returns the updated metadata with status `200`; if a file already exists at the new path, the server 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 server origin.
409
-
410
- 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 and keeps validated metadata in memory while running. Use a disk-backed database and file storage from only one server process at a time, and do not edit stored files or metadata until that process stops. Paths below `files.directory` may not contain symbolic links, and file names are restricted to values that are portable across supported operating systems. The metadata file may be absent initially and is created on the first upload. Metadata created by versions before this path-based API is not compatible with the new format.
411
-
412
- 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
-
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:
744
+ `PATCH` returns the updated metadata with status `200`; if a file already exists at the new path, the server 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.source`, and all returned URLs are relative to the server origin.
443
745
 
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 |
746
+ In disk mode, the binary is stored at `<files.source>/<directory>/<name>`. Metadata defaults to `<files.source>/.files.json`; set `files.metadata` for another location. Directories and the metadata file are created when needed.
451
747
 
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.
748
+ Use one server process per disk database and file store. Stop it before editing stored files or metadata manually. Storage paths cannot contain symbolic links. Uploads and renames cannot overwrite the database, counters, schema, auth users, loaded configuration or metadata file.
453
749
 
454
- Set `openapi.path` in the server config, then generate an OpenAPI 3.0.3 file and exit:
750
+ Send the file as a binary request body. In a browser, use `xhr.send(file)` and track progress through `XMLHttpRequest.upload.onprogress`. 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.
455
751
 
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.
752
+ ## Programmatic API
485
753
 
486
- Use `name` when a resource needs an explicit schema name instead of the automatically singularized name:
754
+ ```js
755
+ import { createServer } from '@kollors/deep-json-server/server';
756
+ import config from './server.config.js';
487
757
 
488
- ```json
489
- {
490
- "$schema": {
491
- "equipment": {
492
- "name": "Equipment"
493
- }
494
- }
495
- }
758
+ const facade = await createServer(config);
759
+ const server = facade.fastify();
760
+ await server.listen();
761
+ // await server.close();
496
762
  ```
497
763
 
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.
764
+ The `openapi()` and `graphql()` methods return schemas and require `database.schema`. `fastify()` returns the server instance for configuration and startup. The database and enabled services initialize on `ready()`, `listen()` or the first `inject()`; initialization errors stop startup.
499
765
 
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`.
766
+ `createServer(config)` takes one argument. Configuration sections control modules exactly as in the CLI. The `openapi()` and `graphql()` methods require their respective sections. They return schemas without writing files.
501
767
 
502
- ## Programmatic API
768
+ The root import `@kollors/deep-json-server` also provides these functions. Server adapters load when enabled. Generators can be used independently:
503
769
 
504
770
  ```js
505
- import { createServer } from '@kollors/deep-json-server';
771
+ import { generateOpenapi, writeOpenapi } from '@kollors/deep-json-server/openapi';
772
+ import { generateGraphql, writeGraphql } from '@kollors/deep-json-server/graphql';
506
773
 
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();
774
+ const document = await generateOpenapi('./schema.json', { files: true });
775
+ const sdl = await generateGraphql('./schema.json');
776
+ await writeOpenapi(document, './generated/openapi.yaml');
777
+ await writeGraphql(sdl, './generated/schema.graphql');
778
+ ```
540
779
 
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();
780
+ Standalone generators accept a schema path or object without a server configuration. The selected function supplies the default format; schema and model `api` settings can restrict it. Timestamps and soft deletion come from the schema. `{ auth: true }` adds ownership fields; OpenAPI also describes auth routes and token requirements. `hashPassword()` is available from the root package.
544
781
 
545
- await runningFastify.listen();
782
+ `generateOpenapi()` also accepts `host`, `port`, `pageSize`, `maxPageSize` and `info`. Pass a schema object instead of a path if preferred. Servers and generators use their own copy of the model. Pagination sizes must be positive integers; `pageSize` cannot exceed `maxPageSize`.
546
783
 
547
- // Later, during application shutdown:
548
- await runningFastify.close();
784
+ ## Data storage
549
785
 
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
- });
786
+ Updates run sequentially within one server instance and are validated on a copy of the data before saving. Use one server process 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.
560
787
 
561
- const memoryFastify = memoryServer.fastify();
562
- const memoryResponse = await memoryFastify.inject({ method: 'GET', url: '/movies/1' });
788
+ ## Development
563
789
 
564
- console.log(memoryResponse.json());
790
+ Source modules: `rest`, `graphql`, `openapi`, `auth`, `files`, `cli` and `server`. Shared models, storage, queries and mutation rules live in `core`. `server` connects the modules; API generators load independently of the HTTP runtime.
565
791
 
566
- await memoryFastify.close();
792
+ ```sh
793
+ npm ci
794
+ npm run verify
567
795
  ```
568
796
 
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.
797
+ The command checks types, code style, test coverage and installation from the package archive.
579
798
 
580
- ## Scope and security
799
+ To publish a new alpha, update the version in `package.json`, `package-lock.json` and `src/core/constants.ts`, then push to `main`. GitHub Actions creates the version tag and publishes to npm `alpha` through trusted publishing. Already published versions are skipped. If the tag exists but publication failed, a retry uses that tag and verifies that the package files match it. Pushing a version tag also triggers publication; stable versions publish to `latest`.
581
800
 
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.
801
+ License: MIT.