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

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 (244) 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 +113 -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 +53 -0
  42. package/dist/src/core/engine.js +249 -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/execute.d.ts +25 -0
  75. package/dist/src/core/query/execute.js +82 -0
  76. package/dist/src/core/query/execute.js.map +1 -0
  77. package/dist/src/core/query/filter.d.ts +6 -0
  78. package/dist/src/core/query/filter.js +100 -0
  79. package/dist/src/core/query/filter.js.map +1 -0
  80. package/dist/src/core/query/options.d.ts +30 -0
  81. package/dist/src/core/query/options.js +44 -0
  82. package/dist/src/core/query/options.js.map +1 -0
  83. package/dist/src/core/records.d.ts +46 -0
  84. package/dist/src/core/records.js +69 -0
  85. package/dist/src/core/records.js.map +1 -0
  86. package/dist/src/core/relation-metadata.d.ts +15 -0
  87. package/dist/src/{relation-metadata.js → core/relation-metadata.js} +7 -2
  88. package/dist/src/core/relation-metadata.js.map +1 -0
  89. package/dist/src/core/storage.d.ts +2 -0
  90. package/dist/src/core/storage.js +2 -0
  91. package/dist/src/core/storage.js.map +1 -0
  92. package/dist/src/core/types.d.ts +10 -0
  93. package/dist/src/{types.js.map → core/types.js.map} +1 -1
  94. package/dist/src/core/utils.d.ts +56 -0
  95. package/dist/src/core/utils.js +102 -0
  96. package/dist/src/core/utils.js.map +1 -0
  97. package/dist/src/files/contract.d.ts +32 -30
  98. package/dist/src/files/contract.js +29 -48
  99. package/dist/src/files/contract.js.map +1 -1
  100. package/dist/src/files/disk-metadata.d.ts +9 -0
  101. package/dist/src/files/disk-metadata.js +44 -0
  102. package/dist/src/files/disk-metadata.js.map +1 -0
  103. package/dist/src/files/disk-paths.d.ts +15 -0
  104. package/dist/src/files/disk-paths.js +100 -0
  105. package/dist/src/files/disk-paths.js.map +1 -0
  106. package/dist/src/files/disk-store.d.ts +5 -1
  107. package/dist/src/files/disk-store.js +46 -173
  108. package/dist/src/files/disk-store.js.map +1 -1
  109. package/dist/src/files/http.d.ts +36 -0
  110. package/dist/src/files/http.js +53 -0
  111. package/dist/src/files/http.js.map +1 -0
  112. package/dist/src/files/index.d.ts +5 -4
  113. package/dist/src/files/index.js +7 -2
  114. package/dist/src/files/index.js.map +1 -1
  115. package/dist/src/files/memory-store.d.ts +4 -1
  116. package/dist/src/files/memory-store.js +29 -22
  117. package/dist/src/files/memory-store.js.map +1 -1
  118. package/dist/src/files/routes.d.ts +3 -0
  119. package/dist/src/files/routes.js +27 -2
  120. package/dist/src/files/routes.js.map +1 -1
  121. package/dist/src/files/streams.d.ts +5 -0
  122. package/dist/src/files/streams.js +20 -0
  123. package/dist/src/files/streams.js.map +1 -0
  124. package/dist/src/graphql/entry.d.ts +12 -0
  125. package/dist/src/graphql/entry.js +13 -0
  126. package/dist/src/graphql/entry.js.map +1 -0
  127. package/dist/src/graphql/generate.d.ts +12 -0
  128. package/dist/src/graphql/generate.js +19 -0
  129. package/dist/src/graphql/generate.js.map +1 -0
  130. package/dist/src/graphql/preflight.d.ts +6 -0
  131. package/dist/src/graphql/preflight.js +44 -0
  132. package/dist/src/graphql/preflight.js.map +1 -0
  133. package/dist/src/graphql/resolvers.d.ts +17 -0
  134. package/dist/src/graphql/resolvers.js +44 -0
  135. package/dist/src/graphql/resolvers.js.map +1 -0
  136. package/dist/src/graphql/routes.d.ts +8 -0
  137. package/dist/src/graphql/routes.js +30 -0
  138. package/dist/src/graphql/routes.js.map +1 -0
  139. package/dist/src/graphql/schema.d.ts +6 -0
  140. package/dist/src/{graphql.js → graphql/schema.js} +59 -49
  141. package/dist/src/graphql/schema.js.map +1 -0
  142. package/dist/src/graphql/write.d.ts +4 -0
  143. package/dist/src/graphql/write.js +10 -0
  144. package/dist/src/graphql/write.js.map +1 -0
  145. package/dist/src/openapi/auth.d.ts +13 -0
  146. package/dist/src/openapi/auth.js +139 -0
  147. package/dist/src/openapi/auth.js.map +1 -0
  148. package/dist/src/openapi/document.d.ts +7 -3
  149. package/dist/src/openapi/document.js +154 -103
  150. package/dist/src/openapi/document.js.map +1 -1
  151. package/dist/src/openapi/entry.d.ts +14 -0
  152. package/dist/src/openapi/entry.js +13 -0
  153. package/dist/src/openapi/entry.js.map +1 -0
  154. package/dist/src/openapi/files.d.ts +6 -0
  155. package/dist/src/{files/openapi.js → openapi/files.js} +6 -5
  156. package/dist/src/openapi/files.js.map +1 -0
  157. package/dist/src/openapi/generate.d.ts +19 -0
  158. package/dist/src/openapi/generate.js +41 -0
  159. package/dist/src/openapi/generate.js.map +1 -0
  160. package/dist/src/openapi/helpers.d.ts +26 -0
  161. package/dist/src/openapi/helpers.js +13 -0
  162. package/dist/src/openapi/helpers.js.map +1 -0
  163. package/dist/src/openapi/options.d.ts +18 -0
  164. package/dist/src/openapi/options.js +13 -0
  165. package/dist/src/openapi/options.js.map +1 -0
  166. package/dist/src/openapi/registry.d.ts +11 -0
  167. package/dist/src/openapi/registry.js +20 -0
  168. package/dist/src/openapi/registry.js.map +1 -0
  169. package/dist/src/{types.d.ts → openapi/types.d.ts} +2 -10
  170. package/dist/src/openapi/types.js +2 -0
  171. package/dist/src/openapi/types.js.map +1 -0
  172. package/dist/src/openapi/write.d.ts +5 -0
  173. package/dist/src/openapi/write.js +12 -0
  174. package/dist/src/openapi/write.js.map +1 -0
  175. package/dist/src/rest/options.d.ts +22 -0
  176. package/dist/src/rest/options.js +65 -0
  177. package/dist/src/rest/options.js.map +1 -0
  178. package/dist/src/rest/projection.d.ts +13 -0
  179. package/dist/src/rest/projection.js +61 -0
  180. package/dist/src/rest/projection.js.map +1 -0
  181. package/dist/src/rest/routes.d.ts +7 -0
  182. package/dist/src/rest/routes.js +58 -0
  183. package/dist/src/rest/routes.js.map +1 -0
  184. package/dist/src/server/bootstrap.d.ts +7 -0
  185. package/dist/src/server/bootstrap.js +48 -0
  186. package/dist/src/server/bootstrap.js.map +1 -0
  187. package/dist/src/server/config.d.ts +74 -0
  188. package/dist/src/server/config.js +142 -0
  189. package/dist/src/server/config.js.map +1 -0
  190. package/dist/src/server/create.d.ts +16 -0
  191. package/dist/src/server/create.js +59 -0
  192. package/dist/src/server/create.js.map +1 -0
  193. package/dist/src/server/features.d.ts +4 -0
  194. package/dist/src/server/features.js +11 -0
  195. package/dist/src/server/features.js.map +1 -0
  196. package/dist/src/server/http.d.ts +12 -0
  197. package/dist/src/server/http.js +40 -0
  198. package/dist/src/server/http.js.map +1 -0
  199. package/dist/src/server/model.d.ts +6 -0
  200. package/dist/src/server/model.js +14 -0
  201. package/dist/src/server/model.js.map +1 -0
  202. package/dist/src/server/openapi-options.d.ts +6 -0
  203. package/dist/src/server/openapi-options.js +15 -0
  204. package/dist/src/server/openapi-options.js.map +1 -0
  205. package/dist/src/server/public.d.ts +2 -0
  206. package/dist/src/server/public.js +2 -0
  207. package/dist/src/server/public.js.map +1 -0
  208. package/package.json +19 -3
  209. package/dist/src/cli.d.ts +0 -4
  210. package/dist/src/cli.js +0 -67
  211. package/dist/src/cli.js.map +0 -1
  212. package/dist/src/config.d.ts +0 -68
  213. package/dist/src/config.js +0 -161
  214. package/dist/src/config.js.map +0 -1
  215. package/dist/src/constants.js.map +0 -1
  216. package/dist/src/database.d.ts +0 -21
  217. package/dist/src/database.js.map +0 -1
  218. package/dist/src/engine.d.ts +0 -47
  219. package/dist/src/engine.js +0 -438
  220. package/dist/src/engine.js.map +0 -1
  221. package/dist/src/files/openapi.d.ts +0 -3
  222. package/dist/src/files/openapi.js.map +0 -1
  223. package/dist/src/graphql.d.ts +0 -4
  224. package/dist/src/graphql.js.map +0 -1
  225. package/dist/src/model.d.ts +0 -67
  226. package/dist/src/model.js.map +0 -1
  227. package/dist/src/openapi/index.d.ts +0 -7
  228. package/dist/src/openapi/index.js +0 -26
  229. package/dist/src/openapi/index.js.map +0 -1
  230. package/dist/src/query/filter.d.ts +0 -1
  231. package/dist/src/query/filter.js +0 -84
  232. package/dist/src/query/filter.js.map +0 -1
  233. package/dist/src/query/options.d.ts +0 -31
  234. package/dist/src/query/options.js +0 -135
  235. package/dist/src/query/options.js.map +0 -1
  236. package/dist/src/relation-metadata.d.ts +0 -10
  237. package/dist/src/relation-metadata.js.map +0 -1
  238. package/dist/src/server.d.ts +0 -14
  239. package/dist/src/server.js +0 -166
  240. package/dist/src/server.js.map +0 -1
  241. package/dist/src/utils.d.ts +0 -19
  242. package/dist/src/utils.js +0 -62
  243. package/dist/src/utils.js.map +0 -1
  244. /package/dist/src/{types.js → core/types.js} +0 -0
