@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.
- package/README.md +168 -109
- package/README.ru.md +171 -112
- package/dist/index.d.ts +5 -6
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/src/auth/contract.d.ts +6 -2
- package/dist/src/auth/contract.js +8 -1
- package/dist/src/auth/contract.js.map +1 -1
- package/dist/src/auth/input.d.ts +18 -0
- package/dist/src/auth/input.js +34 -0
- package/dist/src/auth/input.js.map +1 -0
- package/dist/src/auth/password.d.ts +4 -0
- package/dist/src/auth/password.js +5 -1
- package/dist/src/auth/password.js.map +1 -1
- package/dist/src/auth/routes.d.ts +1 -1
- package/dist/src/auth/routes.js +4 -1
- package/dist/src/auth/routes.js.map +1 -1
- package/dist/src/auth/service.d.ts +48 -9
- package/dist/src/auth/service.js +143 -68
- package/dist/src/auth/service.js.map +1 -1
- package/dist/src/auth/store.d.ts +26 -0
- package/dist/src/auth/store.js +82 -0
- package/dist/src/auth/store.js.map +1 -0
- package/dist/src/cli/index.d.ts +2 -2
- package/dist/src/cli/index.js +70 -84
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/core/config-values.d.ts +28 -0
- package/dist/src/core/config-values.js +53 -0
- package/dist/src/core/config-values.js.map +1 -0
- package/dist/src/core/constants.d.ts +1 -1
- package/dist/src/core/constants.js +1 -1
- package/dist/src/core/database.d.ts +10 -12
- package/dist/src/core/database.js +4 -4
- package/dist/src/core/database.js.map +1 -1
- package/dist/src/core/engine.d.ts +3 -2
- package/dist/src/core/engine.js +53 -44
- package/dist/src/core/engine.js.map +1 -1
- package/dist/src/core/lifecycle/mutation.d.ts +3 -3
- package/dist/src/core/lifecycle/mutation.js +2 -2
- package/dist/src/core/lifecycle/mutation.js.map +1 -1
- package/dist/src/core/lifecycle/options.d.ts +1 -0
- package/dist/src/core/lifecycle/options.js +2 -4
- package/dist/src/core/lifecycle/options.js.map +1 -1
- package/dist/src/core/model.d.ts +18 -3
- package/dist/src/core/model.js +17 -7
- package/dist/src/core/model.js.map +1 -1
- package/dist/src/core/mutations/write.d.ts +1 -2
- package/dist/src/core/mutations/write.js.map +1 -1
- package/dist/src/core/operations.d.ts +33 -0
- package/dist/src/core/operations.js +10 -0
- package/dist/src/core/operations.js.map +1 -0
- package/dist/src/core/pagination.d.ts +2 -2
- package/dist/src/core/pagination.js +3 -5
- package/dist/src/core/pagination.js.map +1 -1
- package/dist/src/core/paths.d.ts +2 -2
- package/dist/src/core/paths.js +15 -5
- package/dist/src/core/paths.js.map +1 -1
- package/dist/src/core/storage.d.ts +2 -0
- package/dist/src/core/storage.js +2 -0
- package/dist/src/core/storage.js.map +1 -0
- package/dist/src/files/contract.d.ts +6 -7
- package/dist/src/files/contract.js.map +1 -1
- package/dist/src/files/disk-store.js +3 -20
- package/dist/src/files/disk-store.js.map +1 -1
- package/dist/src/files/index.d.ts +1 -1
- package/dist/src/files/index.js +5 -2
- package/dist/src/files/index.js.map +1 -1
- package/dist/src/files/memory-store.js +16 -16
- package/dist/src/files/memory-store.js.map +1 -1
- package/dist/src/files/streams.d.ts +5 -0
- package/dist/src/files/streams.js +20 -0
- package/dist/src/files/streams.js.map +1 -0
- package/dist/src/graphql/entry.d.ts +11 -2
- package/dist/src/graphql/entry.js +12 -2
- package/dist/src/graphql/entry.js.map +1 -1
- package/dist/src/graphql/{public.d.ts → generate.d.ts} +3 -2
- package/dist/src/graphql/{public.js → generate.js} +4 -2
- package/dist/src/graphql/generate.js.map +1 -0
- package/dist/src/graphql/resolvers.js +1 -1
- package/dist/src/graphql/resolvers.js.map +1 -1
- package/dist/src/graphql/schema.js +6 -3
- package/dist/src/graphql/schema.js.map +1 -1
- package/dist/src/openapi/auth.d.ts +2 -2
- package/dist/src/openapi/auth.js +82 -15
- package/dist/src/openapi/auth.js.map +1 -1
- package/dist/src/openapi/document.js +16 -13
- package/dist/src/openapi/document.js.map +1 -1
- package/dist/src/openapi/entry.d.ts +12 -1
- package/dist/src/openapi/entry.js +12 -1
- package/dist/src/openapi/entry.js.map +1 -1
- package/dist/src/openapi/{public.d.ts → generate.d.ts} +9 -16
- package/dist/src/openapi/generate.js +41 -0
- package/dist/src/openapi/generate.js.map +1 -0
- package/dist/src/openapi/options.d.ts +18 -0
- package/dist/src/openapi/options.js +13 -0
- package/dist/src/openapi/options.js.map +1 -0
- package/dist/src/openapi/write.d.ts +5 -0
- package/dist/src/openapi/write.js +12 -0
- package/dist/src/openapi/write.js.map +1 -0
- package/dist/src/rest/routes.js +6 -9
- package/dist/src/rest/routes.js.map +1 -1
- package/dist/src/server/config.d.ts +29 -34
- package/dist/src/server/config.js +83 -211
- package/dist/src/server/config.js.map +1 -1
- package/dist/src/server/create.d.ts +3 -5
- package/dist/src/server/create.js +35 -29
- package/dist/src/server/create.js.map +1 -1
- package/dist/src/server/features.d.ts +0 -11
- package/dist/src/server/features.js +0 -20
- package/dist/src/server/features.js.map +1 -1
- package/dist/src/server/model.d.ts +6 -0
- package/dist/src/server/model.js +14 -0
- package/dist/src/server/model.js.map +1 -0
- package/dist/src/server/public.d.ts +1 -2
- package/package.json +1 -1
- package/dist/src/graphql/lazy.d.ts +0 -10
- package/dist/src/graphql/lazy.js +0 -13
- package/dist/src/graphql/lazy.js.map +0 -1
- package/dist/src/graphql/public.js.map +0 -1
- package/dist/src/openapi/index.d.ts +0 -13
- package/dist/src/openapi/index.js +0 -37
- package/dist/src/openapi/index.js.map +0 -1
- package/dist/src/openapi/lazy.d.ts +0 -11
- package/dist/src/openapi/lazy.js +0 -13
- package/dist/src/openapi/lazy.js.map +0 -1
- package/dist/src/openapi/public.js +0 -26
- 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
|
+
**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.
|
|
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
|
-
| `
|
|
52
|
-
| `database.
|
|
53
|
-
| `database.
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `graphql.
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
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 также
|
|
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
|
-
Относительные пути
|
|
72
|
-
|
|
73
|
-
Значение `server.port: 0` позволяет системе выбрать свободный порт. OpenAPI на HTTP-эндпоинте использует относительный адрес сервера.
|
|
80
|
+
Относительные пути разрешаются от каталога файла конфигурации; при вызове `createServer(config)` — от рабочего каталога. Данные в памяти, включая схему, копируются. Порт `0` позволяет системе выбрать свободный порт.
|
|
74
81
|
|
|
75
82
|
### CLI
|
|
76
83
|
|
|
77
|
-
| Флаг
|
|
84
|
+
| Флаг | Действие |
|
|
78
85
|
|---|---|
|
|
79
|
-
| `--
|
|
80
|
-
| `--
|
|
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
|
|
87
|
-
| `--version
|
|
90
|
+
| `--help, -h` | Справка |
|
|
91
|
+
| `--version, -v` | Версия пакета |
|
|
88
92
|
|
|
89
|
-
Приоритет адреса и порта: CLI → конфигурация → `HOST`/`PORT` → значения по умолчанию.
|
|
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
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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` и
|
|
374
|
+
Укажите `database.schema` и добавьте секцию `graphql: {}` в конфигурацию:
|
|
350
375
|
|
|
351
376
|
```sh
|
|
352
|
-
npx deep-json-server
|
|
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.
|
|
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
|
-
|
|
416
|
+
Для сохранения схем задайте пути экспорта:
|
|
402
417
|
|
|
403
418
|
```js
|
|
404
419
|
export default {
|
|
405
|
-
|
|
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
|
-
|
|
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` включает
|
|
438
|
+
Секция `auth` включает регистрацию, вход и проверку прав на изменение записей в REST и GraphQL. Чтение и все операции с файлами остаются открытыми.
|
|
416
439
|
|
|
417
|
-
|
|
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(
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
435
|
-
|
|
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`.
|
|
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
|
-
|
|
471
|
+
При `storage: 'memory'` передайте массив в `auth.source`:
|
|
450
472
|
|
|
451
|
-
|
|
473
|
+
```js
|
|
474
|
+
import { hashPassword } from '@kollors/deep-json-server/auth';
|
|
452
475
|
|
|
453
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
"
|
|
475
|
-
|
|
476
|
-
"
|
|
477
|
-
"
|
|
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
|
-
|
|
520
|
-
|
|
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
|
-
Для временных тестов
|
|
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.
|
|
640
|
+
`PATCH` возвращает обновлённые метаданные со статусом `200`; если по новому пути уже существует файл, сервер возвращает `409`. `DELETE` отвечает статусом `204` без тела. Если файл не найден, любая операция по пути возвращает `404`. Пути в URL задаются относительно `files.source`, а все возвращаемые URL — относительно адреса сервера.
|
|
582
641
|
|
|
583
|
-
При хранении на диске бинарный файл находится по пути `<files.
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
7
|
-
export
|
|
8
|
-
export {
|
|
9
|
-
export
|
|
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/
|
|
3
|
-
export { generateOpenapi, writeOpenapi } from './src/openapi/
|
|
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;
|
|
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
|
-
|
|
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 = {
|
|
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":"
|
|
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"}
|