@kollors/deep-json-server 1.0.0-alpha.1 → 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 (223) hide show
  1. package/README.md +522 -179
  2. package/README.ru.md +525 -180
  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 -6
  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} +1 -0
  36. package/dist/src/{constants.js → core/constants.js} +1 -0
  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} +36 -14
  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/{model.js → core/model.js} +224 -33
  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 +49 -32
  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.js → graphql/schema.js} +57 -50
  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 +7 -3
  140. package/dist/src/openapi/document.js +147 -92
  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/{files/openapi.js → openapi/files.js} +6 -5
  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 -10
  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 +17 -1
  188. package/dist/src/cli.d.ts +0 -4
  189. package/dist/src/cli.js +0 -67
  190. package/dist/src/cli.js.map +0 -1
  191. package/dist/src/config.d.ts +0 -68
  192. package/dist/src/config.js +0 -161
  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 -21
  196. package/dist/src/database.js.map +0 -1
  197. package/dist/src/engine.d.ts +0 -47
  198. package/dist/src/engine.js +0 -438
  199. package/dist/src/engine.js.map +0 -1
  200. package/dist/src/files/openapi.d.ts +0 -3
  201. package/dist/src/files/openapi.js.map +0 -1
  202. package/dist/src/graphql.d.ts +0 -4
  203. package/dist/src/graphql.js.map +0 -1
  204. package/dist/src/model.d.ts +0 -67
  205. package/dist/src/model.js.map +0 -1
  206. package/dist/src/openapi/index.d.ts +0 -7
  207. package/dist/src/openapi/index.js +0 -26
  208. package/dist/src/openapi/index.js.map +0 -1
  209. package/dist/src/query/filter.d.ts +0 -1
  210. package/dist/src/query/filter.js +0 -84
  211. package/dist/src/query/filter.js.map +0 -1
  212. package/dist/src/query/options.d.ts +0 -31
  213. package/dist/src/query/options.js +0 -135
  214. package/dist/src/query/options.js.map +0 -1
  215. package/dist/src/relation-metadata.d.ts +0 -10
  216. package/dist/src/relation-metadata.js.map +0 -1
  217. package/dist/src/server.d.ts +0 -14
  218. package/dist/src/server.js +0 -166
  219. package/dist/src/server.js.map +0 -1
  220. package/dist/src/utils.d.ts +0 -19
  221. package/dist/src/utils.js +0 -62
  222. package/dist/src/utils.js.map +0 -1
  223. /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.1 is a breaking prerelease.** The old `$schema`/`$info` model format and `_where`, `_sort`, `_embed`, `_page`, `_perPage` query parameters are no longer supported.
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
 
@@ -12,90 +12,138 @@ 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
- The `alpha` npm channel is separate from `latest`. Install an exact version with `@1.0.0-alpha.1`.
15
+ To install a specific version, use `@1.0.0-alpha.10`.
16
16
 
17
17
  ## Quick start
18
18
 
19
+ Create two files in the same directory.
20
+
21
+ `database.json`:
22
+
23
+ ```json
24
+ {
25
+ "users": [
26
+ { "id": "1", "fullName": "Мира Волкова" }
27
+ ]
28
+ }
29
+ ```
30
+
19
31
  `server.config.js`:
20
32
 
21
33
  ```js
22
- export default {
23
- database: { path: './database.json', schema: './schema.json' },
24
- graphql: { enabled: true, path: './generated/schema.graphql' },
25
- openapi: { path: './generated/openapi.yaml' },
26
- server: { host: '127.0.0.1', port: 4001, pageSize: 10, maxPageSize: 100 },
27
- };
34
+ export default { storage: 'file', database: { source: './database.json' } };
28
35
  ```
29
36
 
30
37
  ```sh
31
38
  npx deep-json-server server.config.js
32
- npx deep-json-server --openapi-only --graphql-only server.config.js
33
39
  ```
34
40
 
35
- The second command exports both schemas without listening on a port. Starting the server alone does not write schema files.
41
+ The user list is available at `http://127.0.0.1:4001/users`.
36
42
 