package/README.ru.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  [English](README.md)
4
4
 
5
- JSON-сервер для имитации API: REST, GraphQL, вложенные запросы, бинарные файлы и экспорт схем. Требуется Node.js 22 или новее.
5
+ JSON-сервер для имитации API: REST, GraphQL, связанные записи, загрузка файлов и экспорт схем. Поддерживает вход пользователей, права владельца и администратора, даты записей и мягкое удаление. Требуется Node.js 22 или новее.
6
6
 
7
- **1.0.0-alpha.1 предварительная версия с несовместимыми изменениями.** Старые `$schema`/`$info` и параметры `_where`, `_sort`, `_embed`, `_page`, `_perPage` больше не поддерживаются.
7
+ **Breaking changes: 1.0.0-alpha.10.** Изменились форматы конфигурации и схемы. Актуальные примеры в разделах «Конфигурация» и «Схема моделей».
8
8
 
9
9
  ## Установка
10
10
 
@@ -12,88 +12,138 @@ JSON-сервер для имитации API: REST, GraphQL, вложенные
12
12
  npm install @kollors/deep-json-server@alpha
13
13
  ```
14
14
 
15
- Канал npm `alpha` отделён от `latest`. Для точной версии используйте `@1.0.0-alpha.1`.
15
+ Для установки конкретной версии укажите `@1.0.0-alpha.11`.
16
16
 
17
17
  ## Быстрый старт
18
18
 
19
+ Создайте два файла в одном каталоге.
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
- Вторая команда экспортирует обе схемы без запуска сервера. Обычный запуск не записывает файлы схем.
41
+ Список пользователей доступен по адресу `http://127.0.0.1:4001/users`.
36
42
 
