@kollors/deep-json-server 1.0.0-alpha.6 → 1.0.0-alpha.7

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 (211) hide show
  1. package/README.md +344 -230
  2. package/README.ru.md +341 -227
  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 +10 -8
  6. package/dist/index.js +3 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/src/auth/contract.d.ts +3 -2
  9. package/dist/src/auth/contract.js.map +1 -1
  10. package/dist/src/auth/password.d.ts +9 -0
  11. package/dist/src/auth/password.js +12 -0
  12. package/dist/src/auth/password.js.map +1 -1
  13. package/dist/src/auth/routes.d.ts +3 -0
  14. package/dist/src/auth/routes.js +3 -0
  15. package/dist/src/auth/routes.js.map +1 -1
  16. package/dist/src/auth/service.d.ts +21 -0
  17. package/dist/src/auth/service.js +38 -7
  18. package/dist/src/auth/service.js.map +1 -1
  19. package/dist/src/cli/index.d.ts +7 -0
  20. package/dist/src/{cli.js → cli/index.js} +32 -14
  21. package/dist/src/cli/index.js.map +1 -0
  22. package/dist/src/{constants.d.ts → core/constants.d.ts} +1 -1
  23. package/dist/src/{constants.js → core/constants.js} +1 -1
  24. package/dist/src/core/constants.js.map +1 -0
  25. package/dist/src/core/database.d.ts +50 -0
  26. package/dist/src/{database.js → core/database.js} +21 -1
  27. package/dist/src/core/database.js.map +1 -0
  28. package/dist/src/core/engine.d.ts +64 -0
  29. package/dist/src/{engine.js → core/engine.js} +64 -18
  30. package/dist/src/core/engine.js.map +1 -0
  31. package/dist/src/{errors.d.ts → core/errors.d.ts} +4 -1
  32. package/dist/src/core/errors.js +12 -0
  33. package/dist/src/core/errors.js.map +1 -0
  34. package/dist/src/core/http-errors.d.ts +9 -0
  35. package/dist/src/core/http-errors.js +15 -0
  36. package/dist/src/core/http-errors.js.map +1 -0
  37. package/dist/src/core/lifecycle/mutation.d.ts +49 -0
  38. package/dist/src/core/lifecycle/mutation.js +170 -0
  39. package/dist/src/core/lifecycle/mutation.js.map +1 -0
  40. package/dist/src/core/lifecycle/options.d.ts +20 -0
  41. package/dist/src/core/lifecycle/options.js +21 -0
  42. package/dist/src/core/lifecycle/options.js.map +1 -0
  43. package/dist/src/core/model.d.ts +130 -0
  44. package/dist/src/{model.js → core/model.js} +106 -10
  45. package/dist/src/core/model.js.map +1 -0
  46. package/dist/src/{mutations → core/mutations}/write.d.ts +3 -2
  47. package/dist/src/{mutations → core/mutations}/write.js +14 -4
  48. package/dist/src/core/mutations/write.js.map +1 -0
  49. package/dist/src/core/pagination.d.ts +10 -0
  50. package/dist/src/{pagination.js → core/pagination.js} +3 -0
  51. package/dist/src/core/pagination.js.map +1 -0
  52. package/dist/src/core/paths.d.ts +16 -0
  53. package/dist/src/{paths.js → core/paths.js} +12 -0
  54. package/dist/src/core/paths.js.map +1 -0
  55. package/dist/src/core/query/contract.d.ts +6 -0
  56. package/dist/src/{query → core/query}/contract.js +3 -0
  57. package/dist/src/core/query/contract.js.map +1 -0
  58. package/dist/src/core/query/filter.d.ts +6 -0
  59. package/dist/src/{query → core/query}/filter.js +18 -4
  60. package/dist/src/core/query/filter.js.map +1 -0
  61. package/dist/src/core/query/options.d.ts +30 -0
  62. package/dist/src/{query → core/query}/options.js +12 -0
  63. package/dist/src/core/query/options.js.map +1 -0
  64. package/dist/src/core/records.d.ts +46 -0
  65. package/dist/src/{records.js → core/records.js} +24 -3
  66. package/dist/src/core/records.js.map +1 -0
  67. package/dist/src/core/relation-metadata.d.ts +15 -0
  68. package/dist/src/{relation-metadata.js → core/relation-metadata.js} +7 -1
  69. package/dist/src/core/relation-metadata.js.map +1 -0
  70. package/dist/src/core/types.d.ts +10 -0
  71. package/dist/src/{types.js.map → core/types.js.map} +1 -1
  72. package/dist/src/core/utils.d.ts +56 -0
  73. package/dist/src/core/utils.js +102 -0
  74. package/dist/src/core/utils.js.map +1 -0
  75. package/dist/src/files/contract.d.ts +33 -0
  76. package/dist/src/files/contract.js +25 -3
  77. package/dist/src/files/contract.js.map +1 -1
  78. package/dist/src/files/disk-store.d.ts +3 -0
  79. package/dist/src/files/disk-store.js +27 -3
  80. package/dist/src/files/disk-store.js.map +1 -1
  81. package/dist/src/files/http.d.ts +8 -3
  82. package/dist/src/files/http.js +9 -0
  83. package/dist/src/files/http.js.map +1 -1
  84. package/dist/src/files/index.d.ts +4 -3
  85. package/dist/src/files/index.js +3 -1
  86. package/dist/src/files/index.js.map +1 -1
  87. package/dist/src/files/memory-store.d.ts +4 -1
  88. package/dist/src/files/memory-store.js +8 -2
  89. package/dist/src/files/memory-store.js.map +1 -1
  90. package/dist/src/files/routes.d.ts +3 -0
  91. package/dist/src/files/routes.js +25 -1
  92. package/dist/src/files/routes.js.map +1 -1
  93. package/dist/src/graphql/entry.d.ts +2 -2
  94. package/dist/src/graphql/entry.js.map +1 -1
  95. package/dist/src/graphql/lazy.d.ts +10 -0
  96. package/dist/src/graphql/lazy.js +13 -0
  97. package/dist/src/graphql/lazy.js.map +1 -0
  98. package/dist/src/graphql/preflight.d.ts +4 -1
  99. package/dist/src/graphql/preflight.js +3 -0
  100. package/dist/src/graphql/preflight.js.map +1 -1
  101. package/dist/src/graphql/public.d.ts +10 -2
  102. package/dist/src/graphql/public.js +10 -4
  103. package/dist/src/graphql/public.js.map +1 -1
  104. package/dist/src/graphql/resolvers.d.ts +10 -2
  105. package/dist/src/graphql/resolvers.js +8 -2
  106. package/dist/src/graphql/resolvers.js.map +1 -1
  107. package/dist/src/graphql/routes.d.ts +6 -2
  108. package/dist/src/graphql/routes.js +6 -3
  109. package/dist/src/graphql/routes.js.map +1 -1
  110. package/dist/src/graphql/schema.d.ts +6 -0
  111. package/dist/src/{graphql.js → graphql/schema.js} +14 -11
  112. package/dist/src/graphql/schema.js.map +1 -0
  113. package/dist/src/graphql/write.d.ts +3 -0
  114. package/dist/src/graphql/write.js +3 -0
  115. package/dist/src/graphql/write.js.map +1 -1
  116. package/dist/src/openapi/auth.d.ts +13 -0
  117. package/dist/src/{auth/openapi.js → openapi/auth.js} +15 -5
  118. package/dist/src/openapi/auth.js.map +1 -0
  119. package/dist/src/openapi/document.d.ts +5 -2
  120. package/dist/src/openapi/document.js +29 -33
  121. package/dist/src/openapi/document.js.map +1 -1
  122. package/dist/src/openapi/entry.d.ts +2 -2
  123. package/dist/src/openapi/entry.js.map +1 -1
  124. package/dist/src/openapi/files.d.ts +6 -0
  125. package/dist/src/{files/openapi.js → openapi/files.js} +6 -3
  126. package/dist/src/openapi/files.js.map +1 -0
  127. package/dist/src/openapi/helpers.d.ts +10 -1
  128. package/dist/src/openapi/helpers.js +9 -0
  129. package/dist/src/openapi/helpers.js.map +1 -1
  130. package/dist/src/openapi/index.d.ts +7 -1
  131. package/dist/src/openapi/index.js +14 -5
  132. package/dist/src/openapi/index.js.map +1 -1
  133. package/dist/src/openapi/lazy.d.ts +11 -0
  134. package/dist/src/openapi/lazy.js +13 -0
  135. package/dist/src/openapi/lazy.js.map +1 -0
  136. package/dist/src/openapi/public.d.ts +10 -2
  137. package/dist/src/openapi/public.js +9 -3
  138. package/dist/src/openapi/public.js.map +1 -1
  139. package/dist/src/{types.d.ts → openapi/types.d.ts} +1 -10
  140. package/dist/src/openapi/types.js +2 -0
  141. package/dist/src/openapi/types.js.map +1 -0
  142. package/dist/src/rest/options.d.ts +11 -2
  143. package/dist/src/rest/options.js +12 -3
  144. package/dist/src/rest/options.js.map +1 -1
  145. package/dist/src/rest/projection.d.ts +10 -4
  146. package/dist/src/rest/projection.js +10 -4
  147. package/dist/src/rest/projection.js.map +1 -1
  148. package/dist/src/rest/routes.d.ts +6 -2
  149. package/dist/src/rest/routes.js +7 -3
  150. package/dist/src/rest/routes.js.map +1 -1
  151. package/dist/src/server/config.d.ts +79 -0
  152. package/dist/src/{config.js → server/config.js} +57 -19
  153. package/dist/src/server/config.js.map +1 -0
  154. package/dist/src/server/create.d.ts +18 -0
  155. package/dist/src/{server.js → server/create.js} +31 -23
  156. package/dist/src/server/create.js.map +1 -0
  157. package/dist/src/server/features.d.ts +15 -0
  158. package/dist/src/{features.js → server/features.js} +8 -2
  159. package/dist/src/server/features.js.map +1 -0
  160. package/dist/src/server/public.d.ts +3 -3
  161. package/dist/src/server/public.js +1 -1
  162. package/dist/src/server/public.js.map +1 -1
  163. package/package.json +1 -1
  164. package/dist/src/auth/openapi.d.ts +0 -10
  165. package/dist/src/auth/openapi.js.map +0 -1
  166. package/dist/src/cli.d.ts +0 -4
  167. package/dist/src/cli.js.map +0 -1
  168. package/dist/src/config.d.ts +0 -86
  169. package/dist/src/config.js.map +0 -1
  170. package/dist/src/constants.js.map +0 -1
  171. package/dist/src/database.d.ts +0 -20
  172. package/dist/src/database.js.map +0 -1
  173. package/dist/src/engine.d.ts +0 -36
  174. package/dist/src/engine.js.map +0 -1
  175. package/dist/src/errors.js +0 -9
  176. package/dist/src/errors.js.map +0 -1
  177. package/dist/src/features.d.ts +0 -9
  178. package/dist/src/features.js.map +0 -1
  179. package/dist/src/files/openapi.d.ts +0 -3
  180. package/dist/src/files/openapi.js.map +0 -1
  181. package/dist/src/graphql.d.ts +0 -3
  182. package/dist/src/graphql.js.map +0 -1
  183. package/dist/src/http/errors.d.ts +0 -6
  184. package/dist/src/http/errors.js +0 -3
  185. package/dist/src/http/errors.js.map +0 -1
  186. package/dist/src/model.d.ts +0 -72
  187. package/dist/src/model.js.map +0 -1
  188. package/dist/src/mutations/write.js.map +0 -1
  189. package/dist/src/pagination.d.ts +0 -7
  190. package/dist/src/pagination.js.map +0 -1
  191. package/dist/src/paths.d.ts +0 -7
  192. package/dist/src/paths.js.map +0 -1
  193. package/dist/src/query/contract.d.ts +0 -3
  194. package/dist/src/query/contract.js.map +0 -1
  195. package/dist/src/query/filter.d.ts +0 -3
  196. package/dist/src/query/filter.js.map +0 -1
  197. package/dist/src/query/options.d.ts +0 -18
  198. package/dist/src/query/options.js.map +0 -1
  199. package/dist/src/records.d.ts +0 -25
  200. package/dist/src/records.js.map +0 -1
  201. package/dist/src/relation-metadata.d.ts +0 -9
  202. package/dist/src/relation-metadata.js.map +0 -1
  203. package/dist/src/schema.d.ts +0 -8
  204. package/dist/src/schema.js +0 -13
  205. package/dist/src/schema.js.map +0 -1
  206. package/dist/src/server.d.ts +0 -12
  207. package/dist/src/server.js.map +0 -1
  208. package/dist/src/utils.d.ts +0 -12
  209. package/dist/src/utils.js +0 -58
  210. package/dist/src/utils.js.map +0 -1
  211. /package/dist/src/{types.js → core/types.js} +0 -0
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  [Русский](README.ru.md)
4
4
 
