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

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 (127) hide show
  1. package/README.md +168 -109
  2. package/README.ru.md +171 -112
  3. package/dist/index.d.ts +5 -6
  4. package/dist/index.js +2 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/src/auth/contract.d.ts +6 -2
  7. package/dist/src/auth/contract.js +8 -1
  8. package/dist/src/auth/contract.js.map +1 -1
  9. package/dist/src/auth/input.d.ts +18 -0
  10. package/dist/src/auth/input.js +34 -0
  11. package/dist/src/auth/input.js.map +1 -0
  12. package/dist/src/auth/password.d.ts +4 -0
  13. package/dist/src/auth/password.js +5 -1
  14. package/dist/src/auth/password.js.map +1 -1
  15. package/dist/src/auth/routes.d.ts +1 -1
  16. package/dist/src/auth/routes.js +4 -1
  17. package/dist/src/auth/routes.js.map +1 -1
  18. package/dist/src/auth/service.d.ts +48 -9
  19. package/dist/src/auth/service.js +143 -68
  20. package/dist/src/auth/service.js.map +1 -1
  21. package/dist/src/auth/store.d.ts +26 -0
  22. package/dist/src/auth/store.js +82 -0
  23. package/dist/src/auth/store.js.map +1 -0
  24. package/dist/src/cli/index.d.ts +2 -2
  25. package/dist/src/cli/index.js +70 -84
  26. package/dist/src/cli/index.js.map +1 -1
  27. package/dist/src/core/config-values.d.ts +28 -0
  28. package/dist/src/core/config-values.js +53 -0
  29. package/dist/src/core/config-values.js.map +1 -0
  30. package/dist/src/core/constants.d.ts +1 -1
  31. package/dist/src/core/constants.js +1 -1
  32. package/dist/src/core/database.d.ts +10 -12
  33. package/dist/src/core/database.js +4 -4
  34. package/dist/src/core/database.js.map +1 -1
  35. package/dist/src/core/engine.d.ts +3 -2
  36. package/dist/src/core/engine.js +53 -44
  37. package/dist/src/core/engine.js.map +1 -1
  38. package/dist/src/core/lifecycle/mutation.d.ts +3 -3
  39. package/dist/src/core/lifecycle/mutation.js +2 -2
  40. package/dist/src/core/lifecycle/mutation.js.map +1 -1
  41. package/dist/src/core/lifecycle/options.d.ts +1 -0
  42. package/dist/src/core/lifecycle/options.js +2 -4
  43. package/dist/src/core/lifecycle/options.js.map +1 -1
  44. package/dist/src/core/model.d.ts +18 -3
  45. package/dist/src/core/model.js +17 -7
  46. package/dist/src/core/model.js.map +1 -1
  47. package/dist/src/core/mutations/write.d.ts +1 -2
  48. package/dist/src/core/mutations/write.js.map +1 -1
  49. package/dist/src/core/operations.d.ts +33 -0
  50. package/dist/src/core/operations.js +10 -0
  51. package/dist/src/core/operations.js.map +1 -0
  52. package/dist/src/core/pagination.d.ts +2 -2
  53. package/dist/src/core/pagination.js +3 -5
  54. package/dist/src/core/pagination.js.map +1 -1
  55. package/dist/src/core/paths.d.ts +2 -2
  56. package/dist/src/core/paths.js +15 -5
  57. package/dist/src/core/paths.js.map +1 -1
  58. package/dist/src/core/storage.d.ts +2 -0
  59. package/dist/src/core/storage.js +2 -0
  60. package/dist/src/core/storage.js.map +1 -0
  61. package/dist/src/files/contract.d.ts +6 -7
  62. package/dist/src/files/contract.js.map +1 -1
  63. package/dist/src/files/disk-store.js +3 -20
  64. package/dist/src/files/disk-store.js.map +1 -1
  65. package/dist/src/files/index.d.ts +1 -1
  66. package/dist/src/files/index.js +5 -2
  67. package/dist/src/files/index.js.map +1 -1
  68. package/dist/src/files/memory-store.js +16 -16
  69. package/dist/src/files/memory-store.js.map +1 -1
  70. package/dist/src/files/streams.d.ts +5 -0
  71. package/dist/src/files/streams.js +20 -0
  72. package/dist/src/files/streams.js.map +1 -0
  73. package/dist/src/graphql/entry.d.ts +11 -2
  74. package/dist/src/graphql/entry.js +12 -2
  75. package/dist/src/graphql/entry.js.map +1 -1
  76. package/dist/src/graphql/{public.d.ts → generate.d.ts} +3 -2
  77. package/dist/src/graphql/{public.js → generate.js} +4 -2
  78. package/dist/src/graphql/generate.js.map +1 -0
  79. package/dist/src/graphql/resolvers.js +1 -1
  80. package/dist/src/graphql/resolvers.js.map +1 -1
  81. package/dist/src/graphql/schema.js +6 -3
  82. package/dist/src/graphql/schema.js.map +1 -1
  83. package/dist/src/openapi/auth.d.ts +2 -2
  84. package/dist/src/openapi/auth.js +82 -15
  85. package/dist/src/openapi/auth.js.map +1 -1
  86. package/dist/src/openapi/document.js +16 -13
  87. package/dist/src/openapi/document.js.map +1 -1
  88. package/dist/src/openapi/entry.d.ts +12 -1
  89. package/dist/src/openapi/entry.js +12 -1
  90. package/dist/src/openapi/entry.js.map +1 -1
  91. package/dist/src/openapi/{public.d.ts → generate.d.ts} +9 -16
  92. package/dist/src/openapi/generate.js +41 -0
  93. package/dist/src/openapi/generate.js.map +1 -0
  94. package/dist/src/openapi/options.d.ts +18 -0
  95. package/dist/src/openapi/options.js +13 -0
  96. package/dist/src/openapi/options.js.map +1 -0
  97. package/dist/src/openapi/write.d.ts +5 -0
  98. package/dist/src/openapi/write.js +12 -0
  99. package/dist/src/openapi/write.js.map +1 -0
  100. package/dist/src/rest/routes.js +6 -9
  101. package/dist/src/rest/routes.js.map +1 -1
  102. package/dist/src/server/config.d.ts +29 -34
  103. package/dist/src/server/config.js +83 -211
  104. package/dist/src/server/config.js.map +1 -1
  105. package/dist/src/server/create.d.ts +3 -5
  106. package/dist/src/server/create.js +35 -29
  107. package/dist/src/server/create.js.map +1 -1
  108. package/dist/src/server/features.d.ts +0 -11
  109. package/dist/src/server/features.js +0 -20
  110. package/dist/src/server/features.js.map +1 -1
  111. package/dist/src/server/model.d.ts +6 -0
  112. package/dist/src/server/model.js +14 -0
  113. package/dist/src/server/model.js.map +1 -0
  114. package/dist/src/server/public.d.ts +1 -2
  115. package/package.json +1 -1
  116. package/dist/src/graphql/lazy.d.ts +0 -10
  117. package/dist/src/graphql/lazy.js +0 -13
  118. package/dist/src/graphql/lazy.js.map +0 -1
  119. package/dist/src/graphql/public.js.map +0 -1
  120. package/dist/src/openapi/index.d.ts +0 -13
  121. package/dist/src/openapi/index.js +0 -37
  122. package/dist/src/openapi/index.js.map +0 -1
  123. package/dist/src/openapi/lazy.d.ts +0 -11
  124. package/dist/src/openapi/lazy.js +0 -13
  125. package/dist/src/openapi/lazy.js.map +0 -1
  126. package/dist/src/openapi/public.js +0 -26
  127. package/dist/src/openapi/public.js.map +0 -1