37
- Полные примеры каталога: [база](examples/database.json), [схема моделей](examples/schema.json), [конфигурация](examples/server.config.js).
43
+ Для связей и валидации добавьте [схему моделей](#схема-моделей). Далее описаны [запросы](#запросы-и-ответы), [аутентификация](#аутентификация), [мягкое удаление](#даты-записей-удаление-и-владельцы), [файлы](#файлы) и [программный API](#программный-api).
38
44
 
39
45
  ## Конфигурация
40
46
 
41
- | Ключ | Назначение |
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
+ | Настройка | Назначение |
42
62
  |---|---|
43
- | `database.path` / `database.data` | Ровно один: JSON-файл или объект коллекций в памяти |
44
- | `database.schema` | Объект моделей или путь к JSON-файлу; необязателен для REST |
45
- | `openapi.path` | Путь экспорта YAML |
46
- | `openapi.info` | Необязательные метаданные: `title`, `version`, `description` |
47
- | `graphql.enabled` | Включить GraphQL HTTP API; по умолчанию `false` |
48
- | `graphql.endpoint` | Путь GraphQL; по умолчанию `/graphql` |
49
- | `graphql.path` | Путь экспорта GraphQL SDL |
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` |
50
75
  | `server.host`, `server.port` | По умолчанию `127.0.0.1`, `4001`; CLI также читает `HOST`/`PORT` |
51
76
  | `server.pageSize`, `server.maxPageSize` | По умолчанию 10 и 100; размер по умолчанию ограничен максимумом |
52
- | `server.cors`, `server.logger` | По умолчанию `true`; logger также принимает настройки Fastify |
77
+ | `server.cors`, `server.logger` | По умолчанию `true`; logger принимает также настройки Fastify |
53
78
  | `server.maxFileSize` | По умолчанию 100 МиБ |
54
- | `files.data` | Бинарные файлы в памяти |
55
- | `files.directory`, `files.metadata` | Каталог файлов и JSON метаданных; необходимы оба |
56
79
 
57
- Пути конфигурационного файла разрешаются относительно него. При прямом `createServer()` — относительно рабочего каталога. Переданные данные в памяти копируются.
80
+ Относительные пути разрешаются от каталога файла конфигурации; при вызове `createServer(config)` — от рабочего каталога. Данные в памяти, включая схему, копируются. Порт `0` позволяет системе выбрать свободный порт.
81
+
82
+ ### CLI
58
83
 
59
- | Флаг CLI | Действие |
84
+ | Флаг | Действие |
60
85
  |---|---|
61
- | `--files` | Включить файловые маршруты |
62
- | `--graphql` | Включить GraphQL API |
63
- | `--openapi` | Экспортировать OpenAPI и запустить сервер |
64
- | `--openapi-only` | Только экспортировать OpenAPI |
65
- | `--graphql-schema` | Экспортировать GraphQL SDL и запустить сервер |
66
- | `--graphql-only` | Только экспортировать GraphQL SDL |
67
- | `--help` | Показать справку |
86
+ | `--generate` | Экспортировать схемы и запустить сервер |
87
+ | `--generate-only` | Экспортировать схемы и завершить работу |
88
+ | `--host <host>` | Адрес сервера |
89
+ | `--port <port>` | Порт сервера |
90
+ | `--help, -h` | Справка |
91
+ | `--version, -v` | Версия пакета |
68
92
 
69
- Экспортеры можно объединять. Любой флаг `--*-only` исключает запуск сервера. В CLI для файлов нужен `--files`; программный API включает их при наличии секции `files`, если второй аргумент `createServer()` не переопределяет это поведение.
93
+ Приоритет адреса и порта: CLI конфигурация `HOST`/`PORT` значения по умолчанию. Без флагов генерации запускается только сервер. `--generate` и `--generate-only` нельзя передавать вместе.
70
94
 
71
95
  ## Схема моделей
72
96
 
97
+ Примеры: [база данных](examples/database.json), [схема моделей](examples/schema.json), [конфигурация](examples/server.config.js).
98
+
73
99
  ```json
74
100
  {
75
- "Country": {
76
- "collection": "countries",
77
- "api": ["openapi", "graphql"],
78
- "fields": {
79
- "id": { "type": "string", "primary": true, "generated": "uuid" },
80
- "name": { "type": "string", "required": true },
81
- "users": { "type": "User[]", "target": "countryId" }
82
- }
83
- },
84
- "User": {
85
- "collection": "users",
86
- "api": ["openapi", "graphql"],
87
- "fields": {
88
- "id": { "type": "string", "primary": true, "generated": "uuid" },
89
- "fullName": { "type": "string", "required": true },
90
- "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
+ }
91
141
  }
92
142
  }
93
143
  }