5
- A JSON-backed mock server with REST, GraphQL, nested queries, binary files and schema exports. Requires Node.js 22 or newer.
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
- **1.0.0-alpha.6 is a prerelease.** REST queries use `scope=[fields, arguments?]` at every level. When upgrading from an earlier version, update query parameters using the examples below; upgrading from 0.x also requires the new model schema.
7
+ **1.0.0-alpha.7 is a prerelease.** REST queries use `scope=[fields, arguments?]` at every level. When upgrading from an earlier version, update query parameters using the examples below; upgrading from 0.x also requires the new model schema.
8
8
 
9
9
  ## Installation
10
10
 
@@ -12,7 +12,7 @@ A JSON-backed mock server with REST, GraphQL, nested queries, binary files and s
12
12
  npm install @kollors/deep-json-server@alpha
13
13
  ```
14
14
 
15
- To install a specific version, use `@1.0.0-alpha.6`.
15
+ To install a specific version, use `@1.0.0-alpha.7`.
16
16
 
17
17
  ## Quick start
18
18
 
@@ -42,12 +42,16 @@ npx deep-json-server server.config.js
42
42
 
43
43
  The user list is available at `http://127.0.0.1:4001/users`.
44
44
 
45
+ 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.
46
+
45
47
  ## Configuration