package/README.ru.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  JSON-сервер для имитации API: REST, GraphQL, связанные записи, загрузка файлов и экспорт схем. Поддерживает вход пользователей, права владельца и администратора, даты записей и мягкое удаление. Требуется Node.js 22 или новее.
6
6
 
7
- **1.0.0-alpha.7 — предварительная версия.** REST-запросы используют `scope=[поля, аргументы?]` на всех уровнях. При обновлении измените параметры запросов по примерам ниже; для перехода с 0.x также нужна новая схема моделей.
7
+ **1.0.0-alpha.9 — предварительная версия.** При обновлении с предыдущей альфы добавьте общий `storage`, замените источники на `source`, перенесите модели под `models`, а настройки дат и удаления — в корень схемы. Модули теперь включаются наличием секций; для экспорта используйте `--generate` или `--generate-only`. Второй аргумент `createServer` удалён.
8
8
 
9
9
  ## Установка
10
10
 
@@ -12,7 +12,7 @@ JSON-сервер для имитации API: REST, GraphQL, связанные
12
12
  npm install @kollors/deep-json-server@alpha
13
13
  ```
14
14
 
15
- Для установки конкретной версии укажите `@1.0.0-alpha.7`.
15
+ Для установки конкретной версии укажите `@1.0.0-alpha.9`.
16
16
 
17
17
  ## Быстрый старт
18
18
 
@@ -31,9 +31,7 @@ npm install @kollors/deep-json-server@alpha
31
31
  `server.config.js`:
32
32
 
33
33
  ```js
34
- export default {
35
- database: { path: './database.json' },
36
- };
34
+ export default { storage: 'file', database: { source: './database.json' } };
37
35
  ```
38
36
 
39
37
  ```sh
@@ -46,47 +44,53 @@ npx deep-json-server server.config.js
46
44
 
47
45
  ## Конфигурация
48
46
 