94
144
  ```
95
145
 
96
- По умолчанию `api` содержит `["openapi", "graphql"]`. Пустой массив исключает модель из экспорта и GraphQL, но REST продолжает работать. Связанные модели должны разрешать тот же формат экспорта. Имена должны быть допустимыми идентификаторами; конфликты генерируемых типов и операций вызывают ошибку. Имена `and`, `or`, `not` зарезервированы фильтрами.
146
+ Модели находятся в `models`. В корне схемы можно задать `api`, `timestamps` и `softDelete`; у модели эти параметры переопределяют общие значения. Для `api` приоритет такой: модель → корень схемы → секции конфигурации. Массив заменяется целиком; `[]` исключает модель из GraphQL и OpenAPI, но REST продолжает работать. Явный список не включает отсутствующий модуль. Связанные модели должны разрешать тот же формат. Имена моделей должны быть допустимыми идентификаторами; конфликты типов и операций вызывают ошибку. Имена `and`, `or`, `not` зарезервированы фильтрами.
97
147
 
98
148
  | Возможность | Со схемой | Без схемы |
99
149
  |---|---|---|
@@ -102,11 +152,15 @@ npx deep-json-server --openapi-only --graphql-only server.config.js
102
152
  | Экспорт OpenAPI 3.0.3 | Доступен | Ошибка при запросе экспорта |
103
153
  | GraphQL SDL / API | Доступен | Ошибка при запросе |
104
154
 
105
- При явной схеме неизвестные поля и коллекции запрещены. Исключение — хранимые ключи, выведенные из связей. Исходная база проверяется при загрузке. Схемы можно экспортировать для пустых коллекций и пустого объекта базы. REST без схемы сохраняет обычный генерируемый ключ `id`.
155
+ При явной схеме неизвестные поля и коллекции запрещены. Исключение — хранимые ключи, выведенные из связей. Исходная база проверяется при загрузке. Генерация использует описание моделей.
156
+
157
+ REST без схемы создаёт ключ `id` и сохраняет произвольные JSON-поля. Для фильтров и выбора отдельных полей используются имена в формате идентификаторов; остальные поля возвращаются через `scope=[{"*":true}]`. Поля с разными типами значений можно читать, но фильтрация, сортировка и пагинация неоднородных списков требуют явной схемы.
158
+
159
+ У модели обязательны `collection` — имя коллекции в базе и REST-пути — и `fields` — описание полей. Имя модели (`User`) задаёт имена типов и операций GraphQL. `api` управляет доступностью форматов, а `timestamps` и `softDelete` переопределяют глобальные настройки для этой модели.
106
160
 
107
161
  ### Поля
108
162
 
109
- Типы: `string`, `number`, `boolean`, `object` или имя модели. Суффикс `[]` обозначает массив. Нет `integer`, `relation`, `items` и многомерных строк типов. Вложенные поля описываются через точку: `actors.fullName`. Объект внутри массива может содержать собственные массивы.
163
+ Поле `type` принимает `string`, `number`, `boolean`, `object` или имя модели. Для массива добавьте суффикс `[]`: `string[]`, `object[]`, `Genre[]`. Вложенные поля описываются через точку, например `actors.fullName`.
110
164
 
111
165
  | Свойства | Назначение |
112
166
  |---|---|
@@ -114,7 +168,7 @@ npx deep-json-server --openapi-only --graphql-only server.config.js
114
168
  | `description`, `example` | Документация и пример |
115
169
  | `required`, `nullable` | По умолчанию `false`; наличие поля и разрешение `null` независимы |
116
170
  | `default` | Значение при пропуске в create/replace; PATCH не вставляет значения по умолчанию |
117
- | `enum` | Допустимые значения; у массива — значения каждого элемента |
171
+ | `enum` | Допустимые строки, числа или логические значения; у массива — значения каждого элемента |
118
172
  | `primary` | Корневой первичный ключ: обязательный, уникальный, неизменяемый, без `null` |
119
173
  | `generated` | `uuid` для строк, `increment` для чисел; значение создаёт сервер |
120
174
  | `readOnly`, `writeOnly` | Только ответ или только входные данные; взаимно исключаются |
@@ -123,137 +177,40 @@ npx deep-json-server --openapi-only --graphql-only server.config.js
123
177
  | `minimum`, `maximum` | Включительные числовые границы |
124
178
  | `source`, `target`, `onDelete` | Описание связи |
125
179
 
126
- Ограничения строк и чисел применяются к каждому элементу `string[]`/`number[]`. `required`/`nullable` относятся ко всему массиву; `default`/`example` содержат массив целиком. Элементы массива не допускают `null`. Ограничений количества и уникальности элементов нет. Обязательный обычный массив может быть пустым.
180
+ Ограничения строк и чисел применяются к каждому элементу `string[]`/`number[]`. `required` и `nullable` относятся ко всему массиву; `default` и `example` содержат массив целиком. Элементы массива должны соответствовать его типу и быть отличны от `null`. `required` требует наличия поля; обычный массив при этом может быть пустым.
127
181
 
128
- У модели ровно один корневой первичный ключ типа string/number. Имя произвольное: `id`, `username`, `code`. Без `generated` значение передаёт клиент при создании. Генерируемые поля могут быть только корневыми, исключаются из входных типов и несовместимы с `default`. Replace сохраняет корневые генерируемые поля и `readOnly`.
182
+ У модели ровно один первичный ключ типа `string` или `number`, объявленный на верхнем уровне. Имя произвольное: `id`, `username`, `code`. Если `generated` не задан, значение передаёт клиент при создании. Генерируемые поля объявляются на верхнем уровне, исключаются из входных типов и несовместимы с `default`. При замене записи сохраняются генерируемые значения и поля `readOnly`, включая вложенные объекты. Для защиты полей внутри массива задайте `readOnly` всему массиву или содержащему его объекту. Объекты, состоящие из серверных полей, доступны только в ответах.
129
183
 
130
- Например, `LocalUser` с первичным `username` и полем `password: {"type":"string","required":true,"writeOnly":true}` получает запрос `localUser(username: ...)` и маршрут `/localUsers/{username}`. Пароль исключён из ответов, scope, фильтров и сортировки. Это не реализует хеширование и авторизацию.
184
+ Например, `LocalUser` с первичным ключом `username` и полем `password: {"type":"string","required":true,"writeOnly":true}` получает запрос `localUser(username: ...)` и маршрут `/localUsers/{username}`. Поле `writeOnly` доступно для записи и исключено из ответов, `scope`, фильтров и сортировки.
185
+
186
+ Объекты в GraphQL должны содержать хотя бы одно поле, доступное в ответе; REST допускает и пустые объекты.
131
187
 
132
188
  ### Связи
133
189
 
134
190
  ```json