46
48
 
47
49
  | Setting | Meaning |
48
50
  |---|---|
49
- | `database.path` / `database.data` | Exactly one: JSON file or in-memory collection object |
51
+ | `database.path` / `database.data` | Choose one to start the server: a JSON file or an in-memory collection object |
50
52
  | `database.schema` | Model object or JSON schema-file path; optional for REST |
53
+ | `database.timestamps` | Add creation and update times; default `false` |
54
+ | `database.softDelete` | Mark records as deleted and allow restoration; default `false` |
51
55
  | `openapi.enabled` | Enable the specification endpoint; default `false` |
52
56
  | `openapi.endpoint` | Specification path; default `/openapi.json` |
53
57
  | `openapi.path` | YAML export destination |
@@ -55,7 +59,6 @@ The user list is available at `http://127.0.0.1:4001/users`.
55
59
  | `graphql.enabled` | Enable GraphQL HTTP endpoint; default `false` |
56
60
  | `graphql.endpoint` | Endpoint path; default `/graphql` |
57
61
  | `graphql.path` | GraphQL SDL export destination |
58
- | `auth.enabled` | Enable authentication; default `false` |
59
62
  | `auth.users` | Path to a JSON array of auth users, or an in-memory array |
60
63
  | `auth.expiresIn` | Session lifetime in seconds; default 3600 |
61
64
  | `server.host`, `server.port` | Defaults `127.0.0.1`, `4001`; CLI also reads `HOST`/`PORT` |
@@ -69,38 +72,21 @@ Relative paths resolve from the configuration file's directory. When passing a c
69
72
 
70
73
  Set `server.port` to `0` to let the operating system choose an available port. The OpenAPI endpoint uses a relative server URL.
71
74
 