37
- Complete catalog examples: [database](examples/database.json), [model schema](examples/schema.json), [configuration](examples/server.config.js).
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.
38
44
 
39
45
  ## Configuration
40
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
+
41
61
  | Setting | Meaning |
42
62
  |---|---|
43
- | `database.path` / `database.data` | Exactly one: JSON file or in-memory collection object |
44
- | `database.schema` | Model object or JSON schema-file path; optional for REST |
45
- | `openapi.path` | YAML export destination |
46
- | `openapi.info` | Optional `title`, `version`, `description` |
47
- | `graphql.enabled` | Enable GraphQL HTTP endpoint; default `false` |
48
- | `graphql.endpoint` | Endpoint path; default `/graphql` |
49
- | `graphql.path` | GraphQL SDL export destination |
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` |
50
75
  | `server.host`, `server.port` | Defaults `127.0.0.1`, `4001`; CLI also reads `HOST`/`PORT` |
51
76
  | `server.pageSize`, `server.maxPageSize` | Defaults 10 and 100; default size is capped by the maximum |
52
77
  | `server.cors`, `server.logger` | Default `true`; logger also accepts Fastify logger options |
53
78
  | `server.maxFileSize` | Default 100 MiB |
54
- | `files.data` | In-memory binary files |
55
- | `files.directory`, `files.metadata` | Disk storage directory and metadata JSON file; both required |
56
79
 
57
- Configuration-file paths resolve relative to that file. Direct `createServer()` paths resolve relative to the working directory. In-memory input is copied.
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.
58
81
 
59
- CLI flags:
82
+ ### CLI
60
83
 
61
84
  | Flag | Action |
62
85
  |---|---|
63
- | `--files` | Enable binary-file routes |
64
- | `--graphql` | Enable GraphQL endpoint |
65
- | `--openapi` | Export OpenAPI and start |
66
- | `--openapi-only` | Export OpenAPI without starting |
67
- | `--graphql-schema` | Export GraphQL SDL and start |
68
- | `--graphql-only` | Export GraphQL SDL without starting |
69
- | `--help` | Show usage |
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 |
70
92
 
71
- Both exporters can be combined. Any `--*-only` flag prevents startup. CLI file routes require `--files`; programmatic use enables them when `files` is configured unless overridden through the second `createServer()` argument.
93
+ Address precedence: CLI configuration `HOST`/`PORT` defaults. Without generation flags, only the server starts. `--generate` and `--generate-only` are mutually exclusive.
72
94
 
73
95
  ## Model schema
74
96
 
97
+ Examples: [database](examples/database.json), [model schema](examples/schema.json), [configuration](examples/server.config.js).
98
+
75
99
  ```json
76
100
  {
77
- "Country": {
78
- "collection": "countries",
79
- "api": ["openapi", "graphql"],
80
- "fields": {
81
- "id": { "type": "string", "primary": true, "generated": "uuid" },
82
- "name": { "type": "string", "required": true },
83
- "users": { "type": "User[]", "target": "countryId" }
84
- }
85
- },
86
- "User": {
87
- "collection": "users",
88
- "api": ["openapi", "graphql"],
89
- "fields": {
90
- "id": { "type": "string", "primary": true, "generated": "uuid" },
91
- "fullName": { "type": "string", "required": true },
92
- "country": { "type": "Country", "source": "countryId" }
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
+ }
93
141
  }
94
142
  }
95
143
  }
96
144
  ```
97
145
 
98
- `api` defaults to `["openapi", "graphql"]`. `[]` excludes the model from both exports and GraphQL, while REST remains available. Related models must enable the same export format. Names must be valid identifiers and generated type/operation names must not collide. `and`, `or`, `not` are reserved filter names.
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.
99
147
 
100
148
  | Capability | With schema | Without schema |
101
149
  |---|---|---|
@@ -104,11 +152,15 @@ Both exporters can be combined. Any `--*-only` flag prevents startup. CLI file r
104
152
  | OpenAPI 3.0.3 export | Available | Error when requested |
105
153
  | GraphQL SDL / API | Available | Error when requested |
106
154
 
107
- Explicit schemas are strict: undeclared fields and collections are rejected, except storage keys inferred from relations. Existing data is validated on startup. Schemas can be exported for empty collections or an empty database object. REST without a schema retains the standard generated `id` behavior.
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.
156
+
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.
158
+
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.
108
160
 
109
161
  ### Fields
110
162
 
111
- Types: `string`, `number`, `boolean`, `object`, or a model name. Append `[]` for an array. No `integer`, `relation`, `items`, or multidimensional type strings. Nested fields use full dotted paths, for example `actors.fullName`. Objects inside arrays may contain their own arrays.
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`.
112
164
 