135
- "actors.genres": {
136
- "type": "Genre[]",
137
- "source": "actors.genreIds",
138
- "required": true
191
+ {
192
+ "actors.genres": {
193
+ "type": "Genre[]",
194
+ "source": "actors.genreIds",
195
+ "required": true
196
+ }
139
197
  }
140
198
  ```
141
199
 
142
- `Genre` возвращает объект, `Genre[]` — список. Без `source` используем первичный ключ текущей модели, без `target` — первичный ключ целевой. Пути полные, от корня соответствующей записи. Внутри `actors` путь `actors.genreIds` читает ключи текущего актёра. Пропущенный `source` означает ключ корневой модели, а не `actors.id`.
200
+ `Genre` возвращает объект, `Genre[]` — список. По умолчанию `source` указывает на первичный ключ текущей модели, `target` — целевой. Это правило действует и для вложенных связей. Пути задаются от корня соответствующей записи: в примере `actors.genreIds` содержит ключи жанров текущего актёра.
143
201
 
144
- Хранимые ключи остаются в базе и входят в собственные поля. Их типы выводятся из сопоставляемых ключей. Для необъявленного source, ссылающегося на первичный ключ цели, множественная связь означает массив ключей, одиночная одно значение. Если сопоставление неоднозначно, нужное хранимое поле следует описать явно. Генерация не зависит от первой записи базы.
202
+ Ключи связей хранятся в базе и входят в собственные поля записи. Их типы выводятся из сопоставляемых ключей. Если поле `source` ссылается на первичный ключ целевой модели, его можно не объявлять отдельно: для множественной связи создаётся описание массива ключей, для одиночной одного значения. При неоднозначном сопоставлении опишите хранимое поле явно.
145
203
 
146
204
  Обратная связь: `User.movies = {"type":"Movie[]","target":"actors.userId"}`. Фильм возвращается один раз, даже если совпало несколько актёров. Несколько совпадений для одиночной связи — ошибка.
147
205
 
148
- Каждая переданная прямая ссылка должна существовать. `required: true` у связи требует хотя бы одну связанную запись до фильтрации и пагинации ответа. Обратная связь с первичным source может быть пустой, если не объявлена обязательной. Отсутствующая одиночная связь возвращает `null`.
206
+ Каждый переданный ключ прямой связи должен указывать на существующую запись. `required: true` у связи требует хотя бы одну связанную запись до фильтрации и пагинации ответа. Обратная связь с первичным ключом в `source` может быть пустой, если не объявлена обязательной. Отсутствующая одиночная связь возвращает `null`.
149
207
 
150
208
  `onDelete` срабатывает **при удалении целевой записи**:
151
209
 
152
210
  - `restrict` — по умолчанию: запретить удаление, пока на цель ссылается сохраняемая запись.
153
- - `cascade` — удалить ссылающуюся запись. Для `User.country` удаление страны удаляет пользователей. Для `Movie.actors.user` удаление пользователя удаляет соответствующие элементы actors, сохраняя фильм.
154
-
155
- Сначала вычисляется весь каскад, обрабатываются циклы и проверяются ограничения, затем сохраняется результат. Ошибка отменяет всю операцию. Политики действуют и на явно объявленные обратные связи; при описании обоих направлений учитывайте оба правила.
156
-
157
- ## Запросы и ответы
158
-
159
- Коллекции и списки объектов, включая вложенные `object[]`, возвращают:
160
-
161
- ```json
162
- { "data": [], "total": 0 }
163
- ```
164
-
165
- Массивы примитивов остаются обычными массивами. У каждого списка объектов есть необязательные `where`, `order`, `pager`. Порядок обработки: фильтрация → сортировка → пагинация. `total` считается после фильтрации, до пагинации. Страницы начинаются с 1; отсутствие pager включает пагинацию по умолчанию. Превышение максимума, дробные и неположительные значения — ошибка. За пределами списка возвращается пустой data с правильным total.
166
-
167
- Фильтры: `eq`, `ne`, `in`; для строк `contains`, `startsWith`, `endsWith`; сравнения `gt`, `gte`, `lt`, `lte`. Логика — `and`, `or`, `not`. У массивов есть `some`, `every`, `none`; у примитивных массивов также `contains`, `in`. Поиск строк не учитывает регистр. Условия полей записываются объектами операторов, без сокращения до скалярного значения.
168
-
169
- ```json
170
- {
171
- "movies": {
172
- "some": {
173
- "actors": {
174
- "some": {
175
- "genres": { "some": { "id": { "in": ["2", "3"] } } }
176
- }
177
- }
178
- }
179
- }
180
- }
181
- ```
182
-
183
- Корневой where выбирает родителей. Where внутри связи фильтрует её элементы, сохраняя родителя. Каждый вложенный список обрабатывается отдельно. Для фильтра по связи не нужно раскрывать её в ответе.
184
-
185
- `order` — массив правил `{ "field": "fullName", "direction": "ASC" }`. Первое правило приоритетнее; полные совпадения сохраняют порядок хранения. `null` и отсутствующее значение сравниваются одинаково. REST использует путь `profile.name`, GraphQL — enum `profile_name`. Неоднозначные имена enum вызывают ошибку генерации. Сортировка родителей по связям и массивам пока не поддерживается; внутри связи сортировка доступна.
186
-
187
- ### REST
188
-
189
- | Метод | Путь | Операция |
190
- |---|---|---|
191
- | GET | `/users` | `userList` |
192
- | GET | `/users/{id}` | `user` |
193
- | POST | `/users` | `userCreate` |
194
- | PUT | `/users/{id}` | `userReplace` |
195
- | PATCH | `/users/{id}` | `userUpdate` |
196
- | DELETE | `/users/{id}` | `userDelete` |
197
-
198
- Имя параметра пути соответствует первичному ключу. POST/PUT/PATCH принимают обычный объект записи. PUT заменяет запись с сохранением ключа и корневых серверных полей. PATCH поверхностно объединяет поля; переданные вложенные объекты заменяются целиком. Create/replace проверяют обязательность. Update проверяет переданные значения и итоговую запись. Отсутствующая запись — 404, конфликт — 409. DELETE возвращает удалённую запись.
199
-
200
- Параметры `where`, `order`, `pager`, `nested` содержат JSON; `scope` — строку выбора. Пример до URL-кодирования:
201
-
202
- ```text
203
- GET /users?where={"fullName":{"contains":"Мира"}}&order=[{"field":"fullName","direction":"ASC"}]&pager={"page":1,"pageSize":20}
204
- ```
205
-
206
- Кодирование через `URLSearchParams`:
207
-
208
- ```js
209
- const params = new URLSearchParams({
210
- scope: 'id,fullName,movies(id,title)',
211
- nested: JSON.stringify({ movies: { order: [{ field: 'title', direction: 'ASC' }], pager: { page: 1, pageSize: 5 } } }),
212
- });
213
- const response = await fetch(`/users?${params}`);
214
- ```
215
-
216
- `scope=*,actors(user(id,fullName),genres(*))` выбирает собственные поля и явные связи. `*` включает собственные поля текущего объекта и хранимые ключи, исключает writeOnly и не раскрывает связи. Без scope выбираются собственные поля. Оболочки data/total сохраняются.
217
-
218
- `nested` сопоставляет полные пути ответа и настройки списков:
219
-
220
- ```json
221
- {
222
- "actors": { "pager": { "pageSize": 5 } },
223
- "actors.genres": {
224
- "where": { "id": { "in": ["2", "3"] } },
225
- "order": [{ "field": "name", "direction": "ASC" }]
226
- }
227
- }
228
- ```
229
-
230
- Путь nested должен быть выбран через scope и вести к списку объектов. Одиночные маршруты и мутации принимают scope/nested; корневые where/order/pager применяются только к GET коллекции. Неизвестные имена и небезопасные пути возвращают 400.
231
-
232
- ### GraphQL
233
-
234
- ```graphql
235
- query {
236
- userList(
237
- order: [{ field: fullName, direction: ASC }]
238
- pager: { page: 1, pageSize: 20 }
239
- ) {
240
- total
241
- data {
242
- id
243
- fullName
244
- movies(order: [{ field: title, direction: ASC }], pager: { pageSize: 5 }) {
245
- total
246
- data { id title }
247
- }
248
- }
249
- }
250
- }
251
- ```
252
-
253
- Одиночный запрос — `user(id: ...)`, без ById; отсутствующая запись даёт null. Мутации: `userCreate(data: ...)`, `userReplace(id: ..., data: ...)`, `userUpdate(id: ..., data: ...)`, `userDelete(id: ...)`. Если модель содержит только генерируемые поля, создание не имеет аргумента data. Используются те же операции хранения и проверки, что в REST. Выбор связей в результате мутации управляет только ответом.
254
-
255
- Строковый первичный ключ представлен GraphQL ID, обычные строки — String, числа — Float, пагинация — Int. Enum сохраняет допустимые строковые имена; остальные значения получают имена VALUE_0, VALUE_1 и т. д. Длины строк и форматы проверяет общий валидатор во время выполнения: SDL не выражает все ограничения. Доступны интроспекция, обычные query и mutation; подписки и массовые мутации не добавлены.
211
+ - `cascade` — удалить ссылающуюся запись. Для `User.country` удаление страны удаляет пользователей. Для `Movie.actors.user` удаление пользователя удаляет соответствующие элементы `actors`, сохраняя фильм.
256
212
 
213
+ Каскадное удаление выполняется целиком, включая циклические связи. Ошибка проверки отменяет всю операцию. Правила `onDelete` действуют и на явно объявленные обратные связи; при описании обоих направлений учитывайте оба правила.
257
214
 
258
215
  ## Пример базы данных
259
216
 
@@ -359,15 +316,381 @@ query {
359
316
  }
360
317
  ```
361
318
 
319
+ ## Запросы и ответы
320
+
321
+ Примеры с фильмами, актёрами и жанрами используют полную [схему из examples](examples/schema.json). Для их запуска используйте [конфигурацию примера](examples/server.config.js):
322
+
323
+ ```sh
324
+ npx deep-json-server examples/server.config.js
325
+ ```
326
+
327
+ Коллекции и списки объектов, включая вложенные `object[]`, возвращают:
328
+
329
+ ```json
330
+ { "data": [], "total": 0 }
331
+ ```
332
+
333
+ Массивы примитивов возвращаются обычными массивами. Каждый список объектов принимает необязательные `where`, `order` и `pager`. Порядок обработки: фильтрация → сортировка → пагинация. `total` — число записей после фильтрации, до пагинации.
334
+
335
+ В `pager` задаются `page` и `pageSize`. По умолчанию возвращается первая страница с размером из `server.pageSize`. Оба значения должны быть положительными целыми числами; `pageSize` ограничен `server.maxPageSize`. За пределами списка возвращается пустой `data` с общим числом найденных записей в `total`.
336
+
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" } }`.
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
+ Корневой `where` выбирает записи основной коллекции. `where` внутри связи фильтрует её элементы, сохраняя родительскую запись. Каждый вложенный список обрабатывается отдельно. Фильтрация по связи работает независимо от её включения в ответ.
354
+
355
+ `order` — массив правил `{ "field": "fullName", "direction": "ASC" }`. `ASC` сортирует по возрастанию, `DESC` — по убыванию. Первое правило приоритетнее; при равных значениях сохраняется порядок хранения. `null` и отсутствующее значение сравниваются одинаково. REST использует путь `profile.name`, GraphQL — enum `profile_name`. Неоднозначные имена enum вызывают ошибку генерации. Сортировка доступна по скалярным полям текущего объекта, включая вложенные поля. Для связанных списков задаётся собственный `order`.
356
+
357
+ ### REST
358
+
359
+ `GET /` возвращает имена коллекций: `{ "resources": ["users", "movies"] }`. Для каждой коллекции доступны следующие маршруты:
360
+
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` |
369
+
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}`);
398
+ ```
399
+
400
+ Обычные поля выбираются через `true`, объекты и связи — через свой массив `scope`. Если аргументы не нужны, в массиве остаётся только объект полей. `"*": true` включает собственные поля и хранимые ключи, кроме `writeOnly`; связи выбираются явно.
401
+
402
+ Например, собственные поля фильма, пользователи актёров и отсортированные жанры:
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
+ Без `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
432
+
433
+ {
434
+ "actors": [
435
+ {
436
+ "userId": "1",
437
+ "genres": [
438
+ "1",
439
+ { "id": "2", "name": "Обновлённый жанр" },
440
+ { "name": "Новый жанр" }
441
+ ]
442
+ }
443
+ ]
444
+ }
445
+ ```
446
+
447
+ | Значение в связи | Действие |
448
+ |---|---|
449
+ | Ключ, например `"1"` | Связать существующую запись без её изменения |
450
+ | Объект с первичным ключом | PATCH обновляет переданные поля, PUT заменяет связанную запись |
451
+ | Объект без первичного ключа | Создать запись со значениями по умолчанию и сгенерированным ключом |
452
+
453
+ Имя и тип ключа берутся из целевой модели. Объект только с ключом тоже считается обновлением: в PUT он должен содержать обязательные поля модели. При замене сохраняются первичный ключ, генерируемые значения и поля `readOnly`. В POST вложенные объекты с существующими ключами обновляются частично. Если запись по ключу не найдена, операция завершится ошибкой. Для создания вложенной записи без ключа нужна его автоматическая генерация.
454
+
455
+ Переданный список заменяет состав связи. PATCH сохраняет пропущенные связи, PUT очищает пропущенные связи, ключи которых доступны для записи. `[]` очищает список, `null` — одиночную связь с разрешённым `nullable`. Разрыв связи не удаляет связанную запись. Обязательные связи должны оставаться заполненными.
456
+
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
+ }
474
+ ```
475
+
476
+ ### GraphQL
477
+
478
+ Укажите `database.schema` и добавьте секцию `graphql: {}` в конфигурацию:
479
+
480
+ ```sh
481
+ npx deep-json-server server.config.js
482
+ ```
483
+
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
+ ```
509
+
510
+ Запрос `user(id: ...)` возвращает одну запись или `null`, если она отсутствует. Мутации: `userCreate(data: ...)`, `userReplace(id: ..., data: ...)`, `userUpdate(id: ..., data: ...)`, `userDelete(id: ...)`. Для модели, состоящей из генерируемых полей, мутация создания вызывается без аргумента `data`. Правила записи и валидации совпадают с REST. Связи, выбранные в результате мутации, определяют содержимое ответа.
511
+
512
+ Строковый первичный ключ представлен типом GraphQL `ID`, обычные строки — `String`, числа — `Float`, параметры пагинации — `Int`. Enum сохраняет допустимые строковые имена; остальные значения получают имена `VALUE_0`, `VALUE_1` и т. д. Длины строк, форматы и другие ограничения модели проверяются сервером при выполнении запроса. Для просмотра схемы доступна интроспекция. Параметры выбранных списков проверяются до выполнения мутаций.
513
+
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
+ };
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` экспортирует и завершает работу, `--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
+ );
561
+ ```
562
+
563
+ Задайте `DJS_PASSWORD` и выполните `node setup-auth.mjs`. Добавьте файл в конфигурацию сервера:
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
+ Запустите `npx deep-json-server server.config.js`. Каждой исходной учётной записи нужны уникальный строковый `id`, уникальный `username` и `passwordHash`, созданный функцией выше. `isAdmin` по умолчанию равен `false`. Пароли хешируются через scrypt со случайной солью.
574
+
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
+ }
641
+ ```
642
+
643
+ Приоритет: модель → корень схемы → `false`. Явное `false` отключает унаследованную настройку. Без схемы даты и мягкое удаление выключены.
644
+
645
+ | Поле | Когда включено | Значение |
646
+ |---|---|---|
647
+ | `createdAt`, `updatedAt` | `timestamps` | Даты создания и последнего изменения |
648
+ | `deletedAt` | `softDelete` | Дата удаления или `null` для активной записи |
649
+ | `createdById`, `updatedById` | `auth` | Создатель/владелец и последний редактор |
650
+ | `deletedById` | `auth` + `softDelete` | Пользователь, удаливший запись, или `null` |
651
+
652
+ Даты — строки ISO 8601 в UTC. При создании даты создания и изменения совпадают, а при включённом auth создателем и редактором становится текущий пользователь. PUT/PATCH сохраняют создателя и дату создания. DELETE обновляет поля удаления и включённые поля последнего изменения. Для старых записей с неизвестными датами или авторами возвращается `null`. Эти поля доступны только для чтения в REST и GraphQL; обычные вложенные объекты собственных полей авторства и дат не получают. При отключении функции уже сохранённые значения её полей сохраняются.
653
+
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
+ ```
664
+
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 и все операции с файлами остаются открытыми.
674
+
362
675
  ## Файлы