49
- | Ключ | Назначение |
47
+ При `storage: 'file'` все источники и схема задаются путями, при `'memory'` — данными в памяти. Режимы нельзя смешивать. Наличие секций `auth`, `files`, `graphql` и `openapi` включает соответствующие модули; для стандартных маршрутов достаточно `graphql: {}` и `openapi: {}`.
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: { path: './generated/schema.graphql' },
56
+ openapi: { path: './generated/openapi.yaml' },
57
+ server: { host: '127.0.0.1', port: 4001 },
58
+ };
59
+ ```
60
+
61
+ | Настройка | Назначение |
50
62
  |---|---|
51
- | `database.path` / `database.data` | Для запуска укажите один вариант: JSON-файл или объект коллекций в памяти |
52
- | `database.schema` | Объект моделей или путь к JSON-файлу; необязателен для REST |
53
- | `database.timestamps` | Добавить даты создания и изменения; по умолчанию `false` |
54
- | `database.softDelete` | Сохранять удалённые записи с возможностью восстановления; по умолчанию `false` |
55
- | `openapi.enabled` | Включить HTTP-маршрут спецификации; по умолчанию `false` |
56
- | `openapi.endpoint` | Путь спецификации; по умолчанию `/openapi.json` |
57
- | `openapi.path` | Путь экспорта YAML |
58
- | `openapi.info` | Необязательный объект метаданных: обязательные `title` и `version`, необязательный `description` |
59
- | `graphql.enabled` | Включить GraphQL HTTP API; по умолчанию `false` |
60
- | `graphql.endpoint` | Путь GraphQL; по умолчанию `/graphql` |
61
- | `graphql.path` | Путь экспорта GraphQL SDL |
62
- | `auth.users` | Путь к JSON-массиву учётных записей или массив в памяти |
63
- | `auth.expiresIn` | Срок действия сессии в секундах; по умолчанию 3600 |
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 метаданных; по умолчанию `_database.json` внутри `files.source` |
70
+ | `graphql.endpoint` | HTTP-маршрут; по умолчанию `/graphql` |
71
+ | `graphql.path` | Файл для экспорта GraphQL SDL |
72
+ | `openapi.endpoint` | HTTP-маршрут; по умолчанию `/openapi.json` |
73
+ | `openapi.path` | Файл для экспорта OpenAPI |
74
+ | `openapi.info` | Метаданные: обязательные `title`, `version`, необязательный `description` |
64
75
  | `server.host`, `server.port` | По умолчанию `127.0.0.1`, `4001`; CLI также читает `HOST`/`PORT` |
65
76
  | `server.pageSize`, `server.maxPageSize` | По умолчанию 10 и 100; размер по умолчанию ограничен максимумом |
66
- | `server.cors`, `server.logger` | По умолчанию `true`; logger также принимает настройки Fastify |
77
+ | `server.cors`, `server.logger` | По умолчанию `true`; logger принимает также настройки Fastify |
67
78
  | `server.maxFileSize` | По умолчанию 100 МиБ |
68
- | `files.data` | Бинарные файлы в памяти |
69
- | `files.directory`, `files.metadata` | Каталог файлов и JSON метаданных; необходимы оба |
70
79
 
71
- Относительные пути отсчитываются от каталога конфигурационного файла. При вызове `createServer()` с объектом конфигурации — от рабочего каталога. Сервер работает с копией переданных данных в памяти и метаданных.
72
-
73
- Значение `server.port: 0` позволяет системе выбрать свободный порт. OpenAPI на HTTP-эндпоинте использует относительный адрес сервера.
80
+ Относительные пути разрешаются от каталога файла конфигурации; при вызове `createServer(config)` — от рабочего каталога. Данные в памяти, включая схему, копируются. Порт `0` позволяет системе выбрать свободный порт.
74
81
 
75
82
  ### CLI
76
83
 
77
- | Флаг CLI | Действие |
84
+ | Флаг | Действие |
78
85
  |---|---|
79
- | `--files` | Включить файловые маршруты; нужна секция `files` |
80
- | `--timestamps` | Включить даты записей глобально |
81
- | `--soft-delete` | Включить мягкое удаление глобально |
82
- | `--graphql` | Включить GraphQL API |
83
- | `--openapi` | Включить HTTP-маршрут OpenAPI |
86
+ | `--generate` | Экспортировать схемы и запустить сервер |
87
+ | `--generate-only` | Экспортировать схемы и завершить работу |
84
88
  | `--host <host>` | Адрес сервера |
85
89
  | `--port <port>` | Порт сервера |
86
- | `--help`, `-h` | Справка |
87
- | `--version`, `-v` | Версия пакета |
90
+ | `--help, -h` | Справка |
91
+ | `--version, -v` | Версия пакета |
88
92
 
89
- Приоритет адреса и порта: CLI → конфигурация → `HOST`/`PORT` → значения по умолчанию. Файловые маршруты включаются при наличии секции `files`, а вход и проверка прав — при наличии `auth`. Настройки модели могут переопределять глобальные `timestamps` и `softDelete`.
93
+ Приоритет адреса и порта: CLI → конфигурация → `HOST`/`PORT` → значения по умолчанию. Без флагов генерации запускается только сервер. `--generate` и `--generate-only` нельзя передавать вместе.
90
94
 
91
95
  ## Схема моделей
92
96
 
@@ -94,28 +98,52 @@ npx deep-json-server server.config.js
94
98
 
95
99
  ```json
96
100
  {
97
- "Country": {
98
- "collection": "countries",
99
- "api": ["openapi", "graphql"],
100
- "fields": {
101
- "id": { "type": "string", "primary": true, "generated": "uuid" },
102
- "name": { "type": "string", "required": true },
103
- "users": { "type": "User[]", "target": "countryId" }
104
- }
105
- },
106
- "User": {
107
- "collection": "users",
108
- "api": ["openapi", "graphql"],
109
- "fields": {
110
- "id": { "type": "string", "primary": true, "generated": "uuid" },
111
- "fullName": { "type": "string", "required": true },
112
- "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
+ }
113
141
  }
114
142
  }
115
143
  }
116
144
  ```
117
145
 
118
- По умолчанию `api` содержит `["openapi", "graphql"]`. Пустой массив исключает модель из экспорта и GraphQL, но REST продолжает работать. Связанные модели должны разрешать тот же формат экспорта. Имена должны быть допустимыми идентификаторами; конфликты генерируемых типов и операций вызывают ошибку. Имена `and`, `or`, `not` зарезервированы фильтрами.
146
+ Модели находятся в `models`. В корне схемы можно задать `api`, `timestamps` и `softDelete`; у модели эти параметры переопределяют общие значения. Для `api` приоритет такой: модель → корень схемы → секции конфигурации. Массив заменяется целиком; `[]` исключает модель из GraphQL и OpenAPI, но REST продолжает работать. Явный список не включает отсутствующий модуль. Связанные модели должны разрешать тот же формат. Имена моделей должны быть допустимыми идентификаторами; конфликты типов и операций вызывают ошибку. Имена `and`, `or`, `not` зарезервированы фильтрами.
119
147
 
120
148
  | Возможность | Со схемой | Без схемы |
121
149
  |---|---|---|
@@ -252,10 +280,7 @@ const scope = [
252
280
  fullName: true,
253
281
  movies: [
254
282
  { id: true, title: true },
255
- {
256
- order: [{ field: 'title', direction: 'ASC' }],
257
- pager: { page: 1, pageSize: 5 },
258
- },
283
+ { order: [{ field: 'title', direction: 'ASC' }], pager: { page: 1, pageSize: 5 } },
259
284
  ],
260
285
  },
261
286
  {
@@ -346,10 +371,10 @@ mutation {
346
371
 
347
372
  ### GraphQL
348
373
 
349
- Укажите `database.schema` и включите `graphql.enabled: true` в конфигурации или запустите сервер с `--graphql`:
374
+ Укажите `database.schema` и добавьте секцию `graphql: {}` в конфигурацию:
350
375
 
351
376
  ```sh