113
165
  | Properties | Meaning |
114
166
  |---|---|
@@ -116,7 +168,7 @@ Types: `string`, `number`, `boolean`, `object`, or a model name. Append `[]` for
116
168
  | `description`, `example` | Documentation and example value |
117
169
  | `required`, `nullable` | Defaults `false`; presence and explicit null are separate |
118
170
  | `default` | Value when omitted on create/replace; PATCH does not insert defaults |
119
- | `enum` | Allowed values; for arrays, allowed element values |
171
+ | `enum` | Allowed strings, numbers or booleans; for arrays, allowed element values |
120
172
  | `primary` | Root primary key; mandatory, unique, non-null and immutable |
121
173
  | `generated` | `uuid` for strings, `increment` for numbers; server supplies the value |
122
174
  | `readOnly`, `writeOnly` | Output-only or input-only; mutually exclusive |
@@ -125,137 +177,40 @@ Types: `string`, `number`, `boolean`, `object`, or a model name. Append `[]` for
125
177
  | `minimum`, `maximum` | Inclusive numeric bounds |
126
178
  | `source`, `target`, `onDelete` | Relation metadata |
127
179
 
128
- String/numeric constraints on `string[]`/`number[]` apply to every element. `required`/`nullable` apply to the entire array, and `default`/`example` contain a complete array. Array elements are non-null. There are no item-count or uniqueness constraints. A required ordinary array may be empty.
129
-
130
- Primary keys can be named `username`, `code`, etc.; exactly one root string/number primary key is required. Without `generated`, the client supplies it during creation. Generated fields are root fields, absent from create/replace/update input; they cannot have `default`. Replace preserves generated and read-only root values.
131
-
132
- For example, a `LocalUser` with primary `username` and `password: {"type":"string","required":true,"writeOnly":true}` has `localUser(username: ...)` and `/localUsers/{username}`. `writeOnly` excludes passwords from responses, scope, filters and ordering. It does not implement hashing or authentication.
133
-
134
- ### Relations
135
-
136
- ```json
137
- "actors.genres": {
138
- "type": "Genre[]",
139
- "source": "actors.genreIds",
140
- "required": true
141
- }
142
- ```
143
-
144
- `Genre` produces an object; `Genre[]` produces a list. `source` defaults to the current model's primary key, `target` to the target model's primary key. Both paths are rooted at their respective records. Within `actors`, `actors.genreIds` reads the current actor's IDs. An omitted `source` still means the root model key, not `actors.id`.
145
-
146
- Storage keys remain in the database and are included among own fields. Their types can be inferred from the target key. For an undeclared source pointing to a target primary key, a list relation implies an array of keys; a single relation implies a scalar key. Declare storage fields explicitly when the mapping is ambiguous. Generation never guesses from the first database record.
147
-
148
- Reverse example: `User.movies = {"type":"Movie[]","target":"actors.userId"}`. A movie is returned once even if several actors match. A single relation resolving to multiple targets is invalid.
149
-
150
- Every supplied direct reference must resolve. `required: true` on a relation requires at least one target before response filtering/pagination. Reverse views with a primary source may be empty unless required. Missing single relations return `null`.
151
-
152
- `onDelete` describes what happens **when a target record is deleted**:
153
-
154
- - `restrict` (default): refuse deletion while a surviving owner refers to the target.
155
- - `cascade`: delete the referring owner. For `User.country`, deleting the country deletes its users. For `Movie.actors.user`, deleting the user removes matching actor elements and retains the movie.
156
-
157
- Deletion computes the cascade closure, handles cycles, checks restrictions and validates remaining data before committing. A failure cancels the complete operation. Policies also apply to explicitly declared reverse relation views; configure both directions deliberately when both are present.
158
-
159
- ## Queries and responses
160
-
161
- Collections and lists of objects, including embedded `object[]` fields, return:
162
-
163
- ```json
164
- { "data": [], "total": 0 }
165
- ```
166
-
167
- Primitive arrays remain plain arrays. Every object list accepts optional `where`, `order`, `pager`. Processing order is filter → sort → pagination. `total` is the filtered count before pagination. Page numbers start at 1; default pagination applies even when omitted. Exceeding `maxPageSize`, fractional values and nonpositive values are errors. Out-of-range pages return empty `data` with the correct `total`.
168
-
169
- `where` uses field operators `eq`, `ne`, `in`, string `contains`/`startsWith`/`endsWith`, and comparisons `gt`, `gte`, `lt`, `lte`. Logical composition uses `and`, `or`, `not`. Arrays support `some`, `every`, `none`; primitive arrays also support `contains`, `in`. String matching is case-insensitive. Field filters use operator objects, not shorthand scalar values.
170
-
171
- ```json
172
- {
173
- "movies": {
174
- "some": {
175
- "actors": {
176
- "some": {
177
- "genres": { "some": { "id": { "in": ["2", "3"] } } }
178
- }
179
- }
180
- }
181
- }
182
- }
183
- ```
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.
184
181
 