75
+ ### CLI
76
+
72
77
  | CLI flag | Action |
73
78
  |---|---|
74
- | `--files` | Enable file routes |
79
+ | `--files` | Enable file routes; requires `files` configuration |
80
+ | `--timestamps` | Enable record timestamps globally |
81
+ | `--soft-delete` | Enable soft deletion globally |
75
82
  | `--graphql` | Enable the GraphQL API |
76
83
  | `--openapi` | Enable the OpenAPI endpoint |
77
- | `--auth` | Enable authentication using `auth.users` |
78
84
  | `--host <host>` | Server address |
79
85
  | `--port <port>` | Server port |
80
86
  | `--help`, `-h` | Show help |
81
87
  | `--version`, `-v` | Show package version |
82
88
 
83
- Setting priority: CLI → configuration → `HOST`/`PORT` → defaults. File routes are enabled when `files` is configured.
84
-
85
- To generate schemas, specify the format and configuration file:
86
-
87
- ```sh
88
- npx deep-json-server generate openapi server.config.js
89
- npx deep-json-server generate graphql server.config.js
90
- npx deep-json-server generate openapi,graphql server.config.js
91
- ```
92
-
93
- The command reads `database.schema` and writes schemas to `openapi.path` and `graphql.path`. A configuration for generation only can contain:
94
-
95
- ```js
96
- export default {
97
- database: { schema: './schema.json' },
98
- openapi: { path: './generated/openapi.yaml' },
99
- graphql: { path: './generated/schema.graphql' },
100
- };
101
- ```
102
-
103
- Each format needs its own output file. The command rejects destinations that would overwrite the configuration, database, schema, auth users or file metadata.
89
+ Host and port priority: CLI → configuration → `HOST`/`PORT` → defaults. The `files` section enables file routes; `auth` enables login and permission checks. Model settings can override global `timestamps` and `softDelete` values.
104
90
 
105
91
  ## Model schema
106
92
 
@@ -142,6 +128,8 @@ Explicit schemas are strict: undeclared fields and collections are rejected, exc
142
128
 
143
129
  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.
144
130
 
131
+ 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.
132
+
145
133
  ### Fields
146
134
 
147
135
  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`.
@@ -172,10 +160,12 @@ Objects used in GraphQL must have at least one field visible in responses; REST
172
160
  ### Relations
173
161
 
174
162
  ```json