352
- npx deep-json-server --graphql server.config.js
377
+ npx deep-json-server server.config.js
353
378
  ```
354
379
 
355
380
  Отправляйте запросы на `/graphql` методом POST с `Content-Type: application/json` и телом `{ "query": "…", "variables": {} }`. Путь можно изменить через `graphql.endpoint`.
@@ -386,35 +411,33 @@ query {
386
411
 
387
412
  ## OpenAPI и экспорт схем
388
413
 
389
- Экспорт использует OpenAPI 3.0.3. Для `scope` назначение позиций массива описано текстом; их порядок проверяет сервер.
390
-
391
- Для получения спецификации по HTTP укажите `database.schema` и включите `openapi.enabled: true` или запустите сервер с `--openapi`. По умолчанию JSON доступен по адресу `/openapi.json`; путь задаётся в `openapi.endpoint`. Спецификацию можно открыть в отдельно установленном Swagger UI или импортировать в API-клиент.
392
-
393
- Для генерации укажите формат и файл конфигурации:
394
-
395
- ```sh
396
- npx deep-json-server generate openapi server.config.js
397
- npx deep-json-server generate graphql server.config.js
398
- npx deep-json-server generate openapi,graphql server.config.js
399
- ```
414
+ Экспорт использует OpenAPI 3.0.3. Добавьте `openapi: {}` и `database.schema`, чтобы получать спецификацию по HTTP на `/openapi.json`. Путь меняется через `openapi.endpoint`. Спецификацию можно открыть в Swagger UI или импортировать в API-клиент.
400
415
 
401
- Генерация выполняется без запуска сервера и чтения записей базы. Команда читает `database.schema` и сохраняет схемы в `openapi.path` и `graphql.path`. Флаги `enabled` управляют HTTP-маршрутами и для экспорта не требуются. Для генерации достаточно такой конфигурации:
416
+ Для сохранения схем задайте пути экспорта:
402
417
 
403
418
  ```js
404
419
  export default {
405
- database: { schema: './schema.json' },
420
+ storage: 'file',
421
+ database: { source: './database.json', schema: './schema.json' },
406
422
  openapi: { path: './generated/openapi.yaml' },
407
423
  graphql: { path: './generated/schema.graphql' },
408
424
  };
409
425
  ```
410
426
 
411
- Для каждого формата нужен отдельный файл. Команда отклонит путь, который перезапишет конфигурацию, базу, схему, учётные записи auth или метаданные файлов.
427
+ ```bash
428
+ npx deep-json-server server.config.js --generate-only
429
+ npx deep-json-server server.config.js --generate
430
+ ```
431
+
432
+ `--generate-only` экспортирует и завершает работу, `--generate` после экспорта запускает сервер. Форматы определяются наличием секций. Для каждого выбранного формата обязателен свой `path`. Если секций нет, отсутствует путь или генерация завершилась ошибкой, команда возвращает ошибку и сервер не запускается.
433
+
434
+ Для экспорта читается только схема. База, пользователи auth и файловое хранилище не открываются; параметры конфигурации всё равно проверяются. Схема загружается один раз, в том числе при экспорте с последующим запуском. Перед записью проверяются пути и строятся все выбранные форматы. Нельзя перезаписать конфигурацию, базу, схему, пользователей, счётчики или метаданные файлов. Ошибка записи на диск может оставить уже сохранённый файл другого формата.
412
435
 
413
436
  ## Аутентификация
414
437
 
415
- Секция `auth` включает REST-вход и проверку прав на изменение записей в REST и GraphQL. Чтение и все операции с файлами остаются открытыми.
438
+ Секция `auth` включает регистрацию, вход и проверку прав на изменение записей в REST и GraphQL. Чтение и все операции с файлами остаются открытыми.
416
439
 
417
- Создайте учётную запись в отдельном файле с помощью `setup-auth.mjs`:
440
+ Первого администратора задайте в исходных данных. Например, создайте `auth.json` с помощью `setup-auth.mjs`:
418
441
 
419
442
  ```js
420
443
  import { writeFile } from 'node:fs/promises';
@@ -422,64 +445,99 @@ import { hashPassword } from '@kollors/deep-json-server/auth';
422
445
 
423
446
  const password = process.env.DJS_PASSWORD;
424
447
  if (!password) throw new Error('Set DJS_PASSWORD');
425
- await writeFile('./auth.json', JSON.stringify([
426
- { id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true },
427
- ], null, 2), { flag: 'wx', mode: 0o600 });
448
+ await writeFile(
449
+ './auth.json',
450
+ JSON.stringify(
451
+ [{ id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true }],
452
+ null,
453
+ 2,
454
+ ),
455
+ { flag: 'wx', mode: 0o600 },
456
+ );
428
457
  ```
429
458
 
430
459
  Задайте `DJS_PASSWORD` и выполните `node setup-auth.mjs`. Добавьте файл в конфигурацию сервера:
431
460
 
432
461
  ```js
433
462
  export default {
434
- database: { path: './database.json' },
435
- auth: { users: './auth.json', expiresIn: 3600 },
463
+ storage: 'file',
464
+ database: { source: './database.json' },
465
+ auth: { source: './auth.json', expiresIn: 3600 },
436
466
  };