185
- Root filters choose parents. Filters inside a selected relation only trim that relation; they do not remove the parent. Each parent's child list is processed independently. Filtering does not require a relation to be included in the response.
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.
186
183
 
187
- `order` is an array of `{ "field": "fullName", "direction": "ASC" }` rules. Earlier rules have priority; complete ties retain storage order. Null and missing values compare equally. REST uses dotted field paths; GraphQL uses generated enums (`profile_name` for `profile.name`). Ambiguous enum names cause a generation error. Sorting parents by a relation or an array is unsupported; sorting inside the relation is supported.
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.
188
185
 
189
- ### REST
186
+ Objects used in GraphQL must have at least one field visible in responses; REST also accepts empty objects.
190
187
 
191
- | Method | Path | Operation |
192
- |---|---|---|
193
- | GET | `/users` | `userList` |
194
- | GET | `/users/{id}` | `user` |
195
- | POST | `/users` | `userCreate` |
196
- | PUT | `/users/{id}` | `userReplace` |
197
- | PATCH | `/users/{id}` | `userUpdate` |
198
- | DELETE | `/users/{id}` | `userDelete` |
199
-
200
- The path key name follows the primary key. POST/PUT/PATCH receive raw record objects. PUT replaces the record while retaining its key and server-owned root values. PATCH shallowly merges supplied fields; supplied nested objects are full replacements. Create/replace enforce required fields. Update validates supplied values and the final record. Missing targets return 404; conflicts return 409. DELETE returns the deleted record.
201
-
202
- Query parameters `where`, `order`, `pager`, `nested` contain JSON. `scope` is a selection string. Example shown before URL encoding:
203
-
204
- ```text
205
- GET /users?where={"fullName":{"contains":"Мира"}}&order=[{"field":"fullName","direction":"ASC"}]&pager={"page":1,"pageSize":20}
206
- ```
207
-
208
- Construct encoded URLs with `URLSearchParams`:
209
-
210
- ```js
211
- const params = new URLSearchParams({
212
- scope: 'id,fullName,movies(id,title)',
213
- nested: JSON.stringify({ movies: { order: [{ field: 'title', direction: 'ASC' }], pager: { page: 1, pageSize: 5 } } }),
214
- });
215
- const response = await fetch(`/users?${params}`);
216
- ```
217
-
218
- `scope=*,actors(user(id,fullName),genres(*))` selects own fields and explicit relations. `*` selects only own fields of the current object, including inferred storage keys and excluding `writeOnly`. It never recursively expands relations. Without scope, own fields are selected. Wrappers `data`/`total` remain present.
219
-
220
- `nested` maps full response paths to list options:
188
+ ### Relations
221
189
 