363
676
 
364
- Добавьте `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
+ Запустите сервер:
365
688
 
366
689
  ```bash
367
- deep-json-server --files server.config.js
690
+ npx deep-json-server server.config.js
368
691
  ```
369
692
 
370
- Для временных тестов вместо этого используйте `files.data`. Каждая начальная запись содержит `name`, `mimeType`, бинарное `content` в виде `Uint8Array` и необязательный `directory`. Загруженные файлы в таком режиме остаются в памяти до завершения процесса.
693
+ Для временных тестов выберите `storage: 'memory'` и передайте массив в `files.source`. Каждая начальная запись содержит `name`, `mimeType`, бинарное `content` в виде `Uint8Array` и необязательный `directory`. Загруженные файлы в таком режиме остаются в памяти до завершения процесса.
371
694
 
372
695
  Один файл отправляется непосредственно в теле запроса. `Content-Name` содержит URI-кодированное имя файла, `Content-Type` — его MIME-тип, а необязательный `Content-Directory` — URI-кодированный относительный путь к директории:
373
696
 
@@ -418,39 +741,61 @@ Content-Type: application/json
418
741
  }
419
742
  ```
420
743
 
421
- `PATCH` возвращает обновлённые метаданные со статусом `200`; если по новому пути уже существует файл, сервер возвращает `409`. `DELETE` отвечает статусом `204` без тела. Если файл не найден, любая операция по пути возвращает `404`. Пути в URL задаются относительно `files.directory`, а все возвращаемые URL — относительно адреса сервера.
744
+ `PATCH` возвращает обновлённые метаданные со статусом `200`; если по новому пути уже существует файл, сервер возвращает `409`. `DELETE` отвечает статусом `204` без тела. Если файл не найден, любая операция по пути возвращает `404`. Пути в URL задаются относительно `files.source`, а все возвращаемые URL — относительно адреса сервера.
745
+
746
+ При хранении на диске бинарный файл находится по пути `<files.source>/<directory>/<name>`. Метаданные по умолчанию хранятся в `<files.source>/.files.json`; другой путь можно задать через `files.metadata`. Директории и файл метаданных создаются по мере необходимости.
422
747
 
