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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/README.md +566 -347
  2. package/README.ru.md +560 -341
  3. package/dist/bin/deep-json-server.js +1 -1
  4. package/dist/bin/deep-json-server.js.map +1 -1
  5. package/dist/index.d.ts +13 -5
  6. package/dist/index.js +4 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/src/auth/contract.d.ts +31 -0
  9. package/dist/src/auth/contract.js +12 -0
  10. package/dist/src/auth/contract.js.map +1 -0
  11. package/dist/src/auth/input.d.ts +18 -0
  12. package/dist/src/auth/input.js +34 -0
  13. package/dist/src/auth/input.js.map +1 -0
  14. package/dist/src/auth/password.d.ts +17 -0
  15. package/dist/src/auth/password.js +41 -0
  16. package/dist/src/auth/password.js.map +1 -0
  17. package/dist/src/auth/public.d.ts +3 -0
  18. package/dist/src/auth/public.js +5 -0
  19. package/dist/src/auth/public.js.map +1 -0
  20. package/dist/src/auth/routes.d.ts +6 -0
  21. package/dist/src/auth/routes.js +18 -0
  22. package/dist/src/auth/routes.js.map +1 -0
  23. package/dist/src/auth/service.d.ts +78 -0
  24. package/dist/src/auth/service.js +218 -0
  25. package/dist/src/auth/service.js.map +1 -0
  26. package/dist/src/auth/store.d.ts +26 -0
  27. package/dist/src/auth/store.js +82 -0
  28. package/dist/src/auth/store.js.map +1 -0
  29. package/dist/src/cli/index.d.ts +7 -0
  30. package/dist/src/cli/index.js +122 -0
  31. package/dist/src/cli/index.js.map +1 -0
  32. package/dist/src/core/config-values.d.ts +28 -0
  33. package/dist/src/core/config-values.js +53 -0
  34. package/dist/src/core/config-values.js.map +1 -0
  35. package/dist/src/{constants.d.ts → core/constants.d.ts} +2 -1
  36. package/dist/src/{constants.js → core/constants.js} +2 -1
  37. package/dist/src/core/constants.js.map +1 -0
  38. package/dist/src/core/database.d.ts +48 -0
  39. package/dist/src/{database.js → core/database.js} +64 -29
  40. package/dist/src/core/database.js.map +1 -0
  41. package/dist/src/core/engine.d.ts +65 -0
  42. package/dist/src/core/engine.js +312 -0
  43. package/dist/src/core/engine.js.map +1 -0
  44. package/dist/src/core/errors.d.ts +9 -0
  45. package/dist/src/core/errors.js +12 -0
  46. package/dist/src/core/errors.js.map +1 -0
  47. package/dist/src/core/http-errors.d.ts +9 -0
  48. package/dist/src/core/http-errors.js +15 -0
  49. package/dist/src/core/http-errors.js.map +1 -0
  50. package/dist/src/core/lifecycle/mutation.d.ts +49 -0
  51. package/dist/src/core/lifecycle/mutation.js +170 -0
  52. package/dist/src/core/lifecycle/mutation.js.map +1 -0
  53. package/dist/src/core/lifecycle/options.d.ts +21 -0
  54. package/dist/src/core/lifecycle/options.js +19 -0
  55. package/dist/src/core/lifecycle/options.js.map +1 -0
  56. package/dist/src/core/model.d.ts +145 -0
  57. package/dist/src/core/model.js +510 -0
  58. package/dist/src/core/model.js.map +1 -0
  59. package/dist/src/core/mutations/write.d.ts +26 -0
  60. package/dist/src/core/mutations/write.js +284 -0
  61. package/dist/src/core/mutations/write.js.map +1 -0
  62. package/dist/src/core/operations.d.ts +33 -0
  63. package/dist/src/core/operations.js +10 -0
  64. package/dist/src/core/operations.js.map +1 -0
  65. package/dist/src/core/pagination.d.ts +10 -0
  66. package/dist/src/core/pagination.js +13 -0
  67. package/dist/src/core/pagination.js.map +1 -0
  68. package/dist/src/core/paths.d.ts +16 -0
  69. package/dist/src/core/paths.js +64 -0
  70. package/dist/src/core/paths.js.map +1 -0
  71. package/dist/src/core/query/contract.d.ts +6 -0
  72. package/dist/src/core/query/contract.js +22 -0
  73. package/dist/src/core/query/contract.js.map +1 -0
  74. package/dist/src/core/query/filter.d.ts +6 -0
  75. package/dist/src/core/query/filter.js +100 -0
  76. package/dist/src/core/query/filter.js.map +1 -0
  77. package/dist/src/core/query/options.d.ts +30 -0
  78. package/dist/src/core/query/options.js +44 -0
  79. package/dist/src/core/query/options.js.map +1 -0
  80. package/dist/src/core/records.d.ts +46 -0
  81. package/dist/src/core/records.js +69 -0
  82. package/dist/src/core/records.js.map +1 -0
  83. package/dist/src/core/relation-metadata.d.ts +15 -0
  84. package/dist/src/{relation-metadata.js → core/relation-metadata.js} +7 -2
  85. package/dist/src/core/relation-metadata.js.map +1 -0
  86. package/dist/src/core/storage.d.ts +2 -0
  87. package/dist/src/core/storage.js +2 -0
  88. package/dist/src/core/storage.js.map +1 -0
  89. package/dist/src/core/types.d.ts +10 -0
  90. package/dist/src/{types.js.map → core/types.js.map} +1 -1
  91. package/dist/src/core/utils.d.ts +56 -0
  92. package/dist/src/core/utils.js +102 -0
  93. package/dist/src/core/utils.js.map +1 -0
  94. package/dist/src/files/contract.d.ts +32 -30
  95. package/dist/src/files/contract.js +29 -48
  96. package/dist/src/files/contract.js.map +1 -1
  97. package/dist/src/files/disk-store.d.ts +5 -1
  98. package/dist/src/files/disk-store.js +61 -33
  99. package/dist/src/files/disk-store.js.map +1 -1
  100. package/dist/src/files/http.d.ts +36 -0
  101. package/dist/src/files/http.js +53 -0
  102. package/dist/src/files/http.js.map +1 -0
  103. package/dist/src/files/index.d.ts +5 -4
  104. package/dist/src/files/index.js +7 -2
  105. package/dist/src/files/index.js.map +1 -1
  106. package/dist/src/files/memory-store.d.ts +4 -1
  107. package/dist/src/files/memory-store.js +29 -22
  108. package/dist/src/files/memory-store.js.map +1 -1
  109. package/dist/src/files/routes.d.ts +3 -0
  110. package/dist/src/files/routes.js +27 -2
  111. package/dist/src/files/routes.js.map +1 -1
  112. package/dist/src/files/streams.d.ts +5 -0
  113. package/dist/src/files/streams.js +20 -0
  114. package/dist/src/files/streams.js.map +1 -0
  115. package/dist/src/graphql/entry.d.ts +12 -0
  116. package/dist/src/graphql/entry.js +13 -0
  117. package/dist/src/graphql/entry.js.map +1 -0
  118. package/dist/src/graphql/generate.d.ts +12 -0
  119. package/dist/src/graphql/generate.js +19 -0
  120. package/dist/src/graphql/generate.js.map +1 -0
  121. package/dist/src/graphql/preflight.d.ts +6 -0
  122. package/dist/src/graphql/preflight.js +42 -0
  123. package/dist/src/graphql/preflight.js.map +1 -0
  124. package/dist/src/graphql/resolvers.d.ts +17 -0
  125. package/dist/src/graphql/resolvers.js +44 -0
  126. package/dist/src/graphql/resolvers.js.map +1 -0
  127. package/dist/src/graphql/routes.d.ts +8 -0
  128. package/dist/src/graphql/routes.js +30 -0
  129. package/dist/src/graphql/routes.js.map +1 -0
  130. package/dist/src/graphql/schema.d.ts +6 -0
  131. package/dist/src/graphql/schema.js +230 -0
  132. package/dist/src/graphql/schema.js.map +1 -0
  133. package/dist/src/graphql/write.d.ts +4 -0
  134. package/dist/src/graphql/write.js +10 -0
  135. package/dist/src/graphql/write.js.map +1 -0
  136. package/dist/src/openapi/auth.d.ts +13 -0
  137. package/dist/src/openapi/auth.js +139 -0
  138. package/dist/src/openapi/auth.js.map +1 -0
  139. package/dist/src/openapi/document.d.ts +11 -7
  140. package/dist/src/openapi/document.js +287 -318
  141. package/dist/src/openapi/document.js.map +1 -1
  142. package/dist/src/openapi/entry.d.ts +14 -0
  143. package/dist/src/openapi/entry.js +13 -0
  144. package/dist/src/openapi/entry.js.map +1 -0
  145. package/dist/src/openapi/files.d.ts +6 -0
  146. package/dist/src/openapi/files.js +87 -0
  147. package/dist/src/openapi/files.js.map +1 -0
  148. package/dist/src/openapi/generate.d.ts +19 -0
  149. package/dist/src/openapi/generate.js +41 -0
  150. package/dist/src/openapi/generate.js.map +1 -0
  151. package/dist/src/openapi/helpers.d.ts +26 -0
  152. package/dist/src/openapi/helpers.js +13 -0
  153. package/dist/src/openapi/helpers.js.map +1 -0
  154. package/dist/src/openapi/options.d.ts +18 -0
  155. package/dist/src/openapi/options.js +13 -0
  156. package/dist/src/openapi/options.js.map +1 -0
  157. package/dist/src/{types.d.ts → openapi/types.d.ts} +2 -12
  158. package/dist/src/openapi/types.js +2 -0
  159. package/dist/src/openapi/types.js.map +1 -0
  160. package/dist/src/openapi/write.d.ts +5 -0
  161. package/dist/src/openapi/write.js +12 -0
  162. package/dist/src/openapi/write.js.map +1 -0
  163. package/dist/src/rest/options.d.ts +22 -0
  164. package/dist/src/rest/options.js +65 -0
  165. package/dist/src/rest/options.js.map +1 -0
  166. package/dist/src/rest/projection.d.ts +13 -0
  167. package/dist/src/rest/projection.js +61 -0
  168. package/dist/src/rest/projection.js.map +1 -0
  169. package/dist/src/rest/routes.d.ts +7 -0
  170. package/dist/src/rest/routes.js +49 -0
  171. package/dist/src/rest/routes.js.map +1 -0
  172. package/dist/src/server/config.d.ts +74 -0
  173. package/dist/src/server/config.js +142 -0
  174. package/dist/src/server/config.js.map +1 -0
  175. package/dist/src/server/create.d.ts +16 -0
  176. package/dist/src/server/create.js +133 -0
  177. package/dist/src/server/create.js.map +1 -0
  178. package/dist/src/server/features.d.ts +4 -0
  179. package/dist/src/server/features.js +11 -0
  180. package/dist/src/server/features.js.map +1 -0
  181. package/dist/src/server/model.d.ts +6 -0
  182. package/dist/src/server/model.js +14 -0
  183. package/dist/src/server/model.js.map +1 -0
  184. package/dist/src/server/public.d.ts +2 -0
  185. package/dist/src/server/public.js +2 -0
  186. package/dist/src/server/public.js.map +1 -0
  187. package/package.json +25 -4
  188. package/dist/src/cli.d.ts +0 -4
  189. package/dist/src/cli.js +0 -80
  190. package/dist/src/cli.js.map +0 -1
  191. package/dist/src/config.d.ts +0 -54
  192. package/dist/src/config.js +0 -145
  193. package/dist/src/config.js.map +0 -1
  194. package/dist/src/constants.js.map +0 -1
  195. package/dist/src/database.d.ts +0 -20
  196. package/dist/src/database.js.map +0 -1
  197. package/dist/src/openapi/config.d.ts +0 -11
  198. package/dist/src/openapi/config.js +0 -204
  199. package/dist/src/openapi/config.js.map +0 -1
  200. package/dist/src/openapi/index.d.ts +0 -10
  201. package/dist/src/openapi/index.js +0 -27
  202. package/dist/src/openapi/index.js.map +0 -1
  203. package/dist/src/openapi/inference.d.ts +0 -14
  204. package/dist/src/openapi/inference.js +0 -145
  205. package/dist/src/openapi/inference.js.map +0 -1
  206. package/dist/src/query/filter.d.ts +0 -6
  207. package/dist/src/query/filter.js +0 -248
  208. package/dist/src/query/filter.js.map +0 -1
  209. package/dist/src/query/index.d.ts +0 -3
  210. package/dist/src/query/index.js +0 -4
  211. package/dist/src/query/index.js.map +0 -1
  212. package/dist/src/query/pagination.d.ts +0 -11
  213. package/dist/src/query/pagination.js +0 -28
  214. package/dist/src/query/pagination.js.map +0 -1
  215. package/dist/src/query/sort.d.ts +0 -1
  216. package/dist/src/query/sort.js +0 -54
  217. package/dist/src/query/sort.js.map +0 -1
  218. package/dist/src/relation-metadata.d.ts +0 -10
  219. package/dist/src/relation-metadata.js.map +0 -1
  220. package/dist/src/relations.d.ts +0 -12
  221. package/dist/src/relations.js +0 -155
  222. package/dist/src/relations.js.map +0 -1
  223. package/dist/src/server.d.ts +0 -13
  224. package/dist/src/server.js +0 -188
  225. package/dist/src/server.js.map +0 -1
  226. package/dist/src/utils.d.ts +0 -20
  227. package/dist/src/utils.js +0 -63
  228. package/dist/src/utils.js.map +0 -1
  229. /package/dist/src/{types.js → core/types.js} +0 -0