437
467
  ```
438
468
 
439
- Запустите `npx deep-json-server server.config.js`. Наличие секции `auth` включает модуль; без неё изменения записей доступны без токена. Каждой учётной записи нужны уникальный строковый `id`, уникальный `username` и `passwordHash`, созданный функцией выше. `isAdmin` по умолчанию равен `false`. Пароли хешируются через scrypt со случайной солью. Учётные записи auth хранятся отдельно от коллекций базы. Файл читается при запуске; после его изменения перезапустите сервер.
440
-
441
- | REST-запрос | Входные данные | Ответ |
442
- |---|---|---|
443
- | `POST /auth/login` | JSON `{ "username": "admin", "password": "…" }` | `{ accessToken, expiresIn, user: { id, username, isAdmin } }` |
444
- | `GET /auth/me` | Bearer-токен | `{ id, username, isAdmin }` |
445
- | `POST /auth/logout` | Bearer-токен | `{ success: true }` |
446
-
447
- Передавайте токен в заголовке `Authorization: Bearer <accessToken>`. Неверные учётные данные, недействительный или истёкший токен возвращают HTTP 401. Сессии хранятся в памяти и исчезают при перезапуске; выход отзывает переданный токен. Вход может вернуть HTTP 429 при слишком большом числе одновременных попыток или активных сессий.
469
+ Запустите `npx deep-json-server server.config.js`. Каждой исходной учётной записи нужны уникальный строковый `id`, уникальный `username` и `passwordHash`, созданный функцией выше. `isAdmin` по умолчанию равен `false`. Пароли хешируются через scrypt со случайной солью.
448
470
 
449
- В OpenAPI описывается Bearer-аутентификация для изменения записей, `/auth/me` и `/auth/logout`. В Swagger UI токен из ответа на вход можно вставить в **Authorize**. Для экспорта схем включите auth в конфигурации и выполните `generate openapi server.config.js`; файл учётных записей при генерации не читается. Для GraphQL и OpenAPI нужна `database.schema`.
471
+ При `storage: 'memory'` передайте массив в `auth.source`:
450
472
 
451
- ## Даты записей, удаление и владельцы
473
+ ```js
474
+ import { hashPassword } from '@kollors/deep-json-server/auth';
452
475
 
453
- Глобальные настройки задаются в `database`:
476
+ const password = process.env.DJS_PASSWORD;
477
+ if (!password) throw new Error('Set DJS_PASSWORD');
454
478
 
455
- ```js
456
479
  export default {
457
- database: {
458
- path: './database.json',
459
- schema: './schema.json',
460
- timestamps: true,
461
- softDelete: true,
480
+ storage: 'memory',
481
+ database: { source: { items: [] } },
482
+ auth: {
483
+ source: [
484
+ { id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true },
485
+ ],
462
486
  },
463
- auth: { users: './auth.json' },
464
487
  };
465
488
  ```
466
489
 
467
- В модели можно переопределить `timestamps` и `softDelete` рядом с `collection` и `fields`. Отсутствующее значение наследуется из глобальных настроек; `true` или `false` переопределяет его. CLI имеет приоритет над глобальным конфигом, а настройки модели над обоими. Без схемы глобальные значения действуют на все коллекции. Например, эта модель отключает даты и сохраняет удалённые записи независимо от глобальных настроек:
490
+ Учётные записи auth хранятся отдельно от коллекций базы. При `storage: 'file'` регистрация, смена пароля и статуса сохраняются в файл `auth.source` через `lowdb`. При `'memory'` изменения остаются во внутренней копии и исчезают после перезапуска; исходный массив не меняется. Ошибка записи файла отменяет изменение и сохраняет действующие сессии. Файл читается при запуске; после ручного редактирования перезапустите сервер.
491
+
492
+ | REST-запрос | JSON тела | Ответ |
493
+ |---|---|---|
494
+ | `POST /auth/register` | `{ "username": "anna", "password": "…" }` | `201`: `{ id, username, isAdmin: false }` |
495
+ | `POST /auth/login` | `{ "username": "anna", "password": "…" }` | `{ accessToken, expiresIn, user: { id, username, isAdmin } }` |
496
+ | `GET /auth/me` | — | `{ id, username, isAdmin }` |
497
+ | `POST /auth/logout` | — | `{ success: true }` |
498
+ | `PATCH /auth/users/:id/password` | `{ "currentPassword": "…", "newPassword": "…" }` | `{ success: true }` |
499
+ | `PATCH /auth/users/:id/admin` | `{ "isAdmin": true }` | `{ id, username, isAdmin }` |
500
+
501
+ Регистрация и вход доступны без токена. Для остальных методов передавайте `Authorization: Bearer <accessToken>`. Регистрация создаёт обычного пользователя с новым `id`; поля `id`, `passwordHash` и `isAdmin` в запросе запрещены. `username` уникален с учётом регистра, повтор возвращает `409`. Имя должно содержать хотя бы один непробельный символ и не более 256 символов, пароль — от 1 до 1024 символов. Значения не обрезаются. Регистрация не открывает сессию: после неё выполните вход.
502
+
503
+ Пользователь меняет только свой пароль, передавая `currentPassword` и `newPassword`. Для администратора при смене своего пароля действует то же правило. Администратор может сменить пароль обычному пользователю, передав только `newPassword`. Изменение пароля другого администратора возвращает `403`. После успешной смены завершаются все сессии этого пользователя, включая текущую при смене своего пароля; требуется новый вход. Неверный текущий пароль возвращает `401` без изменения сессий.
504
+
505
+ Статус `isAdmin` меняет только администратор. Можно назначать других администраторов и снимать с них статус. Снять статус с себя можно, только если остаётся другой администратор; иначе возвращается `409`. Проверка учитывает параллельные запросы. Новые права действуют с прежними токенами сразу после сохранения, в том числе для мутаций GraphQL. Администратор может снять статус с другого администратора, а затем сменить ему пароль как обычному пользователю.
506
+
507
+ Недействительный или истёкший токен возвращает `401`, недостаток прав — `403`, отсутствующий пользователь при разрешённой операции — `404`. Ошибки тела запроса возвращают `400`. Вход, регистрация и смена пароля могут вернуть `429` при превышении числа одновременных вычислений паролей; вход также ограничивает число активных сессий. Сессии хранятся в памяти и исчезают после перезапуска. Выход завершает только сессию переданного токена.
508
+
509
+ OpenAPI описывает все маршруты auth и требования Bearer-токена. В Swagger UI токен из ответа на вход можно вставить в **Authorize**. Для экспорта схем включите auth в конфигурации и выполните `npx deep-json-server server.config.js --generate-only`; файл учётных записей при генерации не читается. Методы auth доступны через REST. В GraphQL тот же токен проверяется при изменении записей. Для GraphQL и OpenAPI нужна `database.schema`.
510
+
511
+ ## Даты записей, удаление и владельцы
512
+
513
+ Общие настройки задаются в корне схемы. В этом примере модель `Note` наследует мягкое удаление и отключает даты:
468
514
 