423
- При хранении на диске бинарный файл находится по пути `<files.directory>/<directory>/<name>`. Файл метаданных содержит только `directory`, `mimeType` и `name`; `size` считывается у фактического файла, а URL вычисляются. Используйте дисковую базу и файловое хранилище только из одного процесса сервера одновременно и не редактируйте сохранённые файлы или метаданные до его остановки. Сервер автоматически создаёт директории, пути внутри `files.directory` не могут содержать символические ссылки, а имена файлов ограничены переносимыми между поддерживаемыми операционными системами значениями. Изначально файл метаданных может отсутствовать: он создаётся при первой загрузке. Метаданные, созданные версиями до перехода на адресацию по пути, несовместимы с новым форматом.
748
+ Используйте один процесс сервера для дисковой базы и файлового хранилища. Перед ручным изменением файлов или метаданных остановите его. Пути в хранилище не могут содержать символические ссылки. Загрузка и переименование не могут перезаписать базу, счётчики, схему, учётные записи auth, загруженную конфигурацию или файл метаданных.
424
749
 
425
- Используется бинарное тело запроса, а не `multipart/form-data`, поэтому `XMLHttpRequest.upload.onprogress` может показывать прогресс при непосредственной отправке `File` через `xhr.send(file)`. Максимальный размер по умолчанию равен 100 МиБ и настраивается через `server.maxFileSize`. Отсутствующие или небезопасные заголовки и пути возвращают `400`, превышение лимита — `413`, а отсутствующий, некорректный или не поддерживаемый Fastify `Content-Type` — `400` либо `415` в зависимости от этапа проверки.
750
+ Отправляйте файл как бинарное тело запроса. В браузере для этого можно использовать `xhr.send(file)`, а прогресс отслеживать через `XMLHttpRequest.upload.onprogress`. Максимальный размер по умолчанию равен 100 МиБ и настраивается через `server.maxFileSize`. Отсутствующие или небезопасные заголовки и пути возвращают `400`, превышение лимита — `413`, а отсутствующий, некорректный или не поддерживаемый Fastify `Content-Type` — `400` либо `415` в зависимости от этапа проверки.
426
751
 
