@kollors/deep-json-server 1.0.0-alpha.4 → 1.0.0-alpha.6
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 +91 -39
- package/README.ru.md +91 -39
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/src/auth/contract.d.ts +26 -0
- package/dist/src/auth/contract.js +5 -0
- package/dist/src/auth/contract.js.map +1 -0
- package/dist/src/auth/openapi.d.ts +10 -0
- package/dist/src/auth/openapi.js +62 -0
- package/dist/src/auth/openapi.js.map +1 -0
- package/dist/src/auth/password.d.ts +4 -0
- package/dist/src/auth/password.js +25 -0
- package/dist/src/auth/password.js.map +1 -0
- package/dist/src/auth/public.d.ts +3 -0
- package/dist/src/auth/public.js +5 -0
- package/dist/src/auth/public.js.map +1 -0
- package/dist/src/auth/routes.d.ts +3 -0
- package/dist/src/auth/routes.js +12 -0
- package/dist/src/auth/routes.js.map +1 -0
- package/dist/src/auth/service.d.ts +18 -0
- package/dist/src/auth/service.js +112 -0
- package/dist/src/auth/service.js.map +1 -0
- package/dist/src/cli.js +4 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/config.d.ts +5 -0
- package/dist/src/config.js +24 -1
- package/dist/src/config.js.map +1 -1
- package/dist/src/constants.d.ts +1 -1
- package/dist/src/constants.js +1 -1
- package/dist/src/engine.d.ts +0 -1
- package/dist/src/engine.js +2 -3
- package/dist/src/engine.js.map +1 -1
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/features.d.ts +1 -0
- package/dist/src/features.js +10 -3
- package/dist/src/features.js.map +1 -1
- package/dist/src/graphql/preflight.js +9 -0
- package/dist/src/graphql/preflight.js.map +1 -1
- package/dist/src/graphql/resolvers.d.ts +2 -1
- package/dist/src/graphql/resolvers.js +1 -1
- package/dist/src/graphql/resolvers.js.map +1 -1
- package/dist/src/graphql.js +8 -2
- package/dist/src/graphql.js.map +1 -1
- package/dist/src/http/errors.js +1 -1
- package/dist/src/http/errors.js.map +1 -1
- package/dist/src/model.d.ts +4 -0
- package/dist/src/model.js +26 -4
- package/dist/src/model.js.map +1 -1
- package/dist/src/mutations/write.d.ts +2 -1
- package/dist/src/mutations/write.js +22 -18
- package/dist/src/mutations/write.js.map +1 -1
- package/dist/src/openapi/document.d.ts +2 -1
- package/dist/src/openapi/document.js +51 -44
- package/dist/src/openapi/document.js.map +1 -1
- package/dist/src/openapi/public.d.ts +1 -0
- package/dist/src/openapi/public.js +3 -1
- package/dist/src/openapi/public.js.map +1 -1
- package/dist/src/paths.d.ts +1 -0
- package/dist/src/paths.js +2 -1
- package/dist/src/paths.js.map +1 -1
- package/dist/src/records.js +3 -5
- package/dist/src/records.js.map +1 -1
- package/dist/src/rest/options.d.ts +5 -6
- package/dist/src/rest/options.js +33 -55
- package/dist/src/rest/options.js.map +1 -1
- package/dist/src/rest/projection.d.ts +5 -4
- package/dist/src/rest/projection.js +35 -18
- package/dist/src/rest/projection.js.map +1 -1
- package/dist/src/rest/routes.js +8 -8
- package/dist/src/rest/routes.js.map +1 -1
- package/dist/src/server/public.d.ts +1 -1
- package/dist/src/server.js +12 -2
- package/dist/src/server.js.map +1 -1
- package/dist/src/types.d.ts +1 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
A JSON-backed mock server with REST, GraphQL, nested queries, binary files and schema exports. Requires Node.js 22 or newer.
|
|
6
6
|
|
|
7
|
-
**1.0.0-alpha.
|
|
7
|
+
**1.0.0-alpha.6 is a prerelease.** REST queries use `scope=[fields, arguments?]` at every level. When upgrading from an earlier version, update query parameters using the examples below; upgrading from 0.x also requires the new model schema.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ A JSON-backed mock server with REST, GraphQL, nested queries, binary files and s
|
|
|
12
12
|
npm install @kollors/deep-json-server@alpha
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
To install a specific version, use `@1.0.0-alpha.
|
|
15
|
+
To install a specific version, use `@1.0.0-alpha.6`.
|
|
16
16
|
|
|
17
17
|
## Quick start
|
|
18
18
|
|
|
@@ -55,6 +55,9 @@ The user list is available at `http://127.0.0.1:4001/users`.
|
|
|
55
55
|
| `graphql.enabled` | Enable GraphQL HTTP endpoint; default `false` |
|
|
56
56
|
| `graphql.endpoint` | Endpoint path; default `/graphql` |
|
|
57
57
|
| `graphql.path` | GraphQL SDL export destination |
|
|
58
|
+
| `auth.enabled` | Enable authentication; default `false` |
|
|
59
|
+
| `auth.users` | Path to a JSON array of auth users, or an in-memory array |
|
|
60
|
+
| `auth.expiresIn` | Session lifetime in seconds; default 3600 |
|
|
58
61
|
| `server.host`, `server.port` | Defaults `127.0.0.1`, `4001`; CLI also reads `HOST`/`PORT` |
|
|
59
62
|
| `server.pageSize`, `server.maxPageSize` | Defaults 10 and 100; default size is capped by the maximum |
|
|
60
63
|
| `server.cors`, `server.logger` | Default `true`; logger also accepts Fastify logger options |
|
|
@@ -71,6 +74,7 @@ Set `server.port` to `0` to let the operating system choose an available port. T
|
|
|
71
74
|
| `--files` | Enable file routes |
|
|
72
75
|
| `--graphql` | Enable the GraphQL API |
|
|
73
76
|
| `--openapi` | Enable the OpenAPI endpoint |
|
|
77
|
+
| `--auth` | Enable authentication using `auth.users` |
|
|
74
78
|
| `--host <host>` | Server address |
|
|
75
79
|
| `--port <port>` | Server port |
|
|
76
80
|
| `--help`, `-h` | Show help |
|
|
@@ -96,7 +100,7 @@ export default {
|
|
|
96
100
|
};
|
|
97
101
|
```
|
|
98
102
|
|
|
99
|
-
Each format needs its own output file. The command rejects destinations that would overwrite the configuration, database, schema or file metadata.
|
|
103
|
+
Each format needs its own output file. The command rejects destinations that would overwrite the configuration, database, schema, auth users or file metadata.
|
|
100
104
|
|
|
101
105
|
## Model schema
|
|
102
106
|
|
|
@@ -136,7 +140,7 @@ Examples: [database](examples/database.json), [model schema](examples/schema.jso
|
|
|
136
140
|
|
|
137
141
|
Explicit schemas are strict: undeclared fields and collections are rejected, except storage keys inferred from relations. Existing data is validated on startup. Generation uses the model definitions.
|
|
138
142
|
|
|
139
|
-
Schemaless REST generates an `id` and preserves arbitrary JSON fields. Filters and individual field selections use identifier-style names; other fields are returned through `scope={"*":true}`. Fields with mixed value types can be read, but
|
|
143
|
+
Schemaless REST generates an `id` and preserves arbitrary JSON fields. Filters and individual field selections use identifier-style names; other fields are returned through `scope=[{"*":true}]`. Fields with mixed value types can be read, but filtering, ordering and paging heterogeneous lists require an explicit schema.
|
|
140
144
|
|
|
141
145
|
### Fields
|
|
142
146
|
|
|
@@ -288,51 +292,59 @@ mutation {
|
|
|
288
292
|
|
|
289
293
|
### REST query parameters
|
|
290
294
|
|
|
291
|
-
|
|
295
|
+
REST accepts one query parameter, `scope`, containing a JSON array `[fields, arguments?]`. The first object selects fields; the optional second object supplies `where`, `order` and `pager` for a list. The same format applies to the root query, embedded objects and relations.
|
|
292
296
|
|
|
293
|
-
|
|
294
|
-
GET /users?where={"fullName":{"contains":"Мира"}}&order=[{"field":"fullName","direction":"ASC"}]&pager={"page":1,"pageSize":20}
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
Construct encoded URLs with `URLSearchParams`:
|
|
297
|
+
Select users and their movies with independent ordering and pagination:
|
|
298
298
|
|
|
299
299
|
```js
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
300
|
+
const scope = [
|
|
301
|
+
{
|
|
302
|
+
id: true,
|
|
303
|
+
fullName: true,
|
|
304
|
+
movies: [
|
|
305
|
+
{ id: true, title: true },
|
|
306
|
+
{
|
|
307
|
+
order: [{ field: 'title', direction: 'ASC' }],
|
|
308
|
+
pager: { page: 1, pageSize: 5 },
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
where: { fullName: { contains: 'Мира' } },
|
|
314
|
+
order: [{ field: 'fullName', direction: 'ASC' }],
|
|
315
|
+
pager: { page: 1, pageSize: 20 },
|
|
316
|
+
},
|
|
317
|
+
];
|
|
318
|
+
const params = new URLSearchParams({ scope: JSON.stringify(scope) });
|
|
304
319
|
const response = await fetch(`/users?${params}`);
|
|
305
320
|
```
|
|
306
321
|
|
|
307
|
-
`scope`
|
|
322
|
+
Select ordinary fields with `true` and objects or relations with their own scope arrays. Without arguments, the array contains only the fields object. `"*": true` includes own fields and stored keys, except `writeOnly` fields; select relations explicitly.
|
|
323
|
+
|
|
324
|
+
For example, select a movie's own fields, its actors' users and sorted genres:
|
|
308
325
|
|
|
309
326
|
```json
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
"
|
|
314
|
-
|
|
327
|
+
[
|
|
328
|
+
{
|
|
329
|
+
"*": true,
|
|
330
|
+
"actors": [
|
|
331
|
+
{
|
|
332
|
+
"user": [{ "id": true, "fullName": true }],
|
|
333
|
+
"genres": [
|
|
334
|
+
{ "*": true },
|
|
335
|
+
{ "order": [{ "field": "name", "direction": "ASC" }] }
|
|
336
|
+
]
|
|
337
|
+
}
|
|
338
|
+
]
|
|
315
339
|
}
|
|
316
|
-
|
|
340
|
+
]
|
|
317
341
|
```
|
|
318
342
|
|
|
319
|
-
`
|
|
343
|
+
Omitting `scope` returns own fields, as with `[{"*":true}]`. An empty selection `[{}]` returns an object without fields. Lists retain the `{ data, total }` response structure.
|
|
320
344
|
|
|
321
|
-
|
|
345
|
+
Arguments are available only on lists. Single-record queries and mutation responses can set arguments on their embedded lists. Parameters are validated even on empty data; an invalid response selection rolls back record changes. Invalid scopes return `400`. The JSON length limit is 10,000 characters; selection depth is limited to 32 levels.
|
|
322
346
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
```json
|
|
326
|
-
{
|
|
327
|
-
"actors": { "pager": { "pageSize": 5 } },
|
|
328
|
-
"actors.genres": {
|
|
329
|
-
"where": { "id": { "in": ["2", "3"] } },
|
|
330
|
-
"order": [{ "field": "name", "direction": "ASC" }]
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
A path in `nested` must be selected by `scope` and point to an object list. Single-record routes and mutations accept `scope` and `nested`; root `where`, `order` and `pager` apply to collection GET. Invalid names and unsafe paths return `400`.
|
|
347
|
+
OpenAPI remains at version 3.0.3. It cannot define a separate schema for each array position: the documentation describes the elements, and the server strictly validates their order.
|
|
336
348
|
|
|
337
349
|
### GraphQL
|
|
338
350
|
|
|
@@ -526,10 +538,48 @@ Content-Type: application/json
|
|
|
526
538
|
|
|
527
539
|
In disk mode, the binary is stored at `<files.directory>/<directory>/<name>`. Metadata stores `directory`, `mimeType` and `name`; the server reads the size from the file and builds its URLs. Directories and the metadata file are created when needed.
|
|
528
540
|
|
|
529
|
-
Use one server process per disk database and file store. Stop it before editing stored files or metadata manually. Storage paths cannot contain symbolic links. Uploads and renames cannot overwrite the database, counters, schema, loaded configuration or metadata file.
|
|
541
|
+
Use one server process per disk database and file store. Stop it before editing stored files or metadata manually. Storage paths cannot contain symbolic links. Uploads and renames cannot overwrite the database, counters, schema, auth users, loaded configuration or metadata file.
|
|
530
542
|
|
|
531
543
|
Send the file as a binary request body. In a browser, use `xhr.send(file)` and track progress through `XMLHttpRequest.upload.onprogress`. The default maximum size is 100 MiB and can be changed through `server.maxFileSize`. Missing or unsafe headers and paths return `400`, an exceeded limit returns `413`, and a missing, malformed, or Fastify-unsupported `Content-Type` returns `400` or `415`, depending on which validation stage rejects it.
|
|
532
544
|
|
|
545
|
+
## Authentication
|
|
546
|
+
|
|
547
|
+
The optional auth module provides REST routes for login, current user and logout. **It does not restrict access to REST records, files or GraphQL.**
|
|
548
|
+
|
|
549
|
+
Create an auth user in a separate file with `setup-auth.mjs`:
|
|
550
|
+
|
|
551
|
+
```js
|
|
552
|
+
import { writeFile } from 'node:fs/promises';
|
|
553
|
+
import { hashPassword } from '@kollors/deep-json-server/auth';
|
|
554
|
+
|
|
555
|
+
const password = process.env.DJS_PASSWORD;
|
|
556
|
+
if (!password) throw new Error('Set DJS_PASSWORD');
|
|
557
|
+
await writeFile('./auth.json', JSON.stringify([
|
|
558
|
+
{ id: '1', username: 'admin', passwordHash: await hashPassword(password) },
|
|
559
|
+
], null, 2), { flag: 'wx', mode: 0o600 });
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Set `DJS_PASSWORD` and run `node setup-auth.mjs`. Add the file to your server configuration:
|
|
563
|
+
|
|
564
|
+
```js
|
|
565
|
+
export default {
|
|
566
|
+
database: { path: './database.json' },
|
|
567
|
+
auth: { users: './auth.json', expiresIn: 3600 },
|
|
568
|
+
};
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
Start with `npx deep-json-server --auth server.config.js`, or set `auth.enabled: true`. Each user needs a unique string `id`, a unique `username` and a `passwordHash` created by the helper. Passwords use salted scrypt hashes. The file is read at startup; restart the server after editing it.
|
|
572
|
+
|
|
573
|
+
| REST request | Input | Response |
|
|
574
|
+
|---|---|---|
|
|
575
|
+
| `POST /auth/login` | JSON `{ "username": "admin", "password": "…" }` | `{ accessToken, expiresIn, user: { id, username } }` |
|
|
576
|
+
| `GET /auth/me` | Bearer token | `{ id, username }` |
|
|
577
|
+
| `POST /auth/logout` | Bearer token | `{ success: true }` |
|
|
578
|
+
|
|
579
|
+
Pass the token in `Authorization: Bearer <accessToken>`. Incorrect credentials and invalid or expired tokens return HTTP 401. Sessions are kept in memory and disappear on restart; logout revokes the supplied token. Login can return HTTP 429 when the server has too many concurrent login attempts or active sessions.
|
|
580
|
+
|
|
581
|
+
OpenAPI includes these REST operations and a Bearer security scheme for `/auth/me` and `/auth/logout`. In Swagger UI, paste a token from login into **Authorize**. For schema exports, enable auth in the configuration or use `generate openapi --auth server.config.js`; the users file is not read during generation. GraphQL and OpenAPI still require `database.schema`.
|
|
582
|
+
|
|
533
583
|
## Programmatic API
|
|
534
584
|
|
|
535
585
|
```js
|
|
@@ -542,7 +592,7 @@ await server.listen();
|
|
|
542
592
|
// await server.close();
|
|
543
593
|
```
|
|
544
594
|
|
|
545
|
-
The `openapi()` and `graphql()` methods return schemas and require `database.schema`. `fastify()` returns the server instance for configuration and startup. The database and enabled services initialize on `ready()`, `listen()` or the first `inject()`; initialization errors stop startup. Override server features with `createServer(config, { files: false, graphql: true, openapi: true })`.
|
|
595
|
+
The `openapi()` and `graphql()` methods return schemas and require `database.schema`. `fastify()` returns the server instance for configuration and startup. The database and enabled services initialize on `ready()`, `listen()` or the first `inject()`; initialization errors stop startup. Override server features with `createServer(config, { files: false, graphql: true, openapi: true, auth: true })`.
|
|
546
596
|
|
|
547
597
|
The root import `@kollors/deep-json-server` also provides these functions. Server adapters load when enabled. Generators can be used independently:
|
|
548
598
|
|
|
@@ -556,13 +606,15 @@ await writeOpenapi(document, './generated/openapi.yaml');
|
|
|
556
606
|
await writeGraphql(sdl, './generated/schema.graphql');
|
|
557
607
|
```
|
|
558
608
|
|
|
609
|
+
`generateOpenapi()` accepts `{ auth: true }` to include auth operations. `hashPassword()` is also available from the root package.
|
|
610
|
+
|
|
559
611
|
`generateOpenapi()` also accepts `host`, `port`, `pageSize`, `maxPageSize` and `info`. Pass a schema object instead of a path if preferred. Servers and generators use their own copy of the model. Pagination sizes must be positive integers; `pageSize` cannot exceed `maxPageSize`.
|
|
560
612
|
|
|
561
613
|
## Storage and development
|
|
562
614
|
|
|
563
615
|
Updates run sequentially within one server instance and are validated on a copy of the data before saving. Use one server process per database file. `increment` counters are stored next to the database in `<database path>.counters.json`; keep that file with the database. Numbers are reserved before the data write, so a failed write can leave gaps but cannot reuse a reserved number.
|
|
564
616
|
|
|
565
|
-
The server is intended for mocking APIs.
|
|
617
|
+
The server is intended for mocking APIs. Access rules for application data remain the responsibility of your application.
|
|
566
618
|
|
|
567
619
|
```sh
|
|
568
620
|
npm ci
|
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.6 — предварительная версия.** REST-запросы используют `scope=[поля, аргументы?]` на всех уровнях. При обновлении измените параметры запросов по примерам ниже; для перехода с 0.x также нужна новая схема моделей.
|
|
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.6`.
|
|
16
16
|
|
|
17
17
|
## Быстрый старт
|
|
18
18
|
|
|
@@ -55,6 +55,9 @@ npx deep-json-server server.config.js
|
|
|
55
55
|
| `graphql.enabled` | Включить GraphQL HTTP API; по умолчанию `false` |
|
|
56
56
|
| `graphql.endpoint` | Путь GraphQL; по умолчанию `/graphql` |
|
|
57
57
|
| `graphql.path` | Путь экспорта GraphQL SDL |
|
|
58
|
+
| `auth.enabled` | Включить аутентификацию; по умолчанию `false` |
|
|
59
|
+
| `auth.users` | Путь к JSON-массиву учётных записей или массив в памяти |
|
|
60
|
+
| `auth.expiresIn` | Срок действия сессии в секундах; по умолчанию 3600 |
|
|
58
61
|
| `server.host`, `server.port` | По умолчанию `127.0.0.1`, `4001`; CLI также читает `HOST`/`PORT` |
|
|
59
62
|
| `server.pageSize`, `server.maxPageSize` | По умолчанию 10 и 100; размер по умолчанию ограничен максимумом |
|
|
60
63
|
| `server.cors`, `server.logger` | По умолчанию `true`; logger также принимает настройки Fastify |
|
|
@@ -71,6 +74,7 @@ npx deep-json-server server.config.js
|
|
|
71
74
|
| `--files` | Включить файловые маршруты |
|
|
72
75
|
| `--graphql` | Включить GraphQL API |
|
|
73
76
|
| `--openapi` | Включить HTTP-маршрут OpenAPI |
|
|
77
|
+
| `--auth` | Включить аутентификацию с учётными записями из `auth.users` |
|
|
74
78
|
| `--host <host>` | Адрес сервера |
|
|
75
79
|
| `--port <port>` | Порт сервера |
|
|
76
80
|
| `--help`, `-h` | Справка |
|
|
@@ -96,7 +100,7 @@ export default {
|
|
|
96
100
|
};
|
|
97
101
|
```
|
|
98
102
|
|
|
99
|
-
Для каждого формата нужен отдельный файл. Команда отклонит путь, который перезапишет конфигурацию, базу,
|
|
103
|
+
Для каждого формата нужен отдельный файл. Команда отклонит путь, который перезапишет конфигурацию, базу, схему, учётные записи auth или метаданные файлов.
|
|
100
104
|
|
|
101
105
|
## Схема моделей
|
|
102
106
|
|
|
@@ -136,7 +140,7 @@ export default {
|
|
|
136
140
|
|
|
137
141
|
При явной схеме неизвестные поля и коллекции запрещены. Исключение — хранимые ключи, выведенные из связей. Исходная база проверяется при загрузке. Генерация использует описание моделей.
|
|
138
142
|
|
|
139
|
-
REST без схемы создаёт ключ `id` и сохраняет произвольные JSON-поля. Для фильтров и выбора отдельных полей используются имена в формате идентификаторов; остальные поля возвращаются через `scope={"*":true}`. Поля с разными типами значений можно читать, но
|
|
143
|
+
REST без схемы создаёт ключ `id` и сохраняет произвольные JSON-поля. Для фильтров и выбора отдельных полей используются имена в формате идентификаторов; остальные поля возвращаются через `scope=[{"*":true}]`. Поля с разными типами значений можно читать, но фильтрация, сортировка и пагинация неоднородных списков требуют явной схемы.
|
|
140
144
|
|
|
141
145
|
### Поля
|
|
142
146
|
|
|
@@ -288,51 +292,59 @@ mutation {
|
|
|
288
292
|
|
|
289
293
|
### Параметры REST-запросов
|
|
290
294
|
|
|
291
|
-
|
|
295
|
+
REST принимает один query-параметр `scope` с JSON-массивом `[поля, аргументы?]`. Первый объект выбирает поля, второй задаёт `where`, `order` и `pager` для списка. Этот формат одинаков для корневого запроса, вложенных объектов и связей.
|
|
292
296
|
|
|
293
|
-
|
|
294
|
-
GET /users?where={"fullName":{"contains":"Мира"}}&order=[{"field":"fullName","direction":"ASC"}]&pager={"page":1,"pageSize":20}
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
Кодирование через `URLSearchParams`:
|
|
297
|
+
Пример выбора пользователей и их фильмов с отдельной сортировкой и пагинацией:
|
|
298
298
|
|
|
299
299
|
```js
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
300
|
+
const scope = [
|
|
301
|
+
{
|
|
302
|
+
id: true,
|
|
303
|
+
fullName: true,
|
|
304
|
+
movies: [
|
|
305
|
+
{ id: true, title: true },
|
|
306
|
+
{
|
|
307
|
+
order: [{ field: 'title', direction: 'ASC' }],
|
|
308
|
+
pager: { page: 1, pageSize: 5 },
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
where: { fullName: { contains: 'Мира' } },
|
|
314
|
+
order: [{ field: 'fullName', direction: 'ASC' }],
|
|
315
|
+
pager: { page: 1, pageSize: 20 },
|
|
316
|
+
},
|
|
317
|
+
];
|
|
318
|
+
const params = new URLSearchParams({ scope: JSON.stringify(scope) });
|
|
304
319
|
const response = await fetch(`/users?${params}`);
|
|
305
320
|
```
|
|
306
321
|
|
|
307
|
-
`scope`
|
|
322
|
+
Обычные поля выбираются через `true`, объекты и связи — через свой массив `scope`. Если аргументы не нужны, в массиве остаётся только объект полей. `"*": true` включает собственные поля и хранимые ключи, кроме `writeOnly`; связи выбираются явно.
|
|
323
|
+
|
|
324
|
+
Например, собственные поля фильма, пользователи актёров и отсортированные жанры:
|
|
308
325
|
|
|
309
326
|
```json
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
"
|
|
314
|
-
|
|
327
|
+
[
|
|
328
|
+
{
|
|
329
|
+
"*": true,
|
|
330
|
+
"actors": [
|
|
331
|
+
{
|
|
332
|
+
"user": [{ "id": true, "fullName": true }],
|
|
333
|
+
"genres": [
|
|
334
|
+
{ "*": true },
|
|
335
|
+
{ "order": [{ "field": "name", "direction": "ASC" }] }
|
|
336
|
+
]
|
|
337
|
+
}
|
|
338
|
+
]
|
|
315
339
|
}
|
|
316
|
-
|
|
340
|
+
]
|
|
317
341
|
```
|
|
318
342
|
|
|
319
|
-
`
|
|
343
|
+
Без `scope` возвращаются собственные поля, как при `[{"*":true}]`. Пустой выбор `[{}]` возвращает объект без полей. Списки сохраняют структуру `{ data, total }`.
|
|
320
344
|
|
|
321
|
-
|
|
345
|
+
Аргументы доступны только у списков. В запросе отдельной записи и в ответе мутации их можно задать для вложенных списков. Параметры проверяются даже на пустых данных; ошибка в выборе ответа отменяет изменения записи. Некорректный `scope` возвращает `400`. Максимальная длина JSON — 10 000 символов, глубина выбора — 32 уровня.
|
|
322
346
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
```json
|
|
326
|
-
{
|
|
327
|
-
"actors": { "pager": { "pageSize": 5 } },
|
|
328
|
-
"actors.genres": {
|
|
329
|
-
"where": { "id": { "in": ["2", "3"] } },
|
|
330
|
-
"order": [{ "field": "name", "direction": "ASC" }]
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
Путь в `nested` должен быть выбран через `scope` и вести к списку объектов. Маршруты отдельных записей и мутации принимают `scope` и `nested`; корневые `where`, `order` и `pager` применяются к GET коллекции. Неизвестные имена и небезопасные пути возвращают `400`.
|
|
347
|
+
OpenAPI остаётся версии 3.0.3. В этой версии нельзя задать отдельную схему для каждой позиции массива: назначение элементов описано в документации, а их порядок строго проверяет сервер.
|
|
336
348
|
|
|
337
349
|
### GraphQL
|
|
338
350
|
|
|
@@ -526,10 +538,48 @@ Content-Type: application/json
|
|
|
526
538
|
|
|
527
539
|
При хранении на диске бинарный файл находится по пути `<files.directory>/<directory>/<name>`. Метаданные содержат `directory`, `mimeType` и `name`; размер сервер читает из файла, а URL формирует сам. Директории и файл метаданных создаются по мере необходимости.
|
|
528
540
|
|
|
529
|
-
Используйте один процесс сервера для дисковой базы и файлового хранилища. Перед ручным изменением файлов или метаданных остановите его. Пути в хранилище не могут содержать символические ссылки. Загрузка и переименование не могут перезаписать базу, счётчики, схему, загруженную конфигурацию или файл метаданных.
|
|
541
|
+
Используйте один процесс сервера для дисковой базы и файлового хранилища. Перед ручным изменением файлов или метаданных остановите его. Пути в хранилище не могут содержать символические ссылки. Загрузка и переименование не могут перезаписать базу, счётчики, схему, учётные записи auth, загруженную конфигурацию или файл метаданных.
|
|
530
542
|
|
|
531
543
|
Отправляйте файл как бинарное тело запроса. В браузере для этого можно использовать `xhr.send(file)`, а прогресс отслеживать через `XMLHttpRequest.upload.onprogress`. Максимальный размер по умолчанию равен 100 МиБ и настраивается через `server.maxFileSize`. Отсутствующие или небезопасные заголовки и пути возвращают `400`, превышение лимита — `413`, а отсутствующий, некорректный или не поддерживаемый Fastify `Content-Type` — `400` либо `415` в зависимости от этапа проверки.
|
|
532
544
|
|
|
545
|
+
## Аутентификация
|
|
546
|
+
|
|
547
|
+
Модуль auth добавляет REST-маршруты входа, получения текущего пользователя и выхода. **Он не ограничивает доступ к REST-записям, файлам и GraphQL.**
|
|
548
|
+
|
|
549
|
+
Создайте учётную запись в отдельном файле с помощью `setup-auth.mjs`:
|
|
550
|
+
|
|
551
|
+
```js
|
|
552
|
+
import { writeFile } from 'node:fs/promises';
|
|
553
|
+
import { hashPassword } from '@kollors/deep-json-server/auth';
|
|
554
|
+
|
|
555
|
+
const password = process.env.DJS_PASSWORD;
|
|
556
|
+
if (!password) throw new Error('Set DJS_PASSWORD');
|
|
557
|
+
await writeFile('./auth.json', JSON.stringify([
|
|
558
|
+
{ id: '1', username: 'admin', passwordHash: await hashPassword(password) },
|
|
559
|
+
], null, 2), { flag: 'wx', mode: 0o600 });
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Задайте `DJS_PASSWORD` и выполните `node setup-auth.mjs`. Добавьте файл в конфигурацию сервера:
|
|
563
|
+
|
|
564
|
+
```js
|
|
565
|
+
export default {
|
|
566
|
+
database: { path: './database.json' },
|
|
567
|
+
auth: { users: './auth.json', expiresIn: 3600 },
|
|
568
|
+
};
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
Запустите `npx deep-json-server --auth server.config.js` или задайте `auth.enabled: true`. Каждой записи нужны уникальный строковый `id`, уникальный `username` и `passwordHash`, созданный функцией выше. Пароли хешируются через scrypt со случайной солью. Файл читается при запуске; после его изменения перезапустите сервер.
|
|
572
|
+
|
|
573
|
+
| REST-запрос | Входные данные | Ответ |
|
|
574
|
+
|---|---|---|
|
|
575
|
+
| `POST /auth/login` | JSON `{ "username": "admin", "password": "…" }` | `{ accessToken, expiresIn, user: { id, username } }` |
|
|
576
|
+
| `GET /auth/me` | Bearer-токен | `{ id, username }` |
|
|
577
|
+
| `POST /auth/logout` | Bearer-токен | `{ success: true }` |
|
|
578
|
+
|
|
579
|
+
Передавайте токен в заголовке `Authorization: Bearer <accessToken>`. Неверные учётные данные, недействительный или истёкший токен возвращают HTTP 401. Сессии хранятся в памяти и исчезают при перезапуске; выход отзывает переданный токен. Вход может вернуть HTTP 429 при слишком большом числе одновременных попыток или активных сессий.
|
|
580
|
+
|
|
581
|
+
В OpenAPI добавляются эти REST-операции и Bearer-схема для `/auth/me` и `/auth/logout`. В Swagger UI токен из ответа на вход можно вставить в **Authorize**. Для экспорта схем включите auth в конфигурации или используйте `generate openapi --auth server.config.js`; файл учётных записей при генерации не читается. Для GraphQL и OpenAPI по-прежнему нужна `database.schema`.
|
|
582
|
+
|
|
533
583
|
## Программный API
|
|
534
584
|
|
|
535
585
|
```js
|
|
@@ -542,7 +592,7 @@ await server.listen();
|
|
|
542
592
|
// await server.close();
|
|
543
593
|
```
|
|
544
594
|
|
|
545
|
-
Методы `openapi()` и `graphql()` возвращают схемы и требуют `database.schema`. `fastify()` возвращает экземпляр сервера для настройки и запуска. База и включённые сервисы инициализируются при `ready()`, `listen()` или первом `inject()`; ошибка инициализации останавливает запуск. Возможности сервера можно переопределить через `createServer(config, { files: false, graphql: true, openapi: true })`.
|
|
595
|
+
Методы `openapi()` и `graphql()` возвращают схемы и требуют `database.schema`. `fastify()` возвращает экземпляр сервера для настройки и запуска. База и включённые сервисы инициализируются при `ready()`, `listen()` или первом `inject()`; ошибка инициализации останавливает запуск. Возможности сервера можно переопределить через `createServer(config, { files: false, graphql: true, openapi: true, auth: true })`.
|
|
546
596
|
|
|
547
597
|
Эти функции доступны и через общий импорт `@kollors/deep-json-server`. Адаптеры сервера загружаются при включении. Генераторы можно использовать отдельно:
|
|
548
598
|
|
|
@@ -556,13 +606,15 @@ await writeOpenapi(document, './generated/openapi.yaml');
|
|
|
556
606
|
await writeGraphql(sdl, './generated/schema.graphql');
|
|
557
607
|
```
|
|
558
608
|
|
|
609
|
+
`generateOpenapi()` принимает `{ auth: true }`, чтобы добавить операции auth. Функция `hashPassword()` доступна и через общий импорт пакета.
|
|
610
|
+
|
|
559
611
|
`generateOpenapi()` также принимает `host`, `port`, `pageSize`, `maxPageSize` и `info`. Вместо пути можно передать объект схемы. Сервер и генераторы работают с собственной копией модели. Размеры страниц должны быть положительными целыми числами; `pageSize` не может превышать `maxPageSize`.
|
|
560
612
|
|
|
561
613
|
## Хранение и разработка
|
|
562
614
|
|
|
563
615
|
Изменения выполняются последовательно внутри экземпляра сервера и проверяются на копии данных до сохранения. Для одного файла базы используйте один процесс сервера. Счётчики `increment` хранятся рядом с базой в `<путь базы>.counters.json`; сохраняйте этот файл вместе с базой. Номера резервируются до записи данных: сбой может оставить пропуск, но не приводит к повторной выдаче номера.
|
|
564
616
|
|
|
565
|
-
Сервер предназначен для имитации API.
|
|
617
|
+
Сервер предназначен для имитации API. Правила доступа к данным задаются в вашем приложении.
|
|
566
618
|
|
|
567
619
|
```sh
|
|
568
620
|
npm ci
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type { AuthConfig, AuthSession, AuthUser, AuthUserRecord } from './src/auth/contract.js';
|
|
2
|
+
export { hashPassword } from './src/auth/public.js';
|
|
1
3
|
export type { DatabaseConfig, DeepJsonServerConfig, FilesConfig, GraphqlConfig, MemoryFile, OpenapiConfig, ServerConfig } from './src/config.js';
|
|
2
4
|
export type { ServerFeatures } from './src/features.js';
|
|
3
5
|
export type { FileUpdate } from './src/files/contract.js';
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAOpD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/F,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface AuthUser {
|
|
2
|
+
id: string;
|
|
3
|
+
username: string;
|
|
4
|
+
}
|
|
5
|
+
export interface AuthUserRecord extends AuthUser {
|
|
6
|
+
passwordHash: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AuthConfig {
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
users: string | AuthUserRecord[];
|
|
11
|
+
/** Session lifetime in seconds. Defaults to one hour. */
|
|
12
|
+
expiresIn?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface AuthSession {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
expiresIn: number;
|
|
17
|
+
user: AuthUser;
|
|
18
|
+
}
|
|
19
|
+
export declare const AUTH_PATHS: {
|
|
20
|
+
readonly login: '/auth/login';
|
|
21
|
+
readonly me: '/auth/me';
|
|
22
|
+
readonly logout: '/auth/logout';
|
|
23
|
+
};
|
|
24
|
+
export declare const DEFAULT_SESSION_SECONDS = 3600;
|
|
25
|
+
export declare const MAX_USERNAME_LENGTH = 256;
|
|
26
|
+
export declare const MAX_PASSWORD_LENGTH = 1024;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../src/auth/contract.ts"],"names":[],"mappings":"AAkBA,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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { OpenapiDocument, OpenapiSchema } from '../types.js';
|
|
2
|
+
export declare const AUTH_SCHEMAS: Record<string, OpenapiSchema>;
|
|
3
|
+
export declare const AUTH_SECURITY_SCHEMES: {
|
|
4
|
+
AuthBearer: {
|
|
5
|
+
type: string;
|
|
6
|
+
scheme: string;
|
|
7
|
+
description: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export declare function authOpenapiPaths(): OpenapiDocument['paths'];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { json, ref, response } from '../openapi/helpers.js';
|
|
2
|
+
import { AUTH_PATHS, MAX_PASSWORD_LENGTH, MAX_USERNAME_LENGTH } from './contract.js';
|
|
3
|
+
export const AUTH_SCHEMAS = {
|
|
4
|
+
AuthUser: { type: 'object', additionalProperties: false, required: ['id', 'username'], properties: { id: { type: 'string' }, username: { type: 'string' } } },
|
|
5
|
+
AuthLoginInput: {
|
|
6
|
+
type: 'object',
|
|
7
|
+
additionalProperties: false,
|
|
8
|
+
required: ['username', 'password'],
|
|
9
|
+
properties: {
|
|
10
|
+
username: { type: 'string', minLength: 1, maxLength: MAX_USERNAME_LENGTH },
|
|
11
|
+
password: { type: 'string', format: 'password', writeOnly: true, minLength: 1, maxLength: MAX_PASSWORD_LENGTH },
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
AuthSession: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
required: ['accessToken', 'expiresIn', 'user'],
|
|
18
|
+
properties: {
|
|
19
|
+
accessToken: { type: 'string' },
|
|
20
|
+
expiresIn: { type: 'integer', minimum: 1 },
|
|
21
|
+
user: ref('AuthUser'),
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
AuthLogoutResult: { type: 'object', additionalProperties: false, required: ['success'], properties: { success: { type: 'boolean' } } },
|
|
25
|
+
};
|
|
26
|
+
export const AUTH_SECURITY_SCHEMES = { AuthBearer: { type: 'http', scheme: 'bearer', description: 'Session token returned by POST /auth/login. Used only by GET /auth/me and POST /auth/logout.' } };
|
|
27
|
+
export function authOpenapiPaths() {
|
|
28
|
+
const unauthorized = response('Invalid credentials or expired session', ref('Error'));
|
|
29
|
+
return {
|
|
30
|
+
[AUTH_PATHS.login]: {
|
|
31
|
+
post: {
|
|
32
|
+
operationId: 'authLogin',
|
|
33
|
+
tags: ['auth'],
|
|
34
|
+
security: [],
|
|
35
|
+
requestBody: { required: true, ...json(ref('AuthLoginInput')) },
|
|
36
|
+
responses: {
|
|
37
|
+
200: response('Session created', ref('AuthSession')),
|
|
38
|
+
400: response('Invalid input', ref('Error')),
|
|
39
|
+
401: unauthorized,
|
|
40
|
+
429: response('Too many login attempts or sessions', ref('Error')),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
[AUTH_PATHS.me]: {
|
|
45
|
+
get: {
|
|
46
|
+
operationId: 'authMe',
|
|
47
|
+
tags: ['auth'],
|
|
48
|
+
security: [{ AuthBearer: [] }],
|
|
49
|
+
responses: { 200: response('Current auth user', ref('AuthUser')), 401: unauthorized },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
[AUTH_PATHS.logout]: {
|
|
53
|
+
post: {
|
|
54
|
+
operationId: 'authLogout',
|
|
55
|
+
tags: ['auth'],
|
|
56
|
+
security: [{ AuthBearer: [] }],
|
|
57
|
+
responses: { 200: response('Session revoked', ref('AuthLogoutResult')), 401: unauthorized },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=openapi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.js","sourceRoot":"","sources":["../../../src/auth/openapi.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const validPasswordHash: (value: unknown) => value is string;
|
|
2
|
+
export declare function hashPassword(password: string): Promise<string>;
|
|
3
|
+
export declare function verifyPassword(password: string, encoded: string): Promise<boolean>;
|
|
4
|
+
export declare const DUMMY_HASH: string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { MAX_PASSWORD_LENGTH } from './contract.js';
|
|
3
|
+
// OWASP scrypt profile: N=2^14, r=8, p=5 (16 MiB memory).
|
|
4
|
+
const PREFIX = 'scrypt$16384$8$5';
|
|
5
|
+
const HASH_PATTERN = /^scrypt\$16384\$8\$5\$([0-9a-f]{32})\$([0-9a-f]{64})$/;
|
|
6
|
+
export const validPasswordHash = (value) => typeof value === 'string' && HASH_PATTERN.test(value);
|
|
7
|
+
const derive = (password, salt) => new Promise((resolve, reject) => {
|
|
8
|
+
scrypt(password, salt, 32, { N: 16384, r: 8, p: 5 }, (error, key) => (error ? reject(error) : resolve(key)));
|
|
9
|
+
});
|
|
10
|
+
export async function hashPassword(password) {
|
|
11
|
+
if (typeof password !== 'string' || !password.length || password.length > MAX_PASSWORD_LENGTH)
|
|
12
|
+
throw new Error(`Password must contain 1..${MAX_PASSWORD_LENGTH} characters`);
|
|
13
|
+
const salt = randomBytes(16);
|
|
14
|
+
const hash = await derive(password, salt);
|
|
15
|
+
return `${PREFIX}$${salt.toString('hex')}$${hash.toString('hex')}`;
|
|
16
|
+
}
|
|
17
|
+
export async function verifyPassword(password, encoded) {
|
|
18
|
+
const parts = HASH_PATTERN.exec(encoded);
|
|
19
|
+
if (!parts)
|
|
20
|
+
return false;
|
|
21
|
+
const actual = await derive(password, Buffer.from(parts[1], 'hex'));
|
|
22
|
+
return timingSafeEqual(actual, Buffer.from(parts[2], 'hex'));
|
|
23
|
+
}
|
|
24
|
+
export const DUMMY_HASH = `${PREFIX}$${'0'.repeat(32)}$${'0'.repeat(64)}`;
|
|
25
|
+
//# sourceMappingURL=password.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.js","sourceRoot":"","sources":["../../../src/auth/password.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEpD,0DAA0D;AAC1D,MAAM,MAAM,GAAG,kBAAkB,CAAC;AAClC,MAAM,YAAY,GAAG,uDAAuD,CAAC;AAC7E,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAAc,EAAmB,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC5H,MAAM,MAAM,GAAG,CAAC,QAAgB,EAAE,IAAY,EAAmB,EAAE,CACjE,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;IAC9B,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC/G,CAAC,CAAC,CAAC;AACL,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,GAAG,mBAAmB;QAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,mBAAmB,aAAa,CAAC,CAAC;IAC7K,MAAM,IAAI,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC7B,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC1C,OAAO,GAAG,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;AACrE,CAAC;AACD,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,QAAgB,EAAE,OAAe;IACpE,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzC,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IACpE,OAAO,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;AAC/D,CAAC;AACD,MAAM,CAAC,MAAM,UAAU,GAAG,GAAG,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC"}
|