469
515
  ```json
470
516
  {
471
- "Note": {
472
- "collection": "notes",
473
- "timestamps": false,
474
- "softDelete": true,
475
- "fields": {
476
- "id": { "type": "string", "primary": true, "generated": "uuid" },
477
- "text": { "type": "string", "required": true }
517
+ "timestamps": true,
518
+ "softDelete": true,
519
+ "models": {
520
+ "Note": {
521
+ "collection": "notes",
522
+ "timestamps": false,
523
+ "fields": {
524
+ "id": {
525
+ "type": "string",
526
+ "primary": true,
527
+ "generated": "uuid"
528
+ },
529
+ "text": {
530
+ "type": "string",
531
+ "required": true
532
+ }
533
+ }
478
534
  }
479
535
  }
480
536
  }
481
537
  ```
482
538
 
539
+ Приоритет: модель → корень схемы → `false`. Явное `false` отключает унаследованную настройку. Без схемы даты и мягкое удаление выключены.
540
+
483
541
  | Поле | Когда включено | Значение |
484
542
  |---|---|---|
485
543
  | `createdAt`, `updatedAt` | `timestamps` | Даты создания и последнего изменения |
@@ -516,8 +574,9 @@ REST возвращает 401 при отсутствии действитель
516
574
 
517
575
  ```js
518
576
  export default {
519
- database: { path: './database.json' },
520
- files: { directory: './uploads', metadata: './files.json' },
577
+ storage: 'file',
578
+ database: { source: './database.json' },
579
+ files: { source: './uploads', metadata: './files.json' },
521
580
  };
522
581
  ```
523
582
 