427
752
  ## Программный API
428
753
 
429
754
  ```js
430
- import { createServer } from '@kollors/deep-json-server';
755
+ import { createServer } from '@kollors/deep-json-server/server';
431
756
  import config from './server.config.js';
432
757
 
433
758
  const facade = await createServer(config);
434
- const openapi = await facade.openapi();
435
- const sdl = await facade.graphql();
436
759
  const server = facade.fastify();
437
760
  await server.listen();
438
761
  // await server.close();
439
762
  ```
440
763
 
441
- Аксессоры ленивые: экспорт не открывает порт и не инициализирует дисковое файловое хранилище. Возможности сервера можно переопределить через `createServer(config, { files: false, graphql: true })`.
764
+ Методы `openapi()` и `graphql()` возвращают схемы и требуют `database.schema`. `fastify()` возвращает экземпляр сервера для настройки и запуска. База и включённые сервисы инициализируются при `ready()`, `listen()` или первом `inject()`; ошибка инициализации останавливает запуск.
442
765
 
443
- ## Хранение и разработка
766
+ `createServer(config)` принимает один аргумент. Наличие секций управляет модулями так же, как при запуске через CLI. Для методов `openapi()` и `graphql()` нужна соответствующая секция. Методы возвращают схему и не записывают файлы.
444
767
 
445
- Изменения выполняются последовательно внутри экземпляра сервера и проверяются на копии данных до сохранения. Для одного файла базы используйте один пишущий сервер. Счётчики increment хранятся рядом с базой в `<путь базы>.counters.json`; сохраняйте этот файл вместе с базой. Номера резервируются до записи данных: сбой может оставить пропуск, но не приводит к повторной выдаче номера. UUID создаётся встроенным crypto Node.js.
768
+ Эти функции доступны и через общий импорт `@kollors/deep-json-server`. Адаптеры сервера загружаются при включении. Генераторы можно использовать отдельно:
446
769
 
447
- Это сервер для имитации API: авторизации и хеширования паролей нет. Файловые маршруты сохраняют отдельное хранилище и собственную валидацию.
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
+ Отдельные генераторы принимают путь или объект схемы и не требуют конфигурации сервера. Выбранная функция задаёт формат по умолчанию; `api` в схеме и моделях может его ограничить. `timestamps` и `softDelete` берутся из схемы. Опция `{ auth: true }` добавляет поля владельца; OpenAPI также описывает маршруты auth и требования токена. `hashPassword()` доступна и через общий импорт пакета.
781
+
782
+ `generateOpenapi()` также принимает `host`, `port`, `pageSize`, `maxPageSize` и `info`. Вместо пути можно передать объект схемы. Сервер и генераторы работают с собственной копией модели. Размеры страниц должны быть положительными целыми числами; `pageSize` не может превышать `maxPageSize`.
783
+
784
+ ## Хранение данных
785
+
786
+ Изменения выполняются последовательно внутри экземпляра сервера и проверяются на копии данных до сохранения. Для одного файла базы используйте один процесс сервера. Счётчики `increment` хранятся рядом с базой в `<путь базы>.counters.json`; сохраняйте этот файл вместе с базой. Номера резервируются до записи данных: сбой может оставить пропуск, но не приводит к повторной выдаче номера.
787
+
788
+ ## Разработка
789
+
790
+ Исходники разделены на `rest`, `graphql`, `openapi`, `auth`, `files`, `cli` и `server`. Общая модель, хранение, запросы и правила изменения записей находятся в `core`. Модуль `server` подключает остальные; генераторы схем загружаются независимо от HTTP-сервера.
448
791
 
449
792
  ```sh
450
793
  npm ci
451
794
  npm run verify
452
795
  ```
453
796
 
454
- Проверка включает типы, линтер, пороги покрытия и установку упакованного пакета. Новая alpha-версия package.json в main создаёт свой тег и публикуется через GitHub Actions trusted publishing в канал npm alpha. Если тег уже существует, автоматическая публикация пропускается. Явная отправка тега версии также запускает публикацию; стабильные версии используют latest. Скрипт проверяет совпадение Git-тега и версии пакета.
797
+ Команда проверяет типы, стиль кода, покрытие тестами и установку пакета из архива.
798
+
799
+ Для публикации новой альфы обновите версию в `package.json`, `package-lock.json` и `src/core/constants.ts`, затем отправьте изменения в `main`. GitHub Actions создаст тег версии и опубликует пакет в канал npm `alpha` через trusted publishing. Уже опубликованная версия пропускается. Если тег создан, а публикация не завершилась, повторный запуск использует этот тег и проверяет соответствие ему файлов пакета. Отправка тега версии также запускает публикацию; стабильные версии публикуются в `latest`.
455
800
 
456
801
  Лицензия: MIT.