222
190
  ```json
223
191
  {
224
- "actors": { "pager": { "pageSize": 5 } },
225
192
  "actors.genres": {
226
- "where": { "id": { "in": ["2", "3"] } },
227
- "order": [{ "field": "name", "direction": "ASC" }]
193
+ "type": "Genre[]",
194
+ "source": "actors.genreIds",
195
+ "required": true
228
196
  }
229
197
  }
230
198
  ```
231
199
 
232
- A nested path must be selected by scope and must address an object list. Single-record routes and mutations accept `scope` and `nested`; root list parameters only apply to collection GET. Invalid names and unsafe paths return 400.
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.
233
201
 
234
- ### GraphQL
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.
235
203
 
236
- ```graphql
237
- query {
238
- userList(
239
- order: [{ field: fullName, direction: ASC }]
240
- pager: { page: 1, pageSize: 20 }
241
- ) {
242
- total
243
- data {
244
- id
245
- fullName
246
- movies(order: [{ field: title, direction: ASC }], pager: { pageSize: 5 }) {
247
- total
248
- data { id title }
249
- }
250
- }
251
- }
252
- }
253
- ```
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.
254
205
 
255
- Single queries are `user(id: ...)`, with no `ById`; missing records yield null. Mutation names are `userCreate(data: ...)`, `userReplace(id: ..., data: ...)`, `userUpdate(id: ..., data: ...)`, `userDelete(id: ...)`. A generated-only model creates records without a `data` argument. Mutations use the same validation and storage operations as REST. Selecting relations in mutation results shapes the response only.
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`.
256
207
 
257
- String primary keys use GraphQL `ID`; ordinary strings use `String`, numbers use `Float`, pagination uses `Int`. Schema enums preserve valid string labels; other values receive `VALUE_0`, `VALUE_1`, etc. Schema constraints such as string length and formats are enforced by the shared runtime validator; SDL alone cannot express all constraints. Introspection and ordinary query/mutation execution are supported; this release does not add subscriptions or bulk mutations.
208
+ `onDelete` describes what happens **when a target record is deleted**:
258
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.
212
+
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.
259
214
 
260
215
  ## Example database
261
216
 
@@ -361,15 +316,381 @@ String primary keys use GraphQL `ID`; ordinary strings use `String`, numbers use
361
316
  }
362
317
  ```
363
318
 
319
+ ## Queries and responses
320
+
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):
322
+
323
+ ```sh
324
+ npx deep-json-server examples/server.config.js
325
+ ```
326
+
327
+ Collections and lists of objects, including embedded `object[]` fields, return:
328
+
329
+ ```json
330
+ { "data": [], "total": 0 }
331
+ ```
332
+
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.
334
+
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`.
336
+
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" } }`.
338
+
339
+ ```json
340
+ {
341
+ "movies": {
342
+ "some": {
343
+ "actors": {
344
+ "some": {
345
+ "genres": { "some": { "id": { "in": ["2", "3"] } } }
346
+ }
347
+ }
348
+ }
349
+ }
350
+ }
351
+ ```
352
+
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.
354
+
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`.
356
+
357
+ ### REST
358
+
359
+ `GET /` returns collection names: `{ "resources": ["users", "movies"] }`. Each collection has these routes:
360
+
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` |
369
+
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}`);
398
+ ```
399
+
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:
403
+
404
+ ```json
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
+ ]
419
+ ```
420
+
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
432
+
433
+ {
434
+ "actors": [
435
+ {
436
+ "userId": "1",
437
+ "genres": [
438
+ "1",
439
+ { "id": "2", "name": "Updated genre" },
440
+ { "name": "New genre" }
441
+ ]
442
+ }
443
+ ]
444
+ }
445
+ ```
446
+
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 |
452
+
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.
454
+
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.
456
+
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
+ }
474
+ ```
475
+
476
+ ### GraphQL
477
+
478
+ Set `database.schema` and add `graphql: {}` to the configuration:
479
+
480
+ ```sh
481
+ npx deep-json-server server.config.js
482
+ ```
483
+
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
+ ```
509
+
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.
511
+
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.
513
+
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
+ };
529
+ ```
530
+
531
+ ```bash
532
+ npx deep-json-server server.config.js --generate-only
533
+ npx deep-json-server server.config.js --generate
534
+ ```
535
+
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
+ );
561
+ ```
562
+
563
+ Set `DJS_PASSWORD` and run `node setup-auth.mjs`. Add the file to your server configuration:
564
+
565
+ ```js
566
+ export default {
567
+ storage: 'file',
568
+ database: { source: './database.json' },
569
+ auth: { source: './auth.json', expiresIn: 3600 },
570
+ };
571
+ ```
572
+
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.
574
+
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
+ }
641
+ ```
642
+
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` |
651
+
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.
653
+
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
+ ```
664
+
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.
674
+
364
675
  ## Files
365
676
 
366
- 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:
367
688
 
368
689
  ```bash
369
- deep-json-server --files server.config.js
690
+ npx deep-json-server server.config.js
370
691
  ```
371
692
 
372
- 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.
373
694
 
374
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:
375
696
 
@@ -420,39 +741,61 @@ Content-Type: application/json
420
741
  }
421
742
  ```
422
743
 
423
- `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.
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.
424
745
 
425
- 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.
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.
426
747
 
427
- The upload is raw binary rather than `multipart/form-data`, so `XMLHttpRequest.upload.onprogress` can report progress while the browser sends a `File` directly with `xhr.send(file)`. The default maximum size is 100 MiB and can be changed through `server.maxFileSize`. Missing or unsafe headers and paths return `400`, an exceeded limit returns `413`, and a missing, malformed, or Fastify-unsupported `Content-Type` returns `400` or `415`, depending on which validation stage rejects it.
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.
749
+
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.
428
751
 
429
752
  ## Programmatic API
430
753
 
431
754
  ```js
432
- import { createServer } from '@kollors/deep-json-server';
755
+ import { createServer } from '@kollors/deep-json-server/server';
433
756
  import config from './server.config.js';
434
757
 
435
758
  const facade = await createServer(config);
436
- const openapi = await facade.openapi();
437
- const sdl = await facade.graphql();
438
759
  const server = facade.fastify();
439
760
  await server.listen();
440
761
  // await server.close();
441
762
  ```
442
763
 
443
- Accessors are lazy; exporting schemas does not listen or initialize disk file storage. Override runtime features with `createServer(config, { files: false, graphql: true })`.
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.
444
765
 
445
- ## Storage and development
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.
446
767
 
447
- Updates are serialized within one server instance and validated on a draft before persistence. Use one writer per database file. Increment counters are stored next to the database in `<database path>.counters.json`; keep that file with the database. Numbers are reserved before the data write, so a failed write can leave gaps but cannot reuse a reserved number. UUID generation uses Node's built-in crypto API.
768
+ The root import `@kollors/deep-json-server` also provides these functions. Server adapters load when enabled. Generators can be used independently:
448
769
 
449
- This is a mock server; there is no authentication or password hashing. File routes keep their independent storage and validation.
770
+ ```js
771
+ import { generateOpenapi, writeOpenapi } from '@kollors/deep-json-server/openapi';
772
+ import { generateGraphql, writeGraphql } from '@kollors/deep-json-server/graphql';
773
+
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
+ ```
779
+
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.
781
+
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`.
783
+
784
+ ## Data storage
785
+
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.
787
+
788
+ ## Development
789
+
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.
450
791
 
451
792
  ```sh
452
793
  npm ci
453
794
  npm run verify
454
795
  ```
455
796
 
456
- Verification runs type checking, lint, coverage gates and installation checks against the packed package. A new alpha version in `package.json` pushed to `main` creates its version tag and publishes through GitHub Actions trusted publishing to npm `alpha`. An existing tag skips automatic publication. Explicit version-tag pushes also publish; stable versions use `latest`. The publishing script rejects mismatched Git tags.
797
+ The command checks types, code style, test coverage and installation from the package archive.
798
+
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`.
457
800
 
458
801
  License: MIT.