@@ -527,7 +586,7 @@ export default {
527
586
  npx deep-json-server server.config.js
528
587
  ```
529
588
 
530
- Для временных тестов вместо этого используйте `files.data`. Каждая начальная запись содержит `name`, `mimeType`, бинарное `content` в виде `Uint8Array` и необязательный `directory`. Загруженные файлы в таком режиме остаются в памяти до завершения процесса.
589
+ Для временных тестов выберите `storage: 'memory'` и передайте массив в `files.source`. Каждая начальная запись содержит `name`, `mimeType`, бинарное `content` в виде `Uint8Array` и необязательный `directory`. Загруженные файлы в таком режиме остаются в памяти до завершения процесса.
531
590
 
532
591
  Один файл отправляется непосредственно в теле запроса. `Content-Name` содержит URI-кодированное имя файла, `Content-Type` — его MIME-тип, а необязательный `Content-Directory` — URI-кодированный относительный путь к директории:
533
592
 
@@ -578,9 +637,9 @@ Content-Type: application/json
578
637
  }
579
638
  ```
580
639
 
581
- `PATCH` возвращает обновлённые метаданные со статусом `200`; если по новому пути уже существует файл, сервер возвращает `409`. `DELETE` отвечает статусом `204` без тела. Если файл не найден, любая операция по пути возвращает `404`. Пути в URL задаются относительно `files.directory`, а все возвращаемые URL — относительно адреса сервера.
640
+ `PATCH` возвращает обновлённые метаданные со статусом `200`; если по новому пути уже существует файл, сервер возвращает `409`. `DELETE` отвечает статусом `204` без тела. Если файл не найден, любая операция по пути возвращает `404`. Пути в URL задаются относительно `files.source`, а все возвращаемые URL — относительно адреса сервера.
582
641
 
583
- При хранении на диске бинарный файл находится по пути `<files.directory>/<directory>/<name>`. Метаданные содержат `directory`, `mimeType` и `name`; размер сервер читает из файла, а URL формирует сам. Директории и файл метаданных создаются по мере необходимости.
642
+ При хранении на диске бинарный файл находится по пути `<files.source>/<directory>/<name>`. Метаданные содержат `directory`, `mimeType` и `name`; размер сервер читает из файла, а URL формирует сам. Директории и файл метаданных создаются по мере необходимости.
584
643
 
585
644
  Используйте один процесс сервера для дисковой базы и файлового хранилища. Перед ручным изменением файлов или метаданных остановите его. Пути в хранилище не могут содержать символические ссылки. Загрузка и переименование не могут перезаписать базу, счётчики, схему, учётные записи auth, загруженную конфигурацию или файл метаданных.
586
645
 
@@ -600,7 +659,7 @@ await server.listen();
600
659
 
601
660
  Методы `openapi()` и `graphql()` возвращают схемы и требуют `database.schema`. `fastify()` возвращает экземпляр сервера для настройки и запуска. База и включённые сервисы инициализируются при `ready()`, `listen()` или первом `inject()`; ошибка инициализации останавливает запуск.
602
661
 
603
- Второй аргумент переопределяет подключение модулей, например `createServer(config, { files: false, graphql: true })`. Допустимы флаги `files`, `graphql`, `openapi` и `auth`. Для включения auth или файлов нужна соответствующая секция конфигурации; для GraphQL и OpenAPI схема моделей.
662
+ `createServer(config)` принимает один аргумент. Наличие секций управляет модулями так же, как при запуске через CLI. Для методов `openapi()` и `graphql()` нужна соответствующая секция. Методы возвращают схему и не записывают файлы.
604
663
 
605
664
  Эти функции доступны и через общий импорт `@kollors/deep-json-server`. Адаптеры сервера загружаются при включении. Генераторы можно использовать отдельно:
606
665
 
@@ -614,7 +673,7 @@ await writeOpenapi(document, './generated/openapi.yaml');
614
673
  await writeGraphql(sdl, './generated/schema.graphql');
615
674
  ```
616
675
 
617
- Оба генератора принимают `timestamps`, `softDelete` и `auth` для описания полей записей. При `{ auth: true }` OpenAPI также добавляет REST-маршруты auth и требования токена для изменения записей. Функция `hashPassword()` доступна и через общий импорт пакета.
676
+ Отдельные генераторы принимают путь или объект схемы и не требуют конфигурации сервера. Выбранная функция задаёт формат по умолчанию; `api` в схеме и моделях может его ограничить. `timestamps` и `softDelete` берутся из схемы. Опция `{ auth: true }` добавляет поля владельца; OpenAPI также описывает маршруты auth и требования токена. `hashPassword()` доступна и через общий импорт пакета.
618
677
 
619
678
  `generateOpenapi()` также принимает `host`, `port`, `pageSize`, `maxPageSize` и `info`. Вместо пути можно передать объект схемы. Сервер и генераторы работают с собственной копией модели. Размеры страниц должны быть положительными целыми числами; `pageSize` не может превышать `maxPageSize`.
620
679
 
package/dist/index.d.ts CHANGED
@@ -3,12 +3,11 @@ export { hashPassword } from './src/auth/public.js';
3
3
  export type { EntityDefinition, Field, ModelSchema } from './src/core/model.js';
4
4
  export type { FileUpdate } from './src/files/contract.js';
5
5
  export type { FileMetadata } from './src/files/http.js';
6
- export { generateGraphql, writeGraphql } from './src/graphql/lazy.js';
7
- export type { GraphqlOptions } from './src/graphql/public.js';
8
- export { generateOpenapi, writeOpenapi } from './src/openapi/lazy.js';
9
- export type { OpenapiOptions } from './src/openapi/public.js';
6
+ export type { GraphqlOptions } from './src/graphql/entry.js';
7
+ export { generateGraphql, writeGraphql } from './src/graphql/entry.js';
8
+ export type { OpenapiOptions } from './src/openapi/entry.js';
9
+ export { generateOpenapi, writeOpenapi } from './src/openapi/entry.js';
10
10
  export type { OpenapiDocument } from './src/openapi/types.js';
11
- export type { DatabaseConfig, DeepJsonServerConfig, FilesConfig, GraphqlConfig, MemoryFile, OpenapiConfig, ServerConfig } from './src/server/config.js';
11
+ export type { DatabaseConfig, DeepJsonServerConfig, FilesConfig, GraphqlConfig, MemoryFile, OpenapiConfig, ServerConfig, Storage } from './src/server/config.js';
12
12
  export type { ServerFacade } from './src/server/create.js';
13
13
  export { createServer } from './src/server/create.js';
14
- export type { ServerFeatures } from './src/server/features.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { hashPassword } from './src/auth/public.js';
2
- export { generateGraphql, writeGraphql } from './src/graphql/lazy.js';
3
- export { generateOpenapi, writeOpenapi } from './src/openapi/lazy.js';
2
+ export { generateGraphql, writeGraphql } from './src/graphql/entry.js';
3
+ export { generateOpenapi, writeOpenapi } from './src/openapi/entry.js';
4
4
  export { createServer } from './src/server/create.js';
5
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAIpD,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAKtE,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAKpD,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEvE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAIvE,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC"}
@@ -1,3 +1,4 @@
1
+ import type { Source, Storage } from '../core/storage.js';
1
2
  export interface AuthUser {
2
3
  id: string;
3
4
  username: string;
@@ -7,8 +8,8 @@ export interface AuthUserRecord extends Omit<AuthUser, 'isAdmin'> {
7
8
  isAdmin?: boolean;
8
9
  passwordHash: string;
9
10
  }
10
- export interface AuthConfig {
11
- users: string | AuthUserRecord[];
11
+ export interface AuthConfig<S extends Storage = Storage> {
12
+ source: Source<S, AuthUserRecord[]>;
12
13
  /** Session lifetime in seconds. Defaults to one hour. */
13
14
  expiresIn?: number;
14
15
  }
@@ -19,8 +20,11 @@ export interface AuthSession {
19
20
  }
20
21
  export declare const AUTH_PATHS: {
21
22
  readonly login: '/auth/login';
23
+ readonly register: '/auth/register';
22
24
  readonly me: '/auth/me';
23
25
  readonly logout: '/auth/logout';
26
+ readonly password: '/auth/users/:id/password';
27
+ readonly admin: '/auth/users/:id/admin';
24
28
  };
25
29
  export declare const DEFAULT_SESSION_SECONDS = 3600;
26
30
  export declare const MAX_USERNAME_LENGTH = 256;
@@ -1,4 +1,11 @@
1
- export const AUTH_PATHS = { login: '/auth/login', me: '/auth/me', logout: '/auth/logout' };
1
+ export const AUTH_PATHS = {
2
+ login: '/auth/login',
3
+ register: '/auth/register',
4
+ me: '/auth/me',
5
+ logout: '/auth/logout',
6
+ password: '/auth/users/:id/password',
7
+ admin: '/auth/users/:id/admin',
8
+ };
2
9
  export const DEFAULT_SESSION_SECONDS = 3600;
3
10
  export const MAX_USERNAME_LENGTH = 256;
4
11
  export const MAX_PASSWORD_LENGTH = 1024;
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../src/auth/contract.ts"],"names":[],"mappings":"AAmBA,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAW,CAAC;AACpG,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAC5C,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AACvC,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC"}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../src/auth/contract.ts"],"names":[],"mappings":"AAoBA,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,KAAK,EAAE,aAAa;IACpB,QAAQ,EAAE,gBAAgB;IAC1B,EAAE,EAAE,UAAU;IACd,MAAM,EAAE,cAAc;IACtB,QAAQ,EAAE,0BAA0B;IACpC,KAAK,EAAE,uBAAuB;CACtB,CAAC;AACX,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAC5C,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AACvC,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC"}
@@ -0,0 +1,18 @@
1
+ /** Принимает только непустое имя и допустимый пароль без дополнительных полей.
2
+ * @example { username: 'anna', password: 'secret' } → та же пара строк; дополнительный isAdmin → ошибка.
3
+ */
4
+ export declare function credentials(body: unknown): {
5
+ username: string;
6
+ password: string;
7
+ };
8
+ /** Проверяет новый пароль и необязательный текущий пароль; оба значения возвращает без преобразований.
9
+ * @example { newPassword: 'next' } → { newPassword: 'next', currentPassword: undefined }; пустой пароль → ошибка.
10
+ */
11
+ export declare function passwordChange(body: unknown): {
12
+ newPassword: string;
13
+ currentPassword?: string;
14
+ };
15
+ /** Принимает единственный логический флаг без преобразования строк и чисел.
16
+ * @example { isAdmin: false } → false; { isAdmin: 'false' } → ошибка.
17
+ */
18
+ export declare function adminChange(body: unknown): boolean;
@@ -0,0 +1,34 @@
1
+ import { domainError } from '../core/errors.js';
2
+ import { hasOnlyKeys, isObject } from '../core/utils.js';
3
+ import { MAX_USERNAME_LENGTH } from './contract.js';
4
+ import { validPassword } from './password.js';
5
+ /** Принимает только непустое имя и допустимый пароль без дополнительных полей.
6
+ * @example { username: 'anna', password: 'secret' } → та же пара строк; дополнительный isAdmin → ошибка.
7
+ */
8
+ export function credentials(body) {
9
+ if (!isObject(body) ||
10
+ !hasOnlyKeys(body, ['username', 'password']) ||
11
+ typeof body.username !== 'string' ||
12
+ !body.username.trim() ||
13
+ body.username.length > MAX_USERNAME_LENGTH ||
14
+ !validPassword(body.password))
15
+ throw domainError('INVALID_INPUT', 'Supply username and password');
16
+ return { username: body.username, password: body.password };
17
+ }
18
+ /** Проверяет новый пароль и необязательный текущий пароль; оба значения возвращает без преобразований.
19
+ * @example { newPassword: 'next' } → { newPassword: 'next', currentPassword: undefined }; пустой пароль → ошибка.
20
+ */
21
+ export function passwordChange(body) {
22
+ if (!isObject(body) || !hasOnlyKeys(body, ['currentPassword', 'newPassword']) || !validPassword(body.newPassword) || (body.currentPassword !== undefined && !validPassword(body.currentPassword)))
23
+ throw domainError('INVALID_INPUT', 'Supply newPassword and an optional currentPassword');
24
+ return { newPassword: body.newPassword, currentPassword: body.currentPassword };
25
+ }
26
+ /** Принимает единственный логический флаг без преобразования строк и чисел.
27
+ * @example { isAdmin: false } → false; { isAdmin: 'false' } → ошибка.
28
+ */
29
+ export function adminChange(body) {
30
+ if (!isObject(body) || !hasOnlyKeys(body, ['isAdmin']) || typeof body.isAdmin !== 'boolean')
31
+ throw domainError('INVALID_INPUT', 'Supply a boolean isAdmin');
32
+ return body.isAdmin;
33
+ }
34
+ //# sourceMappingURL=input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.js","sourceRoot":"","sources":["../../../src/auth/input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,IAAa;IACvC,IACE,CAAC,QAAQ,CAAC,IAAI,CAAC;QACf,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAC5C,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ;QACjC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE;QACrB,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,mBAAmB;QAC1C,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC;QAE7B,MAAM,WAAW,CAAC,eAAe,EAAE,8BAA8B,CAAC,CAAC;IACrE,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;AAC9D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,IAAa;IAC1C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,KAAK,SAAS,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAC/L,MAAM,WAAW,CAAC,eAAe,EAAE,oDAAoD,CAAC,CAAC;IAC3F,OAAO,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,eAAe,EAAE,IAAI,CAAC,eAAqC,EAAE,CAAC;AACxG,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,IAAa;IACvC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,SAAS,CAAC,CAAC,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,SAAS;QAAE,MAAM,WAAW,CAAC,eAAe,EAAE,0BAA0B,CAAC,CAAC;IAC5J,OAAO,IAAI,CAAC,OAAO,CAAC;AACtB,CAAC"}