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