package/README.ru.md CHANGED
@@ -1,172 +1,272 @@
1
1
  # Deep JSON Server
2
2
 
3
- [Русский](README.ru.md) | [English](README.md)
3
+ [English](README.md)
4
4
 
5
- [GitHub](https://github.com/kollors/deep-json-server) | [npm](https://www.npmjs.com/package/@kollors/deep-json-server)
5
+ JSON-сервер для имитации API: REST, GraphQL, связанные записи, загрузка файлов и экспорт схем. Поддерживает вход пользователей, права владельца и администратора, даты записей и мягкое удаление. Требуется Node.js 22 или новее.
6
6
 
7
- Небольшой сервер для имитации REST API с CRUD, пагинацией, фильтрацией вложенных данных, подстановкой связанных записей через `_embed`, бинарными файлами и генерацией OpenAPI. Данные могут храниться в JSON-файлах или памяти, а связи определяются по именам полей: `countryId`, `genreIds`, `publisherIds` и так далее.
7
+ **Breaking changes: 1.0.0-alpha.10.** Изменились форматы конфигурации и схемы. Актуальные примеры — в разделах «Конфигурация» и «Схема моделей».
8
8
 
9
9
  ## Установка
10
10
 
11
- Требуется Node.js 22 или новее.
12
-
13
- ```bash
14
- npm install --save-dev @kollors/deep-json-server
11
+ ```sh
12
+ npm install @kollors/deep-json-server@alpha
15
13
  ```
16
14
 
15
+ Для установки конкретной версии укажите `@1.0.0-alpha.10`.
16
+
17
17
  ## Быстрый старт
18
18
 
19
- До запуска создайте файл базы `mock/database.json`:
19
+ Создайте два файла в одном каталоге.
20
+
21
+ `database.json`:
20
22
 
21
23
  ```json
22
24
  {
23
- "movies": [
24
- { "id": "1", "title": "Тени Ардении" }
25
+ "users": [
26
+ { "id": "1", "fullName": "Мира Волкова" }
25
27
  ]
26
28
  }
27
29
  ```
28
30
 
29
- Рядом с `package.json` создайте ESM-модуль `server.config.js`:
31
+ `server.config.js`:
30
32
 
31
33
  ```js
32
- export default {
33
- database: {
34
- path: 'mock/database.json',
35
- },
36
- };
34
+ export default { storage: 'file', database: { source: './database.json' } };
37
35
  ```
38
36
 
39
- Запустите сервер:
40
-
41
- ```bash
37
+ ```sh
42
38
  npx deep-json-server server.config.js
43
39
  ```
44
40
 
45
- По умолчанию API доступен по адресу `http://127.0.0.1:4001`. Например, `GET http://127.0.0.1:4001/movies` вернёт страницу:
41
+ Список пользователей доступен по адресу `http://127.0.0.1:4001/users`.
42
+
43
+ Для связей и валидации добавьте [схему моделей](#схема-моделей). Далее описаны [запросы](#запросы-и-ответы), [аутентификация](#аутентификация), [мягкое удаление](#даты-записей-удаление-и-владельцы), [файлы](#файлы) и [программный API](#программный-api).
44
+
45
+ ## Конфигурация
46
+
47
+ При `storage: 'file'` все источники и схема задаются путями, при `'memory'` — данными в памяти. Режимы нельзя смешивать. Наличие секций `auth`, `files`, `graphql` и `openapi` включает соответствующие модули. `graphql: {}` и `openapi: {}` включают только HTTP-маршруты со стандартными адресами; для экспорта добавьте `target`.
48
+
49
+ ```js
50
+ export default {
51
+ storage: 'file',
52
+ database: { source: './database.json', schema: './schema.json' },
53
+ auth: { source: './users.json', expiresIn: 3600 },
54
+ files: { source: './uploads' },
55
+ graphql: { target: './generated/schema.graphql' },
56
+ openapi: { target: './generated/openapi.yaml' },
57
+ server: { host: '127.0.0.1', port: 4001 },
58
+ };
59
+ ```
60
+
61
+ | Настройка | Назначение |
62
+ |---|---|
63
+ | `storage` | Обязательный режим: `file` или `memory`; общий для всех источников и схемы |
64
+ | `database.source` | Путь к JSON базы или объект коллекций |
65
+ | `database.schema` | Путь к JSON схемы или объект схемы; необязателен для REST |
66
+ | `auth.source` | Путь к JSON пользователей или массив пользователей |
67
+ | `auth.expiresIn` | Срок сессии в секундах; по умолчанию 3600 |
68
+ | `files.source` | Каталог файлов или массив начальных файлов |
69
+ | `files.metadata` | Только для `file`: путь JSON метаданных; по умолчанию `.files.json` внутри `files.source` |
70
+ | `graphql.endpoint` | HTTP-маршрут; по умолчанию `/graphql` |
71
+ | `graphql.target` | Файл для экспорта GraphQL SDL |
72
+ | `openapi.endpoint` | HTTP-маршрут; по умолчанию `/openapi.json` |
73
+ | `openapi.target` | Файл для экспорта OpenAPI |
74
+ | `openapi.info` | Метаданные: обязательные `title`, `version`, необязательный `description` |
75
+ | `server.host`, `server.port` | По умолчанию `127.0.0.1`, `4001`; CLI также читает `HOST`/`PORT` |
76
+ | `server.pageSize`, `server.maxPageSize` | По умолчанию 10 и 100; размер по умолчанию ограничен максимумом |
77
+ | `server.cors`, `server.logger` | По умолчанию `true`; logger принимает также настройки Fastify |
78
+ | `server.maxFileSize` | По умолчанию 100 МиБ |
79
+
80
+ Относительные пути разрешаются от каталога файла конфигурации; при вызове `createServer(config)` — от рабочего каталога. Данные в памяти, включая схему, копируются. Порт `0` позволяет системе выбрать свободный порт.
81
+
82
+ ### CLI
83
+
84
+ | Флаг | Действие |
85
+ |---|---|
86
+ | `--generate` | Экспортировать схемы и запустить сервер |
87
+ | `--generate-only` | Экспортировать схемы и завершить работу |
88
+ | `--host <host>` | Адрес сервера |
89
+ | `--port <port>` | Порт сервера |
90
+ | `--help, -h` | Справка |
91
+ | `--version, -v` | Версия пакета |
92
+
93
+ Приоритет адреса и порта: CLI → конфигурация → `HOST`/`PORT` → значения по умолчанию. Без флагов генерации запускается только сервер. `--generate` и `--generate-only` нельзя передавать вместе.
94
+
95
+ ## Схема моделей
96
+
97
+ Примеры: [база данных](examples/database.json), [схема моделей](examples/schema.json), [конфигурация](examples/server.config.js).
46
98
 
47
99
  ```json
48
100
  {
49
- "data": [{ "id": "1", "title": "Тени Ардении" }],
50
- "total": 1
101
+ "api": [
102
+ "openapi",
103
+ "graphql"
104
+ ],
105
+ "models": {
106
+ "Country": {
107
+ "collection": "countries",
108
+ "fields": {
109
+ "id": {
110
+ "type": "string",
111
+ "primary": true,
112
+ "generated": "uuid"
113
+ },
114
+ "name": {
115
+ "type": "string",
116
+ "required": true
117
+ },
118
+ "users": {
119
+ "type": "User[]",
120
+ "target": "countryId"
121
+ }
122
+ }
123
+ },
124
+ "User": {
125
+ "collection": "users",
126
+ "fields": {
127
+ "id": {
128
+ "type": "string",
129
+ "primary": true,
130
+ "generated": "uuid"
131
+ },
132
+ "fullName": {
133
+ "type": "string",
134
+ "required": true
135
+ },
136
+ "country": {
137
+ "type": "Country",
138
+ "source": "countryId"
139
+ }
140
+ }
141
+ }
142
+ }
51
143
  }
52
144
  ```
53
145
 
54
- ## Конфигурация и запуск
146
+ Модели находятся в `models`. В корне схемы можно задать `api`, `timestamps` и `softDelete`; у модели эти параметры переопределяют общие значения. Для `api` приоритет такой: модель → корень схемы → секции конфигурации. Массив заменяется целиком; `[]` исключает модель из GraphQL и OpenAPI, но REST продолжает работать. Явный список не включает отсутствующий модуль. Связанные модели должны разрешать тот же формат. Имена моделей должны быть допустимыми идентификаторами; конфликты типов и операций вызывают ошибку. Имена `and`, `or`, `not` зарезервированы фильтрами.
55
147
 
56
- Создайте ESM-модуль `server.config.js`. В примере ниже показаны настройки всех возможностей:
148
+ | Возможность | Со схемой | Без схемы |
149
+ |---|---|---|
150
+ | REST CRUD | Валидация модели | Только проверки JSON, тела запроса и идентификаторов |
151
+ | Связи | Поля схемы | По ключам `countryId`, `genreIds` и аналогичным |
152
+ | Экспорт OpenAPI 3.0.3 | Доступен | Ошибка при запросе экспорта |
153
+ | GraphQL SDL / API | Доступен | Ошибка при запросе |
57
154
 
58
- ```js
59
- import process from 'node:process';
155
+ При явной схеме неизвестные поля и коллекции запрещены. Исключение — хранимые ключи, выведенные из связей. Исходная база проверяется при загрузке. Генерация использует описание моделей.
60
156
 
61
- export default {
62
- database: {
63
- path: process.env.DATABASE_PATH ?? 'mock/database.json',
64
- schema: 'mock/database-schema.json',
65
- },
66
- files: {
67
- directory: 'mock/files',
68
- metadata: 'mock/files/_database.json',
69
- },
70
- openapi: {
71
- path: 'mock/openapi-schema.yaml',
72
- },
73
- server: {
74
- cors: true,
75
- host: '127.0.0.1',
76
- logger: true,
77
- maxFileSize: 100 * 1024 * 1024,
78
- maxPageSize: 1000,
79
- port: 4001,
80
- },
81
- };
82
- ```
157
+ REST без схемы создаёт ключ `id` и сохраняет произвольные JSON-поля. Для фильтров и выбора отдельных полей используются имена в формате идентификаторов; остальные поля возвращаются через `scope=[{"*":true}]`. Поля с разными типами значений можно читать, но фильтрация, сортировка и пагинация неоднородных списков требуют явной схемы.
83
158
 
84
- Ключи конфигурации:
159
+ У модели обязательны `collection` — имя коллекции в базе и REST-пути — и `fields` — описание полей. Имя модели (`User`) задаёт имена типов и операций GraphQL. `api` управляет доступностью форматов, а `timestamps` и `softDelete` переопределяют глобальные настройки для этой модели.
85
160
 
86
- | Ключ | Условие | Назначение |
87
- | --- | --- | --- |
88
- | `database.path` | Требуется ровно один из `path` или `data` | Существующий JSON-файл базы данных |
89
- | `database.data` | Требуется ровно один из `path` или `data` | Объект базы данных, хранящийся в памяти |
90
- | `database.schema` | Необязателен | Путь к JSON-настройкам либо объект с настройками проверки запросов и OpenAPI |
91
- | `files.directory` | Вместе с `files.metadata` | Директория для бинарного содержимого на диске |
92
- | `files.metadata` | Вместе с `files.directory` | JSON-файл с метаданными файлов на диске |
93
- | `files.data` | Вместо пары `directory` и `metadata` | Файлы в памяти с содержимым в `Uint8Array` |
94
- | `openapi.path` | Обязателен для CLI-флагов `--openapi` и `--openapi-only` | Генерируемый YAML-файл OpenAPI; программный API может вернуть документ без этого пути |
95
- | `server.cors` | Необязателен | Включает разрешающие CORS-заголовки и маршруты `OPTIONS`; по умолчанию `true` |
96
- | `server.host` | Необязателен | Адрес для CLI, `server.openapi()` и вызова `server.fastify().listen()` без аргументов; по умолчанию `127.0.0.1` |
97
- | `server.logger` | Необязателен | Настройки логгера Fastify; по умолчанию `true` |
98
- | `server.maxFileSize` | Необязателен | Максимальный размер загружаемого файла в байтах при включённых файловых маршрутах; по умолчанию 100 МиБ |
99
- | `server.maxPageSize` | Необязателен | Максимальное значение `_perPage` в API и OpenAPI; по умолчанию `1000` |
100
- | `server.port` | Необязателен | Порт для CLI, `server.openapi()` и вызова `server.fastify().listen()` без аргументов; по умолчанию `4001` |
161
+ ### Поля
101
162
 
102
- `server.port` должен быть целым числом от `0` до `65535`. Значение `0` позволяет Fastify выбрать свободный порт при запуске, но не подходит для генерации URL в OpenAPI, где допустимы порты от `1` до `65535`. `server.maxFileSize` и `server.maxPageSize` должны быть положительными целыми числами.
163
+ Поле `type` принимает `string`, `number`, `boolean`, `object` или имя модели. Для массива добавьте суффикс `[]`: `string[]`, `object[]`, `Genre[]`. Вложенные поля описываются через точку, например `actors.fullName`.
103
164
 
104
- Все относительные пути вычисляются от директории с `server.config.js`, а не от текущей рабочей директории. Неизвестные ключи, пустые пути и значения некорректных типов отклоняются до запуска. Конфиг является исполняемым JavaScript: в нём можно читать переменные окружения, импортировать другие модули и вычислять значения перед экспортом объекта. Для `.js`-конфига с `export default` проект должен быть ESM (`"type": "module"`); в CommonJS-проекте сохраните тот же конфиг как `server.config.mjs`.
165
+ | Свойства | Назначение |
166
+ |---|---|
167
+ | `type` | Обязательный тип |
168
+ | `description`, `example` | Документация и пример |
169
+ | `required`, `nullable` | По умолчанию `false`; наличие поля и разрешение `null` независимы |
170
+ | `default` | Значение при пропуске в create/replace; PATCH не вставляет значения по умолчанию |
171
+ | `enum` | Допустимые строки, числа или логические значения; у массива — значения каждого элемента |
172
+ | `primary` | Корневой первичный ключ: обязательный, уникальный, неизменяемый, без `null` |
173
+ | `generated` | `uuid` для строк, `increment` для чисел; значение создаёт сервер |
174
+ | `readOnly`, `writeOnly` | Только ответ или только входные данные; взаимно исключаются |
175
+ | `minLength`, `maxLength`, `pattern` | Ограничения строк |
176
+ | `format` | `date`, `date-time`, `email`, `uri`, `uuid` |
177
+ | `minimum`, `maximum` | Включительные числовые границы |
178
+ | `source`, `target`, `onDelete` | Описание связи |
105
179
 
106
- В том же конфиге все данные можно разместить в памяти. `database.path` и `database.data` взаимоисключающие, а `database.schema` принимает путь или объект. Аналогично, `files.data` нельзя сочетать с `files.directory` или `files.metadata`:
180
+ Ограничения строк и чисел применяются к каждому элементу `string[]`/`number[]`. `required` и `nullable` относятся ко всему массиву; `default` и `example` содержат массив целиком. Элементы массива должны соответствовать его типу и быть отличны от `null`. `required` требует наличия поля; обычный массив при этом может быть пустым.
107
181
 
108
- ```js
109
- export default {
110
- database: {
111
- data: { movies: [{ id: '1', title: 'Тени Ардении' }] },
112
- schema: { $info: { title: 'API фильмов', version: '1.0.0' } },
113
- },
114
- files: {
115
- data: [{ content: new Uint8Array([1, 2, 3]), directory: 'examples', mimeType: 'application/octet-stream', name: 'example.bin' }],
116
- },
117
- };
118
- ```
182
+ У модели ровно один первичный ключ типа `string` или `number`, объявленный на верхнем уровне. Имя произвольное: `id`, `username`, `code`. Если `generated` не задан, значение передаёт клиент при создании. Генерируемые поля объявляются на верхнем уровне, исключаются из входных типов и несовместимы с `default`. При замене записи сохраняются генерируемые значения и поля `readOnly`, включая вложенные объекты. Для защиты полей внутри массива задайте `readOnly` всему массиву или содержащему его объекту. Объекты, состоящие из серверных полей, доступны только в ответах.
183
+
184
+ Например, `LocalUser` с первичным ключом `username` и полем `password: {"type":"string","required":true,"writeOnly":true}` получает запрос `localUser(username: ...)` и маршрут `/localUsers/{username}`. Поле `writeOnly` доступно для записи и исключено из ответов, `scope`, фильтров и сортировки.
119
185
 
120
- Значения в памяти клонируются при инициализации. Поэтому операции с CRUD и файлами не изменяют экспортированный объект конфига, а их результаты исчезают после завершения процесса.
186
+ Объекты в GraphQL должны содержать хотя бы одно поле, доступное в ответе; REST допускает и пустые объекты.
121
187
 
122
- Добавьте нужные команды в `package.json`. Здесь `mock:openapi:files` сначала обновляет OpenAPI, а затем оставляет сервер запущенным с файловыми маршрутами:
188
+ ### Связи
123
189
 
124
190
  ```json
125
191
  {
126
- "scripts": {
127
- "mock": "deep-json-server server.config.js",
128
- "mock:files": "deep-json-server --files server.config.js",
129
- "mock:openapi:files": "deep-json-server --files --openapi server.config.js",
130
- "openapi": "deep-json-server --openapi-only server.config.js",
131
- "openapi:files": "deep-json-server --files --openapi-only server.config.js"
192
+ "actors.genres": {
193
+ "type": "Genre[]",
194
+ "source": "actors.genreIds",
195
+ "required": true
132
196
  }
133
197
  }
134
198
  ```
135
199
 
136
- Режимы CLI:
200
+ `Genre` возвращает объект, `Genre[]` — список. По умолчанию `source` указывает на первичный ключ текущей модели, `target` — целевой. Это правило действует и для вложенных связей. Пути задаются от корня соответствующей записи: в примере `actors.genreIds` содержит ключи жанров текущего актёра.
137
201
 
138
- | Команда | Поведение |
139
- | --- | --- |
140
- | `deep-json-server server.config.js` | Запускает CRUD-сервер без файловых маршрутов |
141
- | `deep-json-server --files server.config.js` | Запускает CRUD-сервер с файловыми маршрутами |
142
- | `deep-json-server --openapi server.config.js` | Генерирует OpenAPI и запускает CRUD-сервер |
143
- | `deep-json-server --files --openapi server.config.js` | Генерирует OpenAPI с файловыми маршрутами и запускает сервер с ними |
144
- | `deep-json-server --openapi-only server.config.js` | Генерирует OpenAPI и завершает работу |
145
- | `deep-json-server --files --openapi-only server.config.js` | Генерирует OpenAPI с файловыми маршрутами и завершает работу |
202
+ Ключи связей хранятся в базе и входят в собственные поля записи. Их типы выводятся из сопоставляемых ключей. Если поле `source` ссылается на первичный ключ целевой модели, его можно не объявлять отдельно: для множественной связи создаётся описание массива ключей, для одиночной — одного значения. При неоднозначном сопоставлении опишите хранимое поле явно.
146
203
 
147
- Флаг `--files` независим: без него файловые маршруты не регистрируются и не добавляются в OpenAPI, даже если секция `files` присутствует в конфиге. Параметры `--openapi` и `--openapi-only` взаимоисключающие. Команда `deep-json-server --help` выводит краткую справку по CLI.
204
+ Обратная связь: `User.movies = {"type":"Movie[]","target":"actors.userId"}`. Фильм возвращается один раз, даже если совпало несколько актёров. Несколько совпадений для одиночной связи — ошибка.
148
205
 
149
- ## Пример базы данных
206
+ Каждый переданный ключ прямой связи должен указывать на существующую запись. `required: true` у связи требует хотя бы одну связанную запись до фильтрации и пагинации ответа. Обратная связь с первичным ключом в `source` может быть пустой, если не объявлена обязательной. Отсутствующая одиночная связь возвращает `null`.
207
+
208
+ `onDelete` срабатывает **при удалении целевой записи**:
209
+
210
+ - `restrict` — по умолчанию: запретить удаление, пока на цель ссылается сохраняемая запись.
211
+ - `cascade` — удалить ссылающуюся запись. Для `User.country` удаление страны удаляет пользователей. Для `Movie.actors.user` удаление пользователя удаляет соответствующие элементы `actors`, сохраняя фильм.
150
212
 
151
- Ниже приведён пример каталога фильмов с тестовыми данными. `Гангстер` связан с родительским жанром `Криминал`.
213
+ Каскадное удаление выполняется целиком, включая циклические связи. Ошибка проверки отменяет всю операцию. Правила `onDelete` действуют и на явно объявленные обратные связи; при описании обоих направлений учитывайте оба правила.
214
+
215
+ ## Пример базы данных
152
216
 
153
217
  ```json
154
218
  {
155
219
  "countries": [
156
- { "id": "1", "isArchived": false, "name": "Ардения" },
157
- { "id": "2", "isArchived": false, "name": "Велория" }
220
+ {
221
+ "id": "1",
222
+ "isArchived": false,
223
+ "name": "Ардения"
224
+ },
225
+ {
226
+ "id": "2",
227
+ "isArchived": false,
228
+ "name": "Велория"
229
+ }
158
230
  ],
159
231
  "genres": [
160
- { "id": "1", "isArchived": false, "name": "Криминал", "parentIds": [] },
161
- { "id": "2", "isArchived": false, "name": "Гангстер", "parentIds": ["1"] },
162
- { "id": "3", "isArchived": false, "name": "Драма", "parentIds": [] },
163
- { "id": "4", "isArchived": false, "name": "Комедия", "parentIds": [] }
232
+ {
233
+ "id": "1",
234
+ "isArchived": false,
235
+ "name": "Криминал",
236
+ "parentIds": []
237
+ },
238
+ {
239
+ "id": "2",
240
+ "isArchived": false,
241
+ "name": "Гангстер",
242
+ "parentIds": ["1"]
243
+ },
244
+ {
245
+ "id": "3",
246
+ "isArchived": false,
247
+ "name": "Драма",
248
+ "parentIds": []
249
+ },
250
+ {
251
+ "id": "4",
252
+ "isArchived": false,
253
+ "name": "Комедия",
254
+ "parentIds": []
255
+ }
164
256
  ],
165
257
  "movies": [
166
258
  {
167
259
  "actors": [
168
- { "genreIds": ["2", "3"], "id": "movie-1-actor-1", "userId": "1" },
169
- { "genreIds": ["3"], "id": "movie-1-actor-2", "userId": "2" }
260
+ {
261
+ "genreIds": ["2", "3"],
262
+ "id": "movie-1-actor-1",
263
+ "userId": "1"
264
+ },
265
+ {
266
+ "genreIds": ["3"],
267
+ "id": "movie-1-actor-2",
268
+ "userId": "2"
269
+ }
170
270
  ],
171
271
  "coverSrc": "https://example.com/covers/shadows-of-ardenia.jpg",
172
272
  "description": "Наследница портового города раскрывает заговор двух соперничающих семей.",
@@ -186,8 +286,16 @@ export default {
186
286
  }
187
287
  ],
188
288
  "publishers": [
189
- { "id": "1", "isArchived": false, "name": "Northlight Studio" },
190
- { "id": "2", "isArchived": false, "name": "Aurora Pictures" }
289
+ {
290
+ "id": "1",
291
+ "isArchived": false,
292
+ "name": "Northlight Studio"
293
+ },
294
+ {
295
+ "id": "2",
296
+ "isArchived": false,
297
+ "name": "Aurora Pictures"
298
+ }
191
299
  ],
192
300
  "users": [
193
301
  {
@@ -208,153 +316,381 @@ export default {
208
316
  }
209
317
  ```
210
318
 
211
- Каждый массив верхнего уровня становится REST-ресурсом:
212
-
213
- ```text
214
- GET /movies
215
- GET /movies/:id
216
- POST /movies
217
- PUT /movies/:id
218
- PATCH /movies/:id
219
- DELETE /movies/:id
220
- ```
319
+ ## Запросы и ответы
221
320
 
222
- `POST` генерирует строковый ID. `PUT` полностью заменяет выбранную запись, а `PATCH` изменяет только переданные поля; обе операции сохраняют существующий ID и его тип. Поле `id` в теле любого запроса не может переопределить ID, которым управляет сервер. Все операции записи — `POST`, `PUT`, `PATCH` и `DELETE` — выполняются последовательно; дисковое хранилище записывает их в JSON, а хранилище в памяти сохраняет до завершения процесса.
321
+ Примеры с фильмами, актёрами и жанрами используют полную [схему из examples](examples/schema.json). Для их запуска используйте [конфигурацию примера](examples/server.config.js):
223
322
 
224
- Файл базы должен существовать до запуска. Имена ресурсов могут содержать латинские буквы, цифры, `_` и `-` и должны начинаться с буквы. Каждый ресурс является массивом JSON-объектов. У каждой записи должен быть непустой строковый или конечный числовой `id`; ID должны быть уникальны внутри ресурса при сравнении как строки, поэтому `1` и `"1"` не могут существовать одновременно. Все вложенные значения должны быть совместимы с JSON: конечные числа, строки, логические значения, `null`, массивы и обычные объекты. Перед каждым GET-запросом к ресурсу и изменением сервер заново читает файл, поэтому корректные правки существующих ресурсов становятся видны сразу. Имена ресурсов и маршруты определяются при запуске; после добавления, удаления или переименования массива верхнего уровня перезапустите сервер.
323
+ ```sh
324
+ npx deep-json-server examples/server.config.js
325
+ ```
225
326
 
226
- Успешная операция записи возвращает созданную, заменённую, обновлённую или удалённую запись. Ошибки используют подходящий HTTP-статус и следующий JSON-формат:
327
+ Коллекции и списки объектов, включая вложенные `object[]`, возвращают:
227
328
 
228
329
  ```json
229
- { "error": "..." }
330
+ { "data": [], "total": 0 }
230
331
  ```
231
332
 
232
- ## Пагинация и сортировка
333
+ Массивы примитивов возвращаются обычными массивами. Каждый список объектов принимает необязательные `where`, `order` и `pager`. Порядок обработки: фильтрация → сортировка → пагинация. `total` — число записей после фильтрации, до пагинации.
233
334
 
234
- ```http
235
- GET /movies?_page=1&_perPage=10&_sort=-id,title
236
- ```
335
+ В `pager` задаются `page` и `pageSize`. По умолчанию возвращается первая страница с размером из `server.pageSize`. Оба значения должны быть положительными целыми числами; `pageSize` ограничен `server.maxPageSize`. За пределами списка возвращается пустой `data` с общим числом найденных записей в `total`.
237
336
 
238
- GET-запрос к коллекции всегда возвращает объект с массивом текущей страницы и общим количеством записей после фильтрации. По умолчанию `_page` равен `1`, а `_perPage` — `10`:
337
+ Фильтры: `eq`, `ne`, `in`; для строк `contains`, `startsWith`, `endsWith`; сравнения `gt`, `gte`, `lt`, `lte`. Несколько условий в одном объекте должны выполняться одновременно. `and` и `or` принимают массив условий, `not` — одно условие; `not` также можно использовать внутри фильтра поля. У массивов есть `some`, `every`, `none`; у примитивных массивов также `contains`, `in`. `contains`, `startsWith` и `endsWith` для строк не учитывают регистр; `eq`, `ne` и `in` сравнивают точные значения. Условие поля задаётся объектом с оператором, например `{ "id": { "eq": "1" } }`.
239
338
 
240
339
  ```json
241
340
  {
242
- "data": [],
243
- "total": 0
341
+ "movies": {
342
+ "some": {
343
+ "actors": {
344
+ "some": {
345
+ "genres": { "some": { "id": { "in": ["2", "3"] } } }
346
+ }
347
+ }
348
+ }
349
+ }
244
350
  }
245
351
  ```
246
352
 
247
- `data` содержит записи только запрошенной страницы. `total` содержит количество всех записей, соответствующих фильтру, до применения пагинации. Номер последней страницы при необходимости вычисляется на клиенте как `Math.max(1, Math.ceil(total / pageSize))`.
353
+ Корневой `where` выбирает записи основной коллекции. `where` внутри связи фильтрует её элементы, сохраняя родительскую запись. Каждый вложенный список обрабатывается отдельно. Фильтрация по связи работает независимо от её включения в ответ.
248
354
 
249
- Оба параметра пагинации должны быть положительными целыми числами. По умолчанию `_perPage` не может превышать `1000`; лимит меняется через `server.maxPageSize` в конфиге, переданном CLI или `createServer()`. При некорректном значении сервер возвращает `400`, а не исправляет его автоматически. Страница после последней возвращает пустой массив `data`, но сохраняет фактическое значение `total`.
355
+ `order` — массив правил `{ "field": "fullName", "direction": "ASC" }`. `ASC` сортирует по возрастанию, `DESC` — по убыванию. Первое правило приоритетнее; при равных значениях сохраняется порядок хранения. `null` и отсутствующее значение сравниваются одинаково. REST использует путь `profile.name`, GraphQL — enum `profile_name`. Неоднозначные имена enum вызывают ошибку генерации. Сортировка доступна по скалярным полям текущего объекта, включая вложенные поля. Для связанных списков задаётся собственный `order`.
250
356
 
251
- `_sort` принимает пути полей через запятую. Правила применяются слева направо; префикс `-` включает сортировку по убыванию. Через точку можно обратиться к полю вложенного объекта, включая поля, добавленные через `_embed`, например `GET /users?_embed=country&_sort=country.name,-id`. Неизвестные и небезопасные поля сортировки возвращают `400`.
357
+ ### REST
252
358
 
253
- ## Фильтры
359
+ `GET /` возвращает имена коллекций: `{ "resources": ["users", "movies"] }`. Для каждой коллекции доступны следующие маршруты:
254
360
 
255
- Передайте JSON-объект через `_where`:
361
+ | Метод | Путь | Операция |
362
+ |---|---|---|
363
+ | GET | `/users` | `userList` |
364
+ | GET | `/users/{id}` | `user` |
365
+ | POST | `/users` | `userCreate` |
366
+ | PUT | `/users/{id}` | `userReplace` |
367
+ | PATCH | `/users/{id}` | `userUpdate` |
368
+ | DELETE | `/users/{id}` | `userDelete` |
256
369
 
257
- ```http
258
- GET /movies?_where={"title":{"contains":"тени"}}
370
+ Имя параметра пути соответствует первичному ключу. POST, PUT и PATCH принимают JSON-объект записи. PUT заменяет запись с сохранением ключа и серверных полей. PATCH объединяет поля на верхнем уровне; переданные вложенные объекты заменяются с сохранением их полей `readOnly`. Создание и замена требуют всех обязательных полей. При обновлении проверяются переданные значения и итоговая запись. Отсутствующая запись — `404`, конфликт — `409`.
371
+
372
+ POST возвращает созданную запись со статусом `201`; PUT, PATCH и DELETE — результат со статусом `200`. Ошибки REST имеют вид `{ "error": "Описание ошибки" }`.
373
+
374
+ ### Параметры REST-запросов
375
+
376
+ Для запросов к записям REST принимает один параметр URL `scope` с JSON-массивом `[поля, аргументы?]`. Первый объект выбирает поля, второй задаёт `where`, `order` и `pager` для списка. Этот формат одинаков для корневого запроса, вложенных объектов и связей.
377
+
378
+ Пример выбора пользователей и их фильмов с отдельной сортировкой и пагинацией:
379
+
380
+ ```js
381
+ const scope = [
382
+ {
383
+ id: true,
384
+ fullName: true,
385
+ movies: [
386
+ { id: true, title: true },
387
+ { order: [{ field: 'title', direction: 'ASC' }], pager: { page: 1, pageSize: 5 } },
388
+ ],
389
+ },
390
+ {
391
+ where: { fullName: { contains: 'Мира' } },
392
+ order: [{ field: 'fullName', direction: 'ASC' }],
393
+ pager: { page: 1, pageSize: 20 },
394
+ },
395
+ ];
396
+ const params = new URLSearchParams({ scope: JSON.stringify(scope) });
397
+ const response = await fetch(`/users?${params}`);
259
398
  ```
260
399
 
261
- Можно фильтровать вложенные объекты и массивы на любой глубине. Условия внутри одного объекта по умолчанию объединяются через `AND`:
400
+ Обычные поля выбираются через `true`, объекты и связи — через свой массив `scope`. Если аргументы не нужны, в массиве остаётся только объект полей. `"*": true` включает собственные поля и хранимые ключи, кроме `writeOnly`; связи выбираются явно.
401
+
402
+ Например, собственные поля фильма, пользователи актёров и отсортированные жанры:
262
403
 
263
404
  ```json
264
- {
265
- "actors": { "some": { "userId": { "eq": "1" } } },
266
- "title": { "contains": "тени" }
267
- }
405
+ [
406
+ {
407
+ "*": true,
408
+ "actors": [
409
+ {
410
+ "user": [{ "id": true, "fullName": true }],
411
+ "genres": [
412
+ { "*": true },
413
+ { "order": [{ "field": "name", "direction": "ASC" }] }
414
+ ]
415
+ }
416
+ ]
417
+ }
418
+ ]
268
419
  ```
269
420
 
270
- Для явных логических групп используйте `and`, `or` и `not`:
421
+ Без `scope` возвращаются собственные поля, как при `[{"*":true}]`. Пустой выбор `[{}]` возвращает объект без полей. Списки сохраняют структуру `{ data, total }`.
422
+
423
+ Аргументы доступны только у списков. В запросе отдельной записи и в ответе мутации их можно задать для вложенных списков. Параметры проверяются даже на пустых данных; ошибка в выборе ответа отменяет изменения записи. Некорректный `scope` возвращает `400`. Максимальная длина JSON — 10 000 символов, глубина выбора — 32 уровня.
424
+
425
+ ### Вложенная запись
426
+
427
+ Поля ключей, например `genreIds: ["1"]`, только задают связь. В поля связей можно передавать записи для создания или обновления:
428
+
429
+ ```http
430
+ PATCH /movies/1
431
+ Content-Type: application/json
271
432
 
272
- ```json
273
433
  {
274
- "and": [
434
+ "actors": [
275
435
  {
276
- "or": [
277
- { "title": { "contains": "тени" } },
278
- { "actors": { "some": { "userId": { "eq": "2" } } } }
436
+ "userId": "1",
437
+ "genres": [
438
+ "1",
439
+ { "id": "2", "name": "Обновлённый жанр" },
440
+ { "name": "Новый жанр" }
279
441
  ]
280
- },
281
- { "not": { "isArchived": { "eq": true } } }
442
+ }
282
443
  ]
283
444
  }
284
445
  ```
285
446
 
286
- Операторы полей:
447
+ | Значение в связи | Действие |
448
+ |---|---|
449
+ | Ключ, например `"1"` | Связать существующую запись без её изменения |
450
+ | Объект с первичным ключом | PATCH обновляет переданные поля, PUT заменяет связанную запись |
451
+ | Объект без первичного ключа | Создать запись со значениями по умолчанию и сгенерированным ключом |
287
452
 
288
- | Оператор | Поведение |
289
- | --- | --- |
290
- | `eq`, `ne` | Равенство или неравенство |
291
- | `contains` | Подстрока без учёта регистра для строк или совпадающий элемент массива |
292
- | `startsWith`, `endsWith` | Начало или окончание строки без учёта регистра |
293
- | `gt`, `gte`, `lt`, `lte` | Сравнение значений; строки дат ISO можно сравнивать лексикографически |
294
- | `in` | Совпадение скалярного значения или элемента массива с одним из переданных значений |
295
- | `some`, `every`, `none` | Применение вложенного условия к элементам массива |
296
- | `not` | Отрицание вложенного условия поля |
453
+ Имя и тип ключа берутся из целевой модели. Объект только с ключом тоже считается обновлением: в PUT он должен содержать обязательные поля модели. При замене сохраняются первичный ключ, генерируемые значения и поля `readOnly`. В POST вложенные объекты с существующими ключами обновляются частично. Если запись по ключу не найдена, операция завершится ошибкой. Для создания вложенной записи без ключа нужна его автоматическая генерация.
297
454
 
298
- Также можно использовать простые query-параметры:
455
+ Переданный список заменяет состав связи. PATCH сохраняет пропущенные связи, PUT очищает пропущенные связи, ключи которых доступны для записи. `[]` очищает список, `null` — одиночную связь с разрешённым `nullable`. Разрыв связи не удаляет связанную запись. Обязательные связи должны оставаться заполненными.
299
456
 
300
- ```http
301
- GET /movies?title:contains=тени
457
+ В одном объекте указывайте либо связь, либо её хранимый ключ: например, `genres` или `genreIds`. Для обратной связи сервер меняет целевой ключ. Если путь проходит через массив и нельзя однозначно выбрать элемент для связи, передайте массив с нужными ключами явно. Защищённые ключи изменять нельзя.
458
+
459
+ Все вложенные изменения входят в транзакцию основной записи. Ошибка проверки, отсутствующая запись или неверный выбор полей ответа отменяет всю операцию. Изменения общей записи видны всем, кто с ней связан.
460
+
461
+ GraphQL принимает в полях связей типизированные объекты. Чтобы при замене изменить только связи, используйте поля ключей, например `genreIds`. Пример:
462
+
463
+ ```graphql
464
+ mutation {
465
+ movieUpdate(id: "1", data: {
466
+ actors: [{
467
+ userId: "1"
468
+ genres: [{ id: "2", name: "Обновлённый жанр" }, { name: "Новый жанр" }]
469
+ }]
470
+ }) {
471
+ actors { data { genres { data { id name } } } }
472
+ }
473
+ }
302
474
  ```
303
475
 
304
- В простых фильтрах распознаются JSON-примитивы: числа, `true`, `false` и `null`. Значения с ведущими нулями, например `001`, остаются строками. Неизвестные операторы, некорректные логические условия и отсутствующие в непустом ресурсе пути фильтра возвращают `400`.
476
+ ### GraphQL
305
477
 
306
- Разные простые query-фильтры объединяются через `AND`. Повтор одного фильтра равенства выбирает любое из его значений, поэтому `GET /movies?id=1&id=2` равнозначен `GET /movies?id:in=1,2`. Для оператора `in` значения перечисляются через запятую. Для поля-массива `in` означает, что хотя бы один элемент поля совпадает хотя бы с одним переданным значением. `every` для пустого массива возвращает `true`, а `some` — `false`.
478
+ Укажите `database.schema` и добавьте секцию `graphql: {}` в конфигурацию:
307
479
 
308
- Если передан `_where`, он становится полным фильтром, а остальные простые параметры фильтрации игнорируются. В примерах JSON оставлен читаемым; при ручном формировании URL HTTP-клиент должен закодировать значение `_where`, например через `encodeURIComponent(JSON.stringify(where))`.
480
+ ```sh
481
+ npx deep-json-server server.config.js
482
+ ```
309
483
 
310
- Фильтрация выполняется после `_embed`. Поэтому фильтр может обращаться к полям добавленной связи, если в том же запросе указан соответствующий `_embed`; сохранённые поля `...Id` и `...Ids` всегда можно фильтровать напрямую.
484
+ Отправляйте запросы на `/graphql` методом POST с `Content-Type: application/json` и телом `{ "query": "…", "variables": {} }`. Путь можно изменить через `graphql.endpoint`.
485
+
486
+ ```graphql
487
+ query {
488
+ userList(
489
+ where: { fullName: { contains: "Мира" } }
490
+ order: [{ field: fullName, direction: ASC }]
491
+ pager: { page: 1, pageSize: 20 }
492
+ ) {
493
+ total
494
+ data {
495
+ id
496
+ fullName
497
+ movies(
498
+ where: { title: { contains: "Тени" } }
499
+ order: [{ field: title, direction: ASC }]
500
+ pager: { pageSize: 5 }
501
+ ) {
502
+ total
503
+ data { id title }
504
+ }
505
+ }
506
+ }
507
+ }
508
+ ```
311
509
 
312
- ## Связи
510
+ Запрос `user(id: ...)` возвращает одну запись или `null`, если она отсутствует. Мутации: `userCreate(data: ...)`, `userReplace(id: ..., data: ...)`, `userUpdate(id: ..., data: ...)`, `userDelete(id: ...)`. Для модели, состоящей из генерируемых полей, мутация создания вызывается без аргумента `data`. Правила записи и валидации совпадают с REST. Связи, выбранные в результате мутации, определяют содержимое ответа.
313
511
 
314
- Используйте `_embed`, чтобы добавить связанные записи в ответ:
512
+ Строковый первичный ключ представлен типом GraphQL `ID`, обычные строки — `String`, числа — `Float`, параметры пагинации — `Int`. Enum сохраняет допустимые строковые имена; остальные значения получают имена `VALUE_0`, `VALUE_1` и т. д. Длины строк, форматы и другие ограничения модели проверяются сервером при выполнении запроса. Для просмотра схемы доступна интроспекция. Параметры выбранных списков проверяются до выполнения мутаций.
315
513
 
316
- ```http
317
- GET /movies/1?_embed=actors.user.country&_embed=actors.genres&_embed=publishers
514
+ Ошибки содержат `extensions.code`: `INVALID_INPUT`, `INVALID_QUERY`, `NOT_FOUND`, `CONFLICT`, `UNAUTHENTICATED`, `FORBIDDEN` или `INTERNAL_ERROR`. Ошибки синтаксиса и типов GraphQL возвращаются в стандартном массиве `errors`. Максимальная глубина запроса — 32 уровня.
515
+
516
+ ## OpenAPI и экспорт схем
517
+
518
+ Экспорт использует OpenAPI 3.0.3. Добавьте `openapi: {}` и `database.schema`, чтобы получать спецификацию по HTTP на `/openapi.json`. Путь меняется через `openapi.endpoint`. Спецификацию можно открыть в Swagger UI или импортировать в API-клиент.
519
+
520
+ Для сохранения схем задайте пути экспорта:
521
+
522
+ ```js
523
+ export default {
524
+ storage: 'file',
525
+ database: { source: './database.json', schema: './schema.json' },
526
+ openapi: { target: './generated/openapi.yaml' },
527
+ graphql: { target: './generated/schema.graphql' },
528
+ };
318
529
  ```
319
530
 
320
- Ответ будет содержать актёров, пользователя и жанры каждого актёра, страну пользователя и издателей. Исходные поля с ID остаются в ответе, а файл базы не изменяется. Сервер не устанавливает фиксированный лимит глубины, но каждый требуемый уровень должен быть явно указан в конечном пути `_embed`:
531
+ ```bash
532
+ npx deep-json-server server.config.js --generate-only
533
+ npx deep-json-server server.config.js --generate
534
+ ```
321
535
 
322
- ```http
323
- GET /movies/1?_embed=actors.user.country
324
- GET /genres/2?_embed=parents.parents
536
+ `--generate-only` экспортирует и завершает работу, `--generate` после экспорта запускает сервер. Форматы определяются наличием секций. Для каждого выбранного формата обязателен свой `target`. Если секций нет, отсутствует `target` или генерация завершилась ошибкой, команда возвращает ошибку и сервер не запускается.
537
+
538
+ Экспорт не открывает базу, учётные записи и файлы. Перед записью проверяются все выбранные `target`; они не могут совпадать с конфигурацией, базой, схемой, пользователями, счётчиками или метаданными файлов.
539
+
540
+ ## Аутентификация
541
+
542
+ Секция `auth` включает регистрацию, вход и проверку прав на изменение записей в REST и GraphQL. Чтение и все операции с файлами остаются открытыми.
543
+
544
+ Первого администратора задайте в исходных данных. Например, создайте `auth.json` с помощью `setup-auth.mjs`:
545
+
546
+ ```js
547
+ import { writeFile } from 'node:fs/promises';
548
+ import { hashPassword } from '@kollors/deep-json-server/auth';
549
+
550
+ const password = process.env.DJS_PASSWORD;
551
+ if (!password) throw new Error('Set DJS_PASSWORD');
552
+ await writeFile(
553
+ './auth.json',
554
+ JSON.stringify(
555
+ [{ id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true }],
556
+ null,
557
+ 2,
558
+ ),
559
+ { flag: 'wx', mode: 0o600 },
560
+ );
325
561
  ```
326
562
 
327
- Неизвестные и некорректные пути `_embed` возвращают `400`.
563
+ Задайте `DJS_PASSWORD` и выполните `node setup-auth.mjs`. Добавьте файл в конфигурацию сервера:
328
564
 
329
- Параметр `_embed` можно передать несколько раз, как в примере выше, либо перечислить пути через запятую в одном параметре. Пагинация применяется только к запрошенной корневой коллекции; вложенные связанные записи возвращаются полностью.
565
+ ```js
566
+ export default {
567
+ storage: 'file',
568
+ database: { source: './database.json' },
569
+ auth: { source: './auth.json', expiresIn: 3600 },
570
+ };
571
+ ```
330
572
 
331
- Поддерживаются и обратные связи:
573
+ Запустите `npx deep-json-server server.config.js`. Каждой исходной учётной записи нужны уникальный строковый `id`, уникальный `username` и `passwordHash`, созданный функцией выше. `isAdmin` по умолчанию равен `false`. Пароли хешируются через scrypt со случайной солью.
332
574
 
333
- ```http
334
- GET /countries/1?_embed=users
575
+ При `storage: 'memory'` передайте массив в `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 хранятся отдельно от базы. В файловом режиме изменения сохраняются в `auth.source`; в режиме памяти они исчезают после перезапуска. Переданный массив пользователей не изменяется. После ручного изменения файла перезапустите сервер.
595
+
596
+ | REST-запрос | JSON тела | Ответ |
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
+ Регистрация и вход доступны без токена. Для остальных методов передавайте `Authorization: Bearer <accessToken>`. Регистрация создаёт обычного пользователя с новым `id`; поля `id`, `passwordHash` и `isAdmin` в запросе запрещены. `username` уникален с учётом регистра, повтор возвращает `409`. Имя должно содержать хотя бы один непробельный символ и не более 256 символов, пароль — от 1 до 1024 символов. Значения не обрезаются. Регистрация не открывает сессию: после неё выполните вход.
606
+
607
+ Пользователь меняет только свой пароль, передавая `currentPassword` и `newPassword`. Для администратора при смене своего пароля действует то же правило. Администратор может сменить пароль обычному пользователю, передав только `newPassword`. Изменение пароля другого администратора возвращает `403`. После успешной смены завершаются все сессии этого пользователя, включая текущую при смене своего пароля; требуется новый вход. Неверный текущий пароль возвращает `401` без изменения сессий.
608
+
609
+ Статус `isAdmin` меняет только администратор. Можно назначать других администраторов и снимать с них статус. Снять статус с себя можно, только если остаётся другой администратор; иначе возвращается `409`. Проверка учитывает параллельные запросы. Новые права действуют с прежними токенами сразу после сохранения, в том числе для мутаций GraphQL. Администратор может снять статус с другого администратора, а затем сменить ему пароль как обычному пользователю.
610
+
611
+ Недействительный или истёкший токен возвращает `401`, недостаток прав — `403`, отсутствующий пользователь при разрешённой операции — `404`. Ошибки тела запроса возвращают `400`. Вход, регистрация и смена пароля могут вернуть `429` при превышении числа одновременных вычислений паролей; вход также ограничивает число активных сессий. Сессии хранятся в памяти и исчезают после перезапуска. Выход завершает только сессию переданного токена.
612
+
613
+ OpenAPI описывает все маршруты auth и требования Bearer-токена. В Swagger UI токен из ответа на вход можно вставить в **Authorize**. Для экспорта схем включите auth в конфигурации и выполните `npx deep-json-server server.config.js --generate-only`; файл учётных записей при генерации не читается. Методы auth доступны через REST. В GraphQL тот же токен проверяется при изменении записей. Для GraphQL и OpenAPI нужна `database.schema`.
614
+
615
+ ## Даты записей, удаление и владельцы
616
+
617
+ Общие настройки задаются в корне схемы. В этом примере модель `Note` наследует мягкое удаление и отключает даты:
618
+
619
+ ```json
620
+ {
621
+ "timestamps": true,
622
+ "softDelete": true,
623
+ "models": {
624
+ "Note": {
625
+ "collection": "notes",
626
+ "timestamps": false,
627
+ "fields": {
628
+ "id": {
629
+ "type": "string",
630
+ "primary": true,
631
+ "generated": "uuid"
632
+ },
633
+ "text": {
634
+ "type": "string",
635
+ "required": true
636
+ }
637
+ }
638
+ }
639
+ }
640
+ }
335
641
  ```
336
642
 
337
- Связи определяются по именам полей. Поле `<relation>Id` создаёт одиночную связь, а `<relation>Ids` — связь с коллекцией. Имя связи сопоставляется с ресурсом верхнего уровня напрямую или через его форму в единственном числе. Например:
643
+ Приоритет: модель → корень схемы → `false`. Явное `false` отключает унаследованную настройку. Без схемы даты и мягкое удаление выключены.
644
+
645
+ | Поле | Когда включено | Значение |
646
+ |---|---|---|
647
+ | `createdAt`, `updatedAt` | `timestamps` | Даты создания и последнего изменения |
648
+ | `deletedAt` | `softDelete` | Дата удаления или `null` для активной записи |
649
+ | `createdById`, `updatedById` | `auth` | Создатель/владелец и последний редактор |
650
+ | `deletedById` | `auth` + `softDelete` | Пользователь, удаливший запись, или `null` |
338
651
 
339
- - `countryId` ссылается на `countries`;
340
- - `userId` ссылается на `users`, если запрошена связь `user`;
341
- - `genreIds` ссылается на `genres`;
342
- - `publisherIds` ссылается на `publishers`;
343
- - `parentIds` ссылается на тот же ресурс, если запрошена связь `_embed=parents`; `_embed=children` загружает обратную связь с дочерними записями.
652
+ Даты — строки ISO 8601 в UTC. При создании даты создания и изменения совпадают, а при включённом auth создателем и редактором становится текущий пользователь. PUT/PATCH сохраняют создателя и дату создания. DELETE обновляет поля удаления и включённые поля последнего изменения. Для старых записей с неизвестными датами или авторами возвращается `null`. Эти поля доступны только для чтения в REST и GraphQL; обычные вложенные объекты собственных полей авторства и дат не получают. При отключении функции уже сохранённые значения её полей сохраняются.
344
653
 
345
- Имя обратной связи совпадает с именем исходного ресурса. Например, `_embed=users` у страны находит пользователей, во вложенных данных которых указан соответствующий `countryId`. Сервер получает связи по запросу, но не проверяет ссылочную целостность при записи данных.
654
+ При мягком удалении DELETE оставляет запись в базе. Повторный DELETE уже удалённой записи сохраняет прежние сведения об удалении. Получение по первичному ключу возвращает и удалённые записи. Успешный PUT/PATCH восстанавливает запись, обнуляя `deletedAt` и `deletedById`. Пустой PATCH восстанавливает без замены остальных полей; для PUT нужно передать все обязательные поля.
655
+
656
+ Списки по умолчанию возвращают активные записи. Для получения удалённых задайте условие по `deletedAt`:
657
+
658
+ ```json
659
+ [
660
+ { "id": true, "deletedAt": true },
661
+ { "where": { "deletedAt": { "ne": null } } }
662
+ ]
663
+ ```
346
664
 
347
- Явные поля `...Id` и `...Ids` определяют связи. Если в записи также сохранено устаревшее вложенное значение, `_embed` заменяет это свойство ответа актуальной связанной записью. Если связанная запись для одиночной связи не найдена, результатом будет `null`; отсутствующие связанные записи коллекции не попадут в итоговый массив. ID-индексы создаются только для ресурсов, используемых при поиске связей в текущем запросе.
665
+ `eq: null` выбирает активные записи, `ne: null` — удалённые, объединение обоих условий через `or` — все записи. Те же фильтры работают в GraphQL. Явное условие по `deletedAt`, в том числе внутри `and`, `or` или `not`, заменяет автоматическую фильтрацию на этом уровне. Фильтры связей и списки связей применяют правило независимо. Одиночные связи скрывают удалённые цели; запрос по первичному ключу позволяет получить их напрямую.
666
+
667
+ Каскадное удаление учитывает `onDelete` и настройку `softDelete` каждой затронутой модели. Восстановление записи, начавшей каскад, возвращает записи, удалённые той же операцией, не затрагивая удалённые ранее. Физически удалённые записи восстановить нельзя. Если каскад удалил вложенный объект, восстановление возможно, пока соответствующее поле объекта не менялось после удаления. Конфликт или отсутствие обязательной связи отменяет всю операцию.
668
+
669
+ Служебные сведения восстановления хранятся вместе с записями и переживают перезапуск; сохраняйте их при резервном копировании базы. Внутреннее поле `djsDeletion` зарезервировано и не выдаётся через API.
670
+
671
+ При включённом auth создавать записи может любой вошедший пользователь. Изменять, удалять и восстанавливать — владелец по `createdById` или пользователь с `isAdmin: true`. Записи без владельца изменяет только администратор. Его правки не меняют владельца. Правила действуют на вложенные записи, изменение ключей связей, каскады и восстановление. Ссылка на существующую запись без её изменения не требует владения ею. Каждая мутация атомарна: отказ сохраняет прежнее состояние всех затронутых записей.
672
+
673
+ REST возвращает 401 при отсутствии действительного токена и 403 при недостатке прав. GraphQL проверяет мутации по тем же правилам и возвращает коды `UNAUTHENTICATED` или `FORBIDDEN`; токен получают через REST-вход и передают в `Authorization: Bearer <токен>`. GraphQL-запросы чтения, OPTIONS и все операции с файлами остаются открытыми.
348
674
 
349
675
  ## Файлы
350
676
 
351
- Добавьте `files.directory` и `files.metadata` в конфиг сервера, затем передайте `--files`, чтобы включить загрузку бинарных файлов:
677
+ Файлы доступны через REST и описываются в OpenAPI. Добавьте хранилище в конфигурацию:
678
+
679
+ ```js
680
+ export default {
681
+ storage: 'file',
682
+ database: { source: './database.json' },
683
+ files: { source: './uploads' },
684
+ };
685
+ ```
686
+
687
+ Запустите сервер:
352
688
 
353
689
  ```bash
354
- deep-json-server --files server.config.js
690
+ npx deep-json-server server.config.js
355
691
  ```
356
692
 
357
- Для временных тестов вместо этого используйте `files.data`. Каждая начальная запись содержит `name`, `mimeType`, бинарное `content` в виде `Uint8Array` и необязательный `directory`. Загруженные файлы в таком режиме остаются в памяти до завершения процесса.
693
+ Для временных тестов выберите `storage: 'memory'` и передайте массив в `files.source`. Каждая начальная запись содержит `name`, `mimeType`, бинарное `content` в виде `Uint8Array` и необязательный `directory`. Загруженные файлы в таком режиме остаются в памяти до завершения процесса.
358
694
 
359
695
  Один файл отправляется непосредственно в теле запроса. `Content-Name` содержит URI-кодированное имя файла, `Content-Type` — его MIME-тип, а необязательный `Content-Directory` — URI-кодированный относительный путь к директории:
360
696
 
@@ -405,178 +741,61 @@ Content-Type: application/json
405
741
  }
406
742
  ```
407
743
 
408
- `PATCH` возвращает обновлённые метаданные со статусом `200`; если по новому пути уже существует файл, сервер возвращает `409`. `DELETE` отвечает статусом `204` без тела. Если файл не найден, любая операция по пути возвращает `404`. Пути в URL задаются относительно `files.directory`, а все возвращаемые URL — относительно адреса сервера.
409
-
410
- При хранении на диске бинарный файл находится по пути `<files.directory>/<directory>/<name>`. Файл метаданных содержит только `directory`, `mimeType` и `name`; `size` считывается у фактического файла, а URL вычисляются. Используйте дисковую базу и файловое хранилище только из одного процесса сервера одновременно и не редактируйте сохранённые файлы или метаданные до его остановки. Сервер автоматически создаёт директории, пути внутри `files.directory` не могут содержать символические ссылки, а имена файлов ограничены переносимыми между поддерживаемыми операционными системами значениями. Изначально файл метаданных может отсутствовать: он создаётся при первой загрузке. Метаданные, созданные версиями до перехода на адресацию по пути, несовместимы с новым форматом.
411
-
412
- Используется бинарное тело запроса, а не `multipart/form-data`, поэтому `XMLHttpRequest.upload.onprogress` может показывать прогресс при непосредственной отправке `File` через `xhr.send(file)`. Максимальный размер по умолчанию равен 100 МиБ и настраивается через `server.maxFileSize`. Отсутствующие или небезопасные заголовки и пути возвращают `400`, превышение лимита — `413`, а отсутствующий, некорректный или не поддерживаемый Fastify `Content-Type` — `400` либо `415` в зависимости от этапа проверки.
413
-
414
- ## Схема базы данных и генерация OpenAPI
415
-
416
- Необязательный путь или объект в `database.schema` настраивает автоматически выведенные схемы. Это собственный формат настроек Deep JSON Server, а не стандартный документ JSON Schema: `$schema` здесь является объектом с настройками ресурсов. Например, файл `mock/database-schema.json` может содержать:
417
-
418
- ```json
419
- {
420
- "$info": {
421
- "title": "API каталога фильмов",
422
- "version": "1.0.0"
423
- },
424
- "$schema": {
425
- "movies": {
426
- "required": ["actors", "actors.genreIds", "actors.userId", "publisherIds", "title"],
427
- "formats": {
428
- "coverSrc": "uri"
429
- }
430
- },
431
- "users": {
432
- "required": ["bornAt", "fullName"],
433
- "formats": {
434
- "avatarSrc": "uri",
435
- "bornAt": "date"
436
- }
437
- }
438
- }
439
- }
440
- ```
441
-
442
- Настройки схемы:
744
+ `PATCH` возвращает обновлённые метаданные со статусом `200`; если по новому пути уже существует файл, сервер возвращает `409`. `DELETE` отвечает статусом `204` без тела. Если файл не найден, любая операция по пути возвращает `404`. Пути в URL задаются относительно `files.source`, а все возвращаемые URL — относительно адреса сервера.
443
745
 
444
- | Ключ | Назначение |
445
- | --- | --- |
446
- | `$info` | Объект `info` в OpenAPI; если он указан, обязательны непустые `title` и `version` |
447
- | `$schema.<resource>.name` | Явное имя компонента, если автоматическое образование единственного числа не подходит или создаёт коллизию |
448
- | `$schema.<resource>.required` | Пути обязательных полей; вложенные пути записываются через точку, например `actors.userId` |
449
- | `$schema.<resource>.formats` | Форматы OpenAPI для автоматически найденных или явно описанных строковых полей, например `date`, `date-time` или `uri` |
450
- | `$schema.<resource>.properties` | Рекурсивные OpenAPI-совместимые схемы полей, объединяемые с автоматически найденными |
746
+ При хранении на диске бинарный файл находится по пути `<files.source>/<directory>/<name>`. Метаданные по умолчанию хранятся в `<files.source>/.files.json`; другой путь можно задать через `files.metadata`. Директории и файл метаданных создаются по мере необходимости.
451
747
 
452
- `formats` — сокращённая запись для назначения `format` уже существующему строковому полю. `properties` позволяет полностью описать поле, в том числе его `type`, `format`, ограничения и вложенные свойства, либо добавить поле, которого нет в данных. Если одно поле получает `format` через оба механизма, значение из `formats` применяется последним.
748
+ Используйте один процесс сервера для дисковой базы и файлового хранилища. Перед ручным изменением файлов или метаданных остановите его. Пути в хранилище не могут содержать символические ссылки. Загрузка и переименование не могут перезаписать базу, счётчики, схему, учётные записи auth, загруженную конфигурацию или файл метаданных.
453
749
 
454
- Укажите `openapi.path` в конфиге сервера, затем сгенерируйте OpenAPI 3.0.3 и завершите работу:
750
+ Отправляйте файл как бинарное тело запроса. В браузере для этого можно использовать `xhr.send(file)`, а прогресс отслеживать через `XMLHttpRequest.upload.onprogress`. Максимальный размер по умолчанию равен 100 МиБ и настраивается через `server.maxFileSize`. Отсутствующие или небезопасные заголовки и пути возвращают `400`, превышение лимита — `413`, а отсутствующий, некорректный или не поддерживаемый Fastify `Content-Type` — `400` либо `415` в зависимости от этапа проверки.
455
751
 
456
- ```bash
457
- deep-json-server --openapi-only server.config.js
458
- ```
459
-
460
- Чтобы включить в документ файловые маршруты, настройте секцию `files` и добавьте флаг `--files`: `deep-json-server --files --openapi-only server.config.js`.
461
-
462
- Генератор определяет ресурсы и типы полей по всем записям базы. По умолчанию все найденные поля необязательные, а поле `id` верхнего уровня всегда обязательное в схемах ответа и исключается из схем создания и обновления. Остальные обязательные поля перечисляются в `required`. Вложенный обязательный путь делает обязательным само вложенное свойство, но не все его родительские пути; при необходимости родителя нужно перечислить отдельно.
463
-
464
- Разные непересекающиеся типы значений определяются независимо и объединяются через `oneOf`; сочетание целых и дробных чисел описывается одной схемой `number`. Перед генерацией проверяются `$info`, имена ресурсов и компонентов, поддерживаемые ключи `properties` и типы их значений; пути из `required` и `formats` должны существовать в итоговой схеме. Явные имена компонентов могут содержать латинские буквы, цифры, точки, подчёркивания и дефисы.
465
-
466
- Используйте `properties`, чтобы описать поля, которые невозможно определить автоматически, особенно у пустого ресурса. Явно заданные свойства объединяются с найденными автоматически:
467
-
468
- ```json
469
- {
470
- "$schema": {
471
- "reviews": {
472
- "properties": {
473
- "rating": { "type": "integer", "minimum": 1, "maximum": 5 },
474
- "text": { "type": "string" }
475
- },
476
- "required": ["rating"]
477
- }
478
- }
479
- }
480
- ```
481
-
482
- Пустой ресурс всё равно получает обязательное строковое поле `id`, поскольку сервер создаёт строковые ID. При совпадении имён схем или `operationId` генерация завершается понятной ошибкой; коллизию имён схем можно устранить с помощью явного `name`. Директория результата создаётся автоматически, а настроенный YAML-файл заменяется при каждой генерации.
483
-
484
- `$info` становится объектом `info` в OpenAPI, а настройки ресурсов находятся внутри `$schema`. В CLI поле `servers` формируется из `server.host` и `server.port`, затем из резервных переменных окружения `HOST` и `PORT`, а при их отсутствии используется `http://127.0.0.1:4001`. При прямом вызове `createServer()` переменные окружения автоматически не читаются: `server.openapi()` использует значения конфига или тот же адрес по умолчанию.
752
+ ## Программный API
485
753
 
486
- Используйте `name`, если ресурсу нужно явно задать имя схемы вместо автоматически полученного имени в единственном числе:
754
+ ```js
755
+ import { createServer } from '@kollors/deep-json-server/server';
756
+ import config from './server.config.js';
487
757
 
488
- ```json
489
- {
490
- "$schema": {
491
- "equipment": {
492
- "name": "Equipment"
493
- }
494
- }
495
- }
758
+ const facade = await createServer(config);
759
+ const server = facade.fastify();
760
+ await server.listen();
761
+ // await server.close();
496
762
  ```
497
763
 
498
- В сгенерированном документе описаны CRUD-маршруты, пагинация, сортировка, фильтрация вложенных данных, `_embed`, а также прямые и обратные связи в ответах, определённые по полям `...Id` и `...Ids`. При наличии `--files` в него также добавляются маршруты бинарной загрузки, получения и удаления файлов с поддержкой произвольных MIME-типов. Числовой ID базы описывается как `integer | string`, поскольку последующий `POST` создаст строковый ID в том же ресурсе. До создания документа генератор отклоняет повторяющиеся имена схем и `operationId`, а также некорректные переопределения схем. Файл можно передать, например, в RTK Query OpenAPI Codegen. OpenAPI создаётся только с параметром `--openapi` или `--openapi-only`; обычный запуск сервера файл не перезаписывает.
764
+ Методы `openapi()` и `graphql()` возвращают схемы и требуют `database.schema`. `fastify()` возвращает экземпляр сервера для настройки и запуска. База и включённые сервисы инициализируются при `ready()`, `listen()` или первом `inject()`; ошибка инициализации останавливает запуск.
499
765
 
500
- При обычном запуске тела запросов проверяются по тем же автоматически выведенным и настроенным схемам. Для `POST` и `PUT` проверяются обязательные поля из `required`; `PATCH` проверяет только фактически переданные поля. Настройки `formats` и `properties` применяются ко всем трём методам. Не перечисленные дополнительные поля объекта остаются разрешёнными. Некорректные тела возвращают `400`.
766
+ `createServer(config)` принимает один аргумент. Наличие секций управляет модулями так же, как при запуске через CLI. Для методов `openapi()` и `graphql()` нужна соответствующая секция. Методы возвращают схему и не записывают файлы.
501
767
 
502
- ## Программный API
768
+ Эти функции доступны и через общий импорт `@kollors/deep-json-server`. Адаптеры сервера загружаются при включении. Генераторы можно использовать отдельно:
503
769
 
504
770
  ```js
505
- import { createServer } from '@kollors/deep-json-server';
771
+ import { generateOpenapi, writeOpenapi } from '@kollors/deep-json-server/openapi';
772
+ import { generateGraphql, writeGraphql } from '@kollors/deep-json-server/graphql';
506
773
 
507
- const config = {
508
- database: {
509
- path: 'mock/database.json',
510
- schema: 'mock/database-schema.json',
511
- },
512
- files: {
513
- directory: 'mock/files',
514
- metadata: 'mock/files/_database.json',
515
- },
516
- openapi: {
517
- path: 'mock/openapi-schema.yaml',
518
- },
519
- server: {
520
- cors: true,
521
- host: '127.0.0.1',
522
- logger: false,
523
- maxFileSize: 100 * 1024 * 1024,
524
- maxPageSize: 1000,
525
- port: 4001,
526
- },
527
- };
528
-
529
- // Запрос без открытия сетевого порта — удобно для автоматических тестов.
530
- const server = await createServer(config);
531
- const fastify = server.fastify();
532
- const response = await fastify.inject({ method: 'GET', url: '/movies' });
533
-
534
- console.log(response.json());
535
-
536
- // Возвращает документ и записывает его в config.openapi.path.
537
- const document = await server.openapi();
538
-
539
- await fastify.close();
774
+ const document = await generateOpenapi('./schema.json', { files: true });
775
+ const sdl = await generateGraphql('./schema.json');
776
+ await writeOpenapi(document, './generated/openapi.yaml');
777
+ await writeGraphql(sdl, './generated/schema.graphql');
778
+ ```
540
779
 
541
- // Запуск сетевого сервера. Вызов без аргументов использует server.host и server.port.
542
- const runningServer = await createServer(config);
543
- const runningFastify = runningServer.fastify();
780
+ Отдельные генераторы принимают путь или объект схемы и не требуют конфигурации сервера. Выбранная функция задаёт формат по умолчанию; `api` в схеме и моделях может его ограничить. `timestamps` и `softDelete` берутся из схемы. Опция `{ auth: true }` добавляет поля владельца; OpenAPI также описывает маршруты auth и требования токена. `hashPassword()` доступна и через общий импорт пакета.
544
781
 
545
- await runningFastify.listen();
782
+ `generateOpenapi()` также принимает `host`, `port`, `pageSize`, `maxPageSize` и `info`. Вместо пути можно передать объект схемы. Сервер и генераторы работают с собственной копией модели. Размеры страниц должны быть положительными целыми числами; `pageSize` не может превышать `maxPageSize`.
546
783
 
547
- // Позже, при завершении приложения:
548
- await runningFastify.close();
784
+ ## Хранение данных
549
785
 
550
- // База, схема и файлы полностью в памяти.
551
- const memoryServer = await createServer({
552
- database: {
553
- data: { movies: [{ id: '1', title: 'Тени Ардении' }] },
554
- schema: { $info: { title: 'API фильмов', version: '1.0.0' } },
555
- },
556
- files: {
557
- data: [{ content: new Uint8Array([1, 2, 3]), directory: 'examples', mimeType: 'application/octet-stream', name: 'example.bin' }],
558
- },
559
- });
786
+ Изменения выполняются последовательно внутри экземпляра сервера и проверяются на копии данных до сохранения. Для одного файла базы используйте один процесс сервера. Счётчики `increment` хранятся рядом с базой в `<путь базы>.counters.json`; сохраняйте этот файл вместе с базой. Номера резервируются до записи данных: сбой может оставить пропуск, но не приводит к повторной выдаче номера.
560
787
 
561
- const memoryFastify = memoryServer.fastify();
562
- const memoryResponse = await memoryFastify.inject({ method: 'GET', url: '/movies/1' });
788
+ ## Разработка
563
789
 
564
- console.log(memoryResponse.json());
790
+ Исходники разделены на `rest`, `graphql`, `openapi`, `auth`, `files`, `cli` и `server`. Общая модель, хранение, запросы и правила изменения записей находятся в `core`. Модуль `server` подключает остальные; генераторы схем загружаются независимо от HTTP-сервера.
565
791
 
566
- await memoryFastify.close();
792
+ ```sh
793
+ npm ci
794
+ npm run verify
567
795
  ```
568
796
 
569
- `createServer()` принимает точно ту же структуру конфига, что и `server.config.js`. Функция загружает и клонирует базу данных, схему и файловое хранилище, а затем возвращает объект с двумя методами:
570
-
571
- | Метод | Назначение |
572
- | --- | --- |
573
- | `server.fastify()` | Лениво создаёт и кэширует настоящий экземпляр Fastify; все нативные методы доступны, а `listen()` без аргументов использует `server.host` и `server.port` |
574
- | `server.openapi()` | Возвращает документ OpenAPI и дополнительно записывает его, если настроен `openapi.path` |
575
-
576
- При программном использовании файловые маршруты включаются при наличии секции `files`. Сигнатура второго аргумента — `{ files?: boolean }`: передайте `{ files: false }`, чтобы оставить настроенное хранилище выключенным, или `{ files: true }`, чтобы потребовать секцию `files` и включить маршруты. `server.openapi()` использует ту же настройку файловых маршрутов, что и `server.fastify()`.
577
-
578
- Вызов `server.fastify().listen()` без аргументов использует `server.host` и `server.port`, а при их отсутствии — `127.0.0.1:4001`. Явные параметры `listen(options)` имеют приоритет. Относительные пути, переданные напрямую в `createServer()`, вычисляются от текущей рабочей директории; пути из `server.config.js` — от директории конфига. Пакет содержит сгенерированные TypeScript-декларации возвращаемого объекта и всех вариантов конфигурации.
797
+ Команда проверяет типы, стиль кода, покрытие тестами и установку пакета из архива.
579
798
 
580
- ## Назначение и безопасность
799
+ Для публикации новой альфы обновите версию в `package.json`, `package-lock.json` и `src/core/constants.ts`, затем отправьте изменения в `main`. GitHub Actions создаст тег версии и опубликует пакет в канал npm `alpha` через trusted publishing. Уже опубликованная версия пропускается. Если тег создан, а публикация не завершилась, повторный запуск использует этот тег и проверяет соответствие ему файлов пакета. Отправка тега версии также запускает публикацию; стабильные версии публикуются в `latest`.
581
800
 
582
- Deep JSON Server предназначен для локальной разработки и автоматических тестов. В нём нет аутентификации и авторизации, CORS по умолчанию разрешён для любого источника, при дисковом хранилище принятые изменения сохраняются в настроенные файлы, а ссылочная целостность не проверяется. Установите `server.cors: false`, чтобы отключить встроенные CORS-заголовки и маршруты `OPTIONS`. Оставляйте адрес loopback по умолчанию, если внешняя среда не предоставляет собственный контроль доступа; не открывайте сервер и файловые маршруты для недоверенной сети.
801
+ Лицензия: MIT.