175
- "actors.genres": {
176
- "type": "Genre[]",
177
- "source": "actors.genreIds",
178
- "required": true
163
+ {
164
+ "actors.genres": {
165
+ "type": "Genre[]",
166
+ "source": "actors.genreIds",
167
+ "required": true
168
+ }
179
169
  }
180
170
  ```
181
171
 
@@ -183,19 +173,25 @@ Objects used in GraphQL must have at least one field visible in responses; REST
183
173
 
184
174
  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.
185
175
 
186
- 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.
176
+ 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.
187
177
 
188
178
  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`.
189
179
 
190
180
  `onDelete` describes what happens **when a target record is deleted**:
191
181
 
192
- - `restrict` (default): refuse deletion while a surviving owner refers to the target.
193
- - `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.
182
+ - `restrict` (default): refuse deletion while a retained record refers to the target.
183
+ - `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.
194
184
 
195
185
  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.
196
186
 
197
187
  ## Queries and responses
198
188
 
189
+ The movie, actor and genre examples use the full [example schema](examples/schema.json). Run them with the [example configuration](examples/server.config.js):
190
+
191
+ ```sh
192
+ npx deep-json-server examples/server.config.js
193
+ ```
194
+
199
195
  Collections and lists of objects, including embedded `object[]` fields, return:
200
196
 
201
197
  ```json
@@ -206,7 +202,7 @@ Primitive arrays are returned as plain arrays. Every object list accepts optiona
206
202
 
207
203
  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`.
208
204
 
209
- `where` uses field operators `eq`, `ne`, `in`, string `contains`/`startsWith`/`endsWith`, and comparisons `gt`, `gte`, `lt`, `lte`. Combine conditions with `and`, `or`, `not`. Arrays support `some`, `every`, `none`; primitive arrays also support `contains`, `in`. String matching is case-insensitive. A field condition is an object containing an operator, such as `{ "id": { "eq": "1" } }`.
205
+ `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" } }`.
210
206
 
211
207
  ```json
212
208
  {
@@ -224,10 +220,12 @@ Use `page` and `pageSize` in `pager`. The default is the first page with the siz
224
220
 
225
221
  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.
226
222
 
227
- `order` is an array of `{ "field": "fullName", "direction": "ASC" }` rules. 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`.
223
+ `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`.
228
224
 
229
225
  ### REST
230
226
 
227
+ `GET /` returns collection names: `{ "resources": ["users", "movies"] }`. Each collection has these routes:
228
+
231
229
  | Method | Path | Operation |
232
230
  |---|---|---|
233
231
  | GET | `/users` | `userList` |
@@ -237,62 +235,13 @@ Root `where` selects records from the main collection. `where` inside a relation
237
235
  | PATCH | `/users/{id}` | `userUpdate` |
238
236
  | DELETE | `/users/{id}` | `userDelete` |
239
237
 
240
- 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`. DELETE returns the deleted record.
238
+ 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`.
241
239
 
242
- ### Nested writes
243
-
244
- Storage keys such as `genreIds: ["1"]` only set a relation. Relation fields also accept records to create or update:
245
-
246
- ```http
247
- PATCH /movies/1
248
- Content-Type: application/json
249
-
250
- {
251
- "actors": [
252
- {
253
- "userId": "1",
254
- "genres": [
255
- "1",
256
- { "id": "2", "name": "Updated genre" },
257
- { "name": "New genre" }
258
- ]
259
- }
260
- ]
261
- }
262
- ```
263
-
264
- | Relation value | Behavior |
265
- |---|---|
266
- | A key, such as `"1"` | Link an existing record without changing it |
267
- | An object with a primary key | PATCH updates supplied fields; PUT replaces the related record |
268
- | An object without a primary key | Create a related record with defaults and a generated key |
269
-
270
- 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.
271
-
272
- 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.
273
-
274
- 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.
275
-
276
- 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.
277
-
278
- GraphQL accepts typed objects in relation fields. To change only the links in a replace mutation, use storage keys such as `genreIds`. For example:
279
-
280
- ```graphql
281
- mutation {
282
- movieUpdate(id: "1", data: {
283
- actors: [{
284
- userId: "1"
285
- genres: [{ id: "2", name: "Updated genre" }, { name: "New genre" }]
286
- }]
287
- }) {
288
- actors { data { genres { data { id name } } } }
289
- }
290
- }
291
- ```
240
+ 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" }`.
292
241
 
293
242
  ### REST query parameters
294
243
 
295
- REST accepts 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.
244
+ 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.
296
245
 
297
246
  Select users and their movies with independent ordering and pagination:
298
247
 
@@ -344,13 +293,71 @@ Omitting `scope` returns own fields, as with `[{"*":true}]`. An empty selection
344
293
 
345
294
  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.
346
295
 
347
- OpenAPI remains at version 3.0.3. It cannot define a separate schema for each array position: the documentation describes the elements, and the server strictly validates their order.
296
+ ### Nested writes
297
+
298
+ Storage keys such as `genreIds: ["1"]` only set a relation. Relation fields also accept records to create or update:
299
+
300
+ ```http
301
+ PATCH /movies/1
302
+ Content-Type: application/json
303
+
304
+ {
305
+ "actors": [
306
+ {
307
+ "userId": "1",
308
+ "genres": [
309
+ "1",
310
+ { "id": "2", "name": "Updated genre" },
311
+ { "name": "New genre" }
312
+ ]
313
+ }
314
+ ]
315
+ }
316
+ ```
317
+
318
+ | Relation value | Behavior |
319
+ |---|---|
320
+ | A key, such as `"1"` | Link an existing record without changing it |
321
+ | An object with a primary key | PATCH updates supplied fields; PUT replaces the related record |
322
+ | An object without a primary key | Create a related record with defaults and a generated key |
323
+
324
+ 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.
325
+
326
+ 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.
327
+
328
+ 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.
329
+
330
+ 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.
331
+
332
+ GraphQL accepts typed objects in relation fields. To change only the links in a replace mutation, use storage keys such as `genreIds`. For example:
333
+
334
+ ```graphql
335
+ mutation {
336
+ movieUpdate(id: "1", data: {
337
+ actors: [{
338
+ userId: "1"
339
+ genres: [{ id: "2", name: "Updated genre" }, { name: "New genre" }]
340
+ }]
341
+ }) {
342
+ actors { data { genres { data { id name } } } }
343
+ }
344
+ }
345
+ ```
348
346
 
349
347
  ### GraphQL
350
348
 
349
+ Set `database.schema` and enable `graphql.enabled: true` in the configuration, or start the server with `--graphql`:
350
+
351
+ ```sh
352
+ npx deep-json-server --graphql server.config.js
353
+ ```
354
+
355
+ Send requests to `/graphql` using POST with `Content-Type: application/json` and a body of `{ "query": "…", "variables": {} }`. Change the path through `graphql.endpoint`.
356
+
351
357
  ```graphql
352
358
  query {
353
359
  userList(
360
+ where: { fullName: { contains: "Мира" } }
354
361
  order: [{ field: fullName, direction: ASC }]
355
362
  pager: { page: 1, pageSize: 20 }
356
363
  ) {
@@ -358,7 +365,11 @@ query {
358
365
  data {
359
366
  id
360
367
  fullName
361
- movies(order: [{ field: title, direction: ASC }], pager: { pageSize: 5 }) {
368
+ movies(
369
+ where: { title: { contains: "Тени" } }
370
+ order: [{ field: title, direction: ASC }]
371
+ pager: { pageSize: 5 }
372
+ ) {
362
373
  total
363
374
  data { id title }
364
375
  }
@@ -369,7 +380,243 @@ query {
369
380
 
370
381
  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.
371
382
 
372
- 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. Errors include `extensions.code`: `INVALID_INPUT`, `INVALID_QUERY`, `NOT_FOUND`, `CONFLICT` or `INTERNAL_ERROR`.
383
+ 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.
384
+
385
+ 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.
386
+
387
+ ## OpenAPI and schema exports
388
+
389
+ Exports use OpenAPI 3.0.3. The purpose of each `scope` array position is described in text; the server validates their order.
390
+
391
+ To serve the specification over HTTP, set `database.schema` and enable `openapi.enabled: true`, or start the server with `--openapi`. The JSON document is available at `/openapi.json` by default; change the path through `openapi.endpoint`. Open it in a separately installed Swagger UI or import it into an API client.
392
+
393
+ To generate schemas, specify the format and configuration file:
394
+
395
+ ```sh
396
+ npx deep-json-server generate openapi server.config.js
397
+ npx deep-json-server generate graphql server.config.js
398
+ npx deep-json-server generate openapi,graphql server.config.js
399
+ ```
400
+
401
+ Generation runs without starting the server or reading database records. The command reads `database.schema` and writes schemas to `openapi.path` and `graphql.path`. The `enabled` flags control HTTP endpoints and are not required for exports. A configuration for generation only can contain:
402
+
403
+ ```js
404
+ export default {
405
+ database: { schema: './schema.json' },
406
+ openapi: { path: './generated/openapi.yaml' },
407
+ graphql: { path: './generated/schema.graphql' },
408
+ };
409
+ ```
410
+
411
+ Each format needs its own output file. The command rejects destinations that would overwrite the configuration, database, schema, auth users or file metadata.
412
+
413
+ ## Authentication
414
+
415
+ The `auth` section enables REST login and protects record changes in REST and GraphQL. Reads and all file operations remain public.
416
+
417
+ Create an auth user in a separate file with `setup-auth.mjs`:
418
+
419
+ ```js
420
+ import { writeFile } from 'node:fs/promises';
421
+ import { hashPassword } from '@kollors/deep-json-server/auth';
422
+
423
+ const password = process.env.DJS_PASSWORD;
424
+ if (!password) throw new Error('Set DJS_PASSWORD');
425
+ await writeFile('./auth.json', JSON.stringify([
426
+ { id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true },
427
+ ], null, 2), { flag: 'wx', mode: 0o600 });
428
+ ```
429
+
430
+ Set `DJS_PASSWORD` and run `node setup-auth.mjs`. Add the file to your server configuration:
431
+
432
+ ```js
433
+ export default {
434
+ database: { path: './database.json' },
435
+ auth: { users: './auth.json', expiresIn: 3600 },
436
+ };
437
+ ```
438
+
439
+ Start with `npx deep-json-server server.config.js`. The presence of `auth` enables the module; omit the section to leave record changes open. Each 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. Auth users are stored separately from database collections. The file is read at startup; restart the server after editing it.
440
+
441
+ | REST request | Input | Response |
442
+ |---|---|---|
443
+ | `POST /auth/login` | JSON `{ "username": "admin", "password": "…" }` | `{ accessToken, expiresIn, user: { id, username, isAdmin } }` |
444
+ | `GET /auth/me` | Bearer token | `{ id, username, isAdmin }` |
445
+ | `POST /auth/logout` | Bearer token | `{ success: true }` |
446
+
447
+ Pass the token in `Authorization: Bearer <accessToken>`. Incorrect credentials and invalid or expired tokens return HTTP 401. Sessions are kept in memory and disappear on restart; logout revokes the supplied token. Login can return HTTP 429 when the server has too many concurrent login attempts or active sessions.
448
+
449
+ OpenAPI describes Bearer authentication for record changes, `/auth/me` and `/auth/logout`. In Swagger UI, paste a token from login into **Authorize**. For schema exports, enable auth in the configuration and run `generate openapi server.config.js`; the users file is not read during generation. GraphQL and OpenAPI require `database.schema`.
450
+
451
+ ## Record dates, deletion and ownership
452
+
453
+ Configure defaults in `database`:
454
+
455
+ ```js
456
+ export default {
457
+ database: {
458
+ path: './database.json',
459
+ schema: './schema.json',
460
+ timestamps: true,
461
+ softDelete: true,
462
+ },
463
+ auth: { users: './auth.json' },
464
+ };
465
+ ```
466
+
467
+ Each model can override `timestamps` and `softDelete` beside `collection` and `fields`. An omitted setting inherits the global value; `true` or `false` overrides it. CLI flags override global configuration, and model settings take priority over both. Without a schema, global settings apply to every collection. This model disables timestamps and keeps deleted records regardless of the global settings:
468
+
469
+ ```json
470
+ {
471
+ "Note": {
472
+ "collection": "notes",
473
+ "timestamps": false,
474
+ "softDelete": true,
475
+ "fields": {
476
+ "id": { "type": "string", "primary": true, "generated": "uuid" },
477
+ "text": { "type": "string", "required": true }
478
+ }
479
+ }
480
+ }
481
+ ```
482
+
483
+ | Field | Enabled by | Meaning |
484
+ |---|---|---|
485
+ | `createdAt`, `updatedAt` | `timestamps` | Creation and last update time |
486
+ | `deletedAt` | `softDelete` | Deletion time, or `null` for an active record |
487
+ | `createdById`, `updatedById` | `auth` | Creator/owner and last editor |
488
+ | `deletedById` | `auth` + `softDelete` | User who deleted the record, or `null` |
489
+
490
+ 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.
491
+
492
+ 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.
493
+
494
+ Lists return active records by default. To select deleted records, specify `deletedAt` in `where`:
495
+
496
+ ```json
497
+ [
498
+ { "id": true, "deletedAt": true },
499
+ { "where": { "deletedAt": { "ne": null } } }
500
+ ]
501
+ ```
502
+
503
+ 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.
504
+
505
+ 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.
506
+
507
+ 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.
508
+
509
+ 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.
510
+
511
+ 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.
512
+
513
+ ## Files
514
+
515
+ Files are available through REST and documented in OpenAPI. Add storage to the configuration:
516
+
517
+ ```js
518
+ export default {
519
+ database: { path: './database.json' },
520
+ files: { directory: './uploads', metadata: './files.json' },
521
+ };
522
+ ```
523
+
524
+ Start the server:
525
+
526
+ ```bash
527
+ npx deep-json-server server.config.js
528
+ ```
529
+
530
+ 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.
531
+
532
+ 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:
533
+
534
+ ```http
535
+ POST /_files/storage
536
+ Content-Name: shadows-of-ardenia.jpg
537
+ Content-Directory: posters
538
+ Content-Type: image/jpeg
539
+
540
+ <binary body>
541
+ ```
542
+
543
+ A new file returns status `201` and its computed metadata:
544
+
545
+ ```json
546
+ {
547
+ "directory": "posters",
548
+ "downloadUrl": "/_files/download/posters/shadows-of-ardenia.jpg",
549
+ "metadataUrl": "/_files/metadata/posters/shadows-of-ardenia.jpg",
550
+ "mimeType": "image/jpeg",
551
+ "name": "shadows-of-ardenia.jpg",
552
+ "size": 182340,
553
+ "url": "/_files/storage/posters/shadows-of-ardenia.jpg"
554
+ }
555
+ ```
556
+
557
+ The combination of `directory` and `name` identifies a file. Uploading to an existing path returns `409`. Pass `Content-Override: true` to replace it; a successful replacement returns `200`. The server supports these file routes:
558
+
559
+ ```text
560
+ POST /_files/storage Upload or replace a file
561
+ GET /_files/storage/* Return file contents inline
562
+ PATCH /_files/storage/* Rename or move a file
563
+ DELETE /_files/storage/* Delete a file
564
+
565
+ GET /_files/metadata/* Return file metadata as JSON
566
+ GET /_files/download/* Download a file as an attachment
567
+ ```
568
+
569
+ Rename, move, or perform both operations with a JSON body. At least one field is required:
570
+
571
+ ```http
572
+ PATCH /_files/storage/posters/shadows-of-ardenia.jpg
573
+ Content-Type: application/json
574
+
575
+ {
576
+ "directory": "archive/posters",
577
+ "name": "ardenia-shadows.jpg"
578
+ }
579
+ ```
580
+
581
+ `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.
582
+
583
+ In disk mode, the binary is stored at `<files.directory>/<directory>/<name>`. Metadata stores `directory`, `mimeType` and `name`; the server reads the size from the file and builds its URLs. Directories and the metadata file are created when needed.
584
+
585
+ 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.
586
+
587
+ 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.
588
+
589
+ ## Programmatic API
590
+
591
+ ```js
592
+ import { createServer } from '@kollors/deep-json-server/server';
593
+ import config from './server.config.js';
594
+
595
+ const facade = await createServer(config);
596
+ const server = facade.fastify();
597
+ await server.listen();
598
+ // await server.close();
599
+ ```
600
+
601
+ 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.
602
+
603
+ The second argument overrides module settings, for example `createServer(config, { files: false, graphql: true })`. Supported flags are `files`, `graphql`, `openapi` and `auth`. Enabling auth or files requires the corresponding configuration section; GraphQL and OpenAPI require a model schema.
604
+
605
+ The root import `@kollors/deep-json-server` also provides these functions. Server adapters load when enabled. Generators can be used independently:
606
+
607
+ ```js
608
+ import { generateOpenapi, writeOpenapi } from '@kollors/deep-json-server/openapi';
609
+ import { generateGraphql, writeGraphql } from '@kollors/deep-json-server/graphql';
610
+
611
+ const document = await generateOpenapi('./schema.json', { files: true });
612
+ const sdl = await generateGraphql('./schema.json');
613
+ await writeOpenapi(document, './generated/openapi.yaml');
614
+ await writeGraphql(sdl, './generated/schema.graphql');
615
+ ```
616
+
617
+ Both generators accept `timestamps`, `softDelete` and `auth` options for the record fields. With `{ auth: true }`, OpenAPI also includes REST auth routes and mutation security requirements. `hashPassword()` is also available from the root package.
618
+
619
+ `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`.
373
620
 
374
621
  ## Example database
375
622
 
@@ -475,146 +722,13 @@ String primary keys use GraphQL `ID`; ordinary strings use `String`, numbers use
475
722
  }
476
723
  ```
477
724
 
478
- ## Files
479
-
480
- Add `files.directory` and `files.metadata` to the configuration and start the server:
481
-
482
- ```bash
483
- deep-json-server server.config.js
484
- ```
485
-
486
- 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.
487
-
488
- 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:
489
-
490
- ```http
491
- POST /_files/storage
492
- Content-Name: shadows-of-ardenia.jpg
493
- Content-Directory: posters
494
- Content-Type: image/jpeg
495
-
496
- <binary body>
497
- ```
498
-
499
- A new file returns status `201` and its computed metadata:
500
-
501
- ```json
502
- {
503
- "directory": "posters",
504
- "downloadUrl": "/_files/download/posters/shadows-of-ardenia.jpg",
505
- "metadataUrl": "/_files/metadata/posters/shadows-of-ardenia.jpg",
506
- "mimeType": "image/jpeg",
507
- "name": "shadows-of-ardenia.jpg",
508
- "size": 182340,
509
- "url": "/_files/storage/posters/shadows-of-ardenia.jpg"
510
- }
511
- ```
512
-
513
- The combination of `directory` and `name` identifies a file. Uploading to an existing path returns `409`. Pass `Content-Override: true` to replace it; a successful replacement returns `200`. The server supports these file routes:
514
-
515
- ```text
516
- POST /_files/storage Upload or replace a file
517
- GET /_files/storage/* Return file contents inline
518
- PATCH /_files/storage/* Rename or move a file
519
- DELETE /_files/storage/* Delete a file
520
-
521
- GET /_files/metadata/* Return file metadata as JSON
522
- GET /_files/download/* Download a file as an attachment
523
- ```
524
-
525
- Rename, move, or perform both operations with a JSON body. At least one field is required:
526
-
527
- ```http
528
- PATCH /_files/storage/posters/shadows-of-ardenia.jpg
529
- Content-Type: application/json
530
-
531
- {
532
- "directory": "archive/posters",
533
- "name": "ardenia-shadows.jpg"
534
- }
535
- ```
536
-
537
- `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.
538
-
539
- In disk mode, the binary is stored at `<files.directory>/<directory>/<name>`. Metadata stores `directory`, `mimeType` and `name`; the server reads the size from the file and builds its URLs. Directories and the metadata file are created when needed.
540
-
541
- 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.
542
-
543
- 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.
544
-
545
- ## Authentication
546
-
547
- The optional auth module provides REST routes for login, current user and logout. **It does not restrict access to REST records, files or GraphQL.**
548
-
549
- Create an auth user in a separate file with `setup-auth.mjs`:
550
-
551
- ```js
552
- import { writeFile } from 'node:fs/promises';
553
- import { hashPassword } from '@kollors/deep-json-server/auth';
554
-
555
- const password = process.env.DJS_PASSWORD;
556
- if (!password) throw new Error('Set DJS_PASSWORD');
557
- await writeFile('./auth.json', JSON.stringify([
558
- { id: '1', username: 'admin', passwordHash: await hashPassword(password) },
559
- ], null, 2), { flag: 'wx', mode: 0o600 });
560
- ```
561
-
562
- Set `DJS_PASSWORD` and run `node setup-auth.mjs`. Add the file to your server configuration:
563
-
564
- ```js
565
- export default {
566
- database: { path: './database.json' },
567
- auth: { users: './auth.json', expiresIn: 3600 },
568
- };
569
- ```
570
-
571
- Start with `npx deep-json-server --auth server.config.js`, or set `auth.enabled: true`. Each user needs a unique string `id`, a unique `username` and a `passwordHash` created by the helper. Passwords use salted scrypt hashes. The file is read at startup; restart the server after editing it.
572
-
573
- | REST request | Input | Response |
574
- |---|---|---|
575
- | `POST /auth/login` | JSON `{ "username": "admin", "password": "…" }` | `{ accessToken, expiresIn, user: { id, username } }` |
576
- | `GET /auth/me` | Bearer token | `{ id, username }` |
577
- | `POST /auth/logout` | Bearer token | `{ success: true }` |
578
-
579
- Pass the token in `Authorization: Bearer <accessToken>`. Incorrect credentials and invalid or expired tokens return HTTP 401. Sessions are kept in memory and disappear on restart; logout revokes the supplied token. Login can return HTTP 429 when the server has too many concurrent login attempts or active sessions.
580
-
581
- OpenAPI includes these REST operations and a Bearer security scheme for `/auth/me` and `/auth/logout`. In Swagger UI, paste a token from login into **Authorize**. For schema exports, enable auth in the configuration or use `generate openapi --auth server.config.js`; the users file is not read during generation. GraphQL and OpenAPI still require `database.schema`.
582
-
583
- ## Programmatic API
584
-
585
- ```js
586
- import { createServer } from '@kollors/deep-json-server/server';
587
- import config from './server.config.js';
588
-
589
- const facade = await createServer(config);
590
- const server = facade.fastify();
591
- await server.listen();
592
- // await server.close();
593
- ```
594
-
595
- 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. Override server features with `createServer(config, { files: false, graphql: true, openapi: true, auth: true })`.
596
-
597
- The root import `@kollors/deep-json-server` also provides these functions. Server adapters load when enabled. Generators can be used independently:
598
-
599
- ```js
600
- import { generateOpenapi, writeOpenapi } from '@kollors/deep-json-server/openapi';
601
- import { generateGraphql, writeGraphql } from '@kollors/deep-json-server/graphql';
602
-
603
- const document = await generateOpenapi('./schema.json', { files: true });
604
- const sdl = await generateGraphql('./schema.json');
605
- await writeOpenapi(document, './generated/openapi.yaml');
606
- await writeGraphql(sdl, './generated/schema.graphql');
607
- ```
608
-
609
- `generateOpenapi()` accepts `{ auth: true }` to include auth operations. `hashPassword()` is also available from the root package.
610
-
611
- `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`.
612
-
613
- ## Storage and development
725
+ ## Data storage
614
726
 
615
727
  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.
616
728
 
617
- The server is intended for mocking APIs. Access rules for application data remain the responsibility of your application.
729
+ ## Development
730
+
731
+ 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.
618
732
 
619
733
  ```sh
620
734
  npm ci
@@ -623,6 +737,6 @@ npm run verify
623
737
 
624
738
  The command checks types, code style, test coverage and installation from the package archive.
625
739
 
626
- To publish a new alpha, update the version in `package.json`, `package-lock.json` and `src/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`.
740
+ 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`.
627
741
 
628
742
  License: MIT.