@kollors/deep-json-server 0.9.0 → 1.0.0-alpha.2
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 +313 -403
- package/README.ru.md +307 -397
- package/dist/index.d.ts +5 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/src/cli.js +95 -66
- package/dist/src/cli.js.map +1 -1
- package/dist/src/config.d.ts +18 -2
- package/dist/src/config.js +39 -10
- package/dist/src/config.js.map +1 -1
- package/dist/src/constants.d.ts +2 -1
- package/dist/src/constants.js +2 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/database.d.ts +4 -3
- package/dist/src/database.js +32 -19
- package/dist/src/database.js.map +1 -1
- package/dist/src/engine.d.ts +47 -0
- package/dist/src/engine.js +371 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/errors.d.ts +6 -0
- package/dist/src/errors.js +9 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/features.d.ts +8 -0
- package/dist/src/features.js +18 -0
- package/dist/src/features.js.map +1 -0
- package/dist/src/files/disk-store.js +17 -6
- package/dist/src/files/disk-store.js.map +1 -1
- package/dist/src/files/openapi.d.ts +3 -0
- package/dist/src/files/openapi.js +86 -0
- package/dist/src/files/openapi.js.map +1 -0
- package/dist/src/graphql/preflight.d.ts +3 -0
- package/dist/src/graphql/preflight.js +28 -0
- package/dist/src/graphql/preflight.js.map +1 -0
- package/dist/src/graphql/routes.d.ts +4 -0
- package/dist/src/graphql/routes.js +28 -0
- package/dist/src/graphql/routes.js.map +1 -0
- package/dist/src/graphql.d.ts +4 -0
- package/dist/src/graphql.js +227 -0
- package/dist/src/graphql.js.map +1 -0
- package/dist/src/model.d.ts +65 -0
- package/dist/src/model.js +345 -0
- package/dist/src/model.js.map +1 -0
- package/dist/src/openapi/document.d.ts +7 -7
- package/dist/src/openapi/document.js +237 -320
- package/dist/src/openapi/document.js.map +1 -1
- package/dist/src/openapi/index.d.ts +3 -6
- package/dist/src/openapi/index.js +2 -3
- package/dist/src/openapi/index.js.map +1 -1
- package/dist/src/query/contract.d.ts +3 -0
- package/dist/src/query/contract.js +19 -0
- package/dist/src/query/contract.js.map +1 -0
- package/dist/src/query/filter.d.ts +3 -5
- package/dist/src/query/filter.js +76 -240
- package/dist/src/query/filter.js.map +1 -1
- package/dist/src/query/options.d.ts +18 -0
- package/dist/src/query/options.js +30 -0
- package/dist/src/query/options.js.map +1 -0
- package/dist/src/rest/options.d.ts +14 -0
- package/dist/src/rest/options.js +108 -0
- package/dist/src/rest/options.js.map +1 -0
- package/dist/src/rest/projection.d.ts +6 -0
- package/dist/src/rest/projection.js +37 -0
- package/dist/src/rest/projection.js.map +1 -0
- package/dist/src/rest/routes.d.ts +3 -0
- package/dist/src/rest/routes.js +48 -0
- package/dist/src/rest/routes.js.map +1 -0
- package/dist/src/schema.d.ts +17 -0
- package/dist/src/schema.js +23 -0
- package/dist/src/schema.js.map +1 -0
- package/dist/src/server.d.ts +3 -5
- package/dist/src/server.js +76 -162
- package/dist/src/server.js.map +1 -1
- package/dist/src/types.d.ts +1 -3
- package/dist/src/utils.d.ts +0 -1
- package/dist/src/utils.js +0 -1
- package/dist/src/utils.js.map +1 -1
- package/package.json +9 -4
- 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/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/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/relations.d.ts +0 -12
- package/dist/src/relations.js +0 -155
- package/dist/src/relations.js.map +0 -1
package/README.ru.md
CHANGED
|
@@ -1,172 +1,336 @@
|
|
|
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
|
+
**1.0.0-alpha.2 — предварительная версия.** При переходе с 0.x обновите схему моделей и параметры запросов по примерам ниже.
|
|
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.2`.
|
|
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
34
|
export default {
|
|
33
|
-
database: {
|
|
34
|
-
path: 'mock/database.json',
|
|
35
|
-
},
|
|
35
|
+
database: { path: './database.json' },
|
|
36
36
|
};
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
```bash
|
|
39
|
+
```sh
|
|
42
40
|
npx deep-json-server server.config.js
|
|
43
41
|
```
|
|
44
42
|
|
|
45
|
-
|
|
43
|
+
Список пользователей доступен по адресу `http://127.0.0.1:4001/users`.
|
|
44
|
+
|
|
45
|
+
## Конфигурация
|
|
46
|
+
|
|
47
|
+
| Ключ | Назначение |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `database.path` / `database.data` | Ровно один: JSON-файл или объект коллекций в памяти |
|
|
50
|
+
| `database.schema` | Объект моделей или путь к JSON-файлу; необязателен для REST |
|
|
51
|
+
| `openapi.enabled` | Включить HTTP-маршрут спецификации; по умолчанию `false` |
|
|
52
|
+
| `openapi.endpoint` | Путь спецификации; по умолчанию `/openapi.json` |
|
|
53
|
+
| `openapi.path` | Путь экспорта YAML |
|
|
54
|
+
| `openapi.info` | Необязательные метаданные: `title`, `version`, `description` |
|
|
55
|
+
| `graphql.enabled` | Включить GraphQL HTTP API; по умолчанию `false` |
|
|
56
|
+
| `graphql.endpoint` | Путь GraphQL; по умолчанию `/graphql` |
|
|
57
|
+
| `graphql.path` | Путь экспорта GraphQL SDL |
|
|
58
|
+
| `server.host`, `server.port` | По умолчанию `127.0.0.1`, `4001`; CLI также читает `HOST`/`PORT` |
|
|
59
|
+
| `server.pageSize`, `server.maxPageSize` | По умолчанию 10 и 100; размер по умолчанию ограничен максимумом |
|
|
60
|
+
| `server.cors`, `server.logger` | По умолчанию `true`; logger также принимает настройки Fastify |
|
|
61
|
+
| `server.maxFileSize` | По умолчанию 100 МиБ |
|
|
62
|
+
| `files.data` | Бинарные файлы в памяти |
|
|
63
|
+
| `files.directory`, `files.metadata` | Каталог файлов и JSON метаданных; необходимы оба |
|
|
64
|
+
|
|
65
|
+
Относительные пути отсчитываются от каталога конфигурационного файла. При вызове `createServer()` с объектом конфигурации — от рабочего каталога. Сервер работает с копией переданных данных в памяти.
|
|
66
|
+
|
|
67
|
+
| Флаг CLI | Действие |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `--files` | Включить файловые маршруты |
|
|
70
|
+
| `--graphql` | Включить GraphQL API |
|
|
71
|
+
| `--openapi` | Включить HTTP-маршрут OpenAPI |
|
|
72
|
+
| `--host <host>` | Адрес сервера |
|
|
73
|
+
| `--port <port>` | Порт сервера |
|
|
74
|
+
| `--help`, `-h` | Справка |
|
|
75
|
+
| `--version`, `-v` | Версия пакета |
|
|
76
|
+
|
|
77
|
+
Приоритет настроек: CLI → конфигурация → `HOST`/`PORT` → значения по умолчанию. Файловые маршруты включаются при наличии секции `files`.
|
|
78
|
+
|
|
79
|
+
Для генерации укажите формат и файл конфигурации:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
npx deep-json-server generate openapi server.config.js
|
|
83
|
+
npx deep-json-server generate graphql server.config.js
|
|
84
|
+
npx deep-json-server generate openapi,graphql server.config.js
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Команда читает `database.schema` и сохраняет схемы по путям `openapi.path` и `graphql.path`. Данные базы и файловое хранилище для генерации не требуются.
|
|
88
|
+
|
|
89
|
+
## Схема моделей
|
|
90
|
+
|
|
91
|
+
Примеры: [база данных](examples/database.json), [схема моделей](examples/schema.json), [конфигурация](examples/server.config.js).
|
|
46
92
|
|
|
47
93
|
```json
|
|
48
94
|
{
|
|
49
|
-
"
|
|
50
|
-
|
|
95
|
+
"Country": {
|
|
96
|
+
"collection": "countries",
|
|
97
|
+
"api": ["openapi", "graphql"],
|
|
98
|
+
"fields": {
|
|
99
|
+
"id": { "type": "string", "primary": true, "generated": "uuid" },
|
|
100
|
+
"name": { "type": "string", "required": true },
|
|
101
|
+
"users": { "type": "User[]", "target": "countryId" }
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"User": {
|
|
105
|
+
"collection": "users",
|
|
106
|
+
"api": ["openapi", "graphql"],
|
|
107
|
+
"fields": {
|
|
108
|
+
"id": { "type": "string", "primary": true, "generated": "uuid" },
|
|
109
|
+
"fullName": { "type": "string", "required": true },
|
|
110
|
+
"country": { "type": "Country", "source": "countryId" }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
51
113
|
}
|
|
52
114
|
```
|
|
53
115
|
|
|
54
|
-
|
|
116
|
+
По умолчанию `api` содержит `["openapi", "graphql"]`. Пустой массив исключает модель из экспорта и GraphQL, но REST продолжает работать. Связанные модели должны разрешать тот же формат экспорта. Имена должны быть допустимыми идентификаторами; конфликты генерируемых типов и операций вызывают ошибку. Имена `and`, `or`, `not` зарезервированы фильтрами.
|
|
55
117
|
|
|
56
|
-
|
|
118
|
+
| Возможность | Со схемой | Без схемы |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| REST CRUD | Валидация модели | Только проверки JSON, тела запроса и идентификаторов |
|
|
121
|
+
| Связи | Поля схемы | По ключам `countryId`, `genreIds` и аналогичным |
|
|
122
|
+
| Экспорт OpenAPI 3.0.3 | Доступен | Ошибка при запросе экспорта |
|
|
123
|
+
| GraphQL SDL / API | Доступен | Ошибка при запросе |
|
|
57
124
|
|
|
58
|
-
|
|
59
|
-
import process from 'node:process';
|
|
125
|
+
При явной схеме неизвестные поля и коллекции запрещены. Исключение — хранимые ключи, выведенные из связей. Исходная база проверяется при загрузке. Генерация использует описание моделей. REST без схемы создаёт ключ `id` и сохраняет произвольные JSON-поля. Для фильтров и выбора отдельных полей используются имена в формате идентификаторов; остальные поля возвращаются через `scope=*`.
|
|
60
126
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
127
|
+
### Поля
|
|
128
|
+
|
|
129
|
+
Поле `type` принимает `string`, `number`, `boolean`, `object` или имя модели. Для массива добавьте суффикс `[]`: `string[]`, `object[]`, `Genre[]`. Вложенные поля описываются через точку, например `actors.fullName`.
|
|
130
|
+
|
|
131
|
+
| Свойства | Назначение |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `type` | Обязательный тип |
|
|
134
|
+
| `description`, `example` | Документация и пример |
|
|
135
|
+
| `required`, `nullable` | По умолчанию `false`; наличие поля и разрешение `null` независимы |
|
|
136
|
+
| `default` | Значение при пропуске в create/replace; PATCH не вставляет значения по умолчанию |
|
|
137
|
+
| `enum` | Допустимые значения; у массива — значения каждого элемента |
|
|
138
|
+
| `primary` | Корневой первичный ключ: обязательный, уникальный, неизменяемый, без `null` |
|
|
139
|
+
| `generated` | `uuid` для строк, `increment` для чисел; значение создаёт сервер |
|
|
140
|
+
| `readOnly`, `writeOnly` | Только ответ или только входные данные; взаимно исключаются |
|
|
141
|
+
| `minLength`, `maxLength`, `pattern` | Ограничения строк |
|
|
142
|
+
| `format` | `date`, `date-time`, `email`, `uri`, `uuid` |
|
|
143
|
+
| `minimum`, `maximum` | Включительные числовые границы |
|
|
144
|
+
| `source`, `target`, `onDelete` | Описание связи |
|
|
145
|
+
|
|
146
|
+
Ограничения строк и чисел применяются к каждому элементу `string[]`/`number[]`. `required` и `nullable` относятся ко всему массиву; `default` и `example` содержат массив целиком. Элементы массива должны соответствовать его типу и быть отличны от `null`. `required` требует наличия поля; обычный массив при этом может быть пустым.
|
|
147
|
+
|
|
148
|
+
У модели ровно один первичный ключ типа `string` или `number`, объявленный на верхнем уровне. Имя произвольное: `id`, `username`, `code`. Если `generated` не задан, значение передаёт клиент при создании. Генерируемые поля объявляются на верхнем уровне, исключаются из входных типов и несовместимы с `default`. При замене записи сохраняются генерируемые значения и поля `readOnly`, включая вложенные объекты. Для защиты полей внутри массива задайте `readOnly` всему массиву или содержащему его объекту. Объекты, состоящие из серверных полей, доступны только в ответах.
|
|
149
|
+
|
|
150
|
+
Например, `LocalUser` с первичным ключом `username` и полем `password: {"type":"string","required":true,"writeOnly":true}` получает запрос `localUser(username: ...)` и маршрут `/localUsers/{username}`. Поле `writeOnly` доступно для записи и исключено из ответов, `scope`, фильтров и сортировки.
|
|
151
|
+
|
|
152
|
+
### Связи
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
"actors.genres": {
|
|
156
|
+
"type": "Genre[]",
|
|
157
|
+
"source": "actors.genreIds",
|
|
158
|
+
"required": true
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`Genre` возвращает объект, `Genre[]` — список. По умолчанию `source` указывает на первичный ключ текущей модели, `target` — целевой. Это правило действует и для вложенных связей. Пути задаются от корня соответствующей записи: в примере `actors.genreIds` содержит ключи жанров текущего актёра.
|
|
163
|
+
|
|
164
|
+
Ключи связей хранятся в базе и входят в собственные поля записи. Их типы выводятся из сопоставляемых ключей. Если поле `source` ссылается на первичный ключ целевой модели, его можно не объявлять отдельно: для множественной связи создаётся описание массива ключей, для одиночной — одного значения. При неоднозначном сопоставлении опишите хранимое поле явно.
|
|
165
|
+
|
|
166
|
+
Обратная связь: `User.movies = {"type":"Movie[]","target":"actors.userId"}`. Фильм возвращается один раз, даже если совпало несколько актёров. Несколько совпадений для одиночной связи — ошибка.
|
|
167
|
+
|
|
168
|
+
Каждый переданный ключ прямой связи должен указывать на существующую запись. `required: true` у связи требует хотя бы одну связанную запись до фильтрации и пагинации ответа. Обратная связь с первичным ключом в `source` может быть пустой, если не объявлена обязательной. Отсутствующая одиночная связь возвращает `null`.
|
|
169
|
+
|
|
170
|
+
`onDelete` срабатывает **при удалении целевой записи**:
|
|
171
|
+
|
|
172
|
+
- `restrict` — по умолчанию: запретить удаление, пока на цель ссылается сохраняемая запись.
|
|
173
|
+
- `cascade` — удалить ссылающуюся запись. Для `User.country` удаление страны удаляет пользователей. Для `Movie.actors.user` удаление пользователя удаляет соответствующие элементы `actors`, сохраняя фильм.
|
|
174
|
+
|
|
175
|
+
Каскадное удаление выполняется целиком, включая циклические связи. Ошибка проверки отменяет всю операцию. Правила `onDelete` действуют и на явно объявленные обратные связи; при описании обоих направлений учитывайте оба правила.
|
|
176
|
+
|
|
177
|
+
## Запросы и ответы
|
|
178
|
+
|
|
179
|
+
Коллекции и списки объектов, включая вложенные `object[]`, возвращают:
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{ "data": [], "total": 0 }
|
|
82
183
|
```
|
|
83
184
|
|
|
84
|
-
|
|
185
|
+
Массивы примитивов возвращаются обычными массивами. Каждый список объектов принимает необязательные `where`, `order` и `pager`. Порядок обработки: фильтрация → сортировка → пагинация. `total` — число записей после фильтрации, до пагинации.
|
|
186
|
+
|
|
187
|
+
В `pager` задаются `page` и `pageSize`. По умолчанию возвращается первая страница с размером из `server.pageSize`. Оба значения должны быть положительными целыми числами; `pageSize` ограничен `server.maxPageSize`. За пределами списка возвращается пустой `data` с общим числом найденных записей в `total`.
|
|
188
|
+
|
|
189
|
+
Фильтры: `eq`, `ne`, `in`; для строк `contains`, `startsWith`, `endsWith`; сравнения `gt`, `gte`, `lt`, `lte`. Условия объединяются через `and`, `or`, `not`. У массивов есть `some`, `every`, `none`; у примитивных массивов также `contains`, `in`. Поиск строк не учитывает регистр. Условие поля задаётся объектом с оператором, например `{ "id": { "eq": "1" } }`.
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"movies": {
|
|
194
|
+
"some": {
|
|
195
|
+
"actors": {
|
|
196
|
+
"some": {
|
|
197
|
+
"genres": { "some": { "id": { "in": ["2", "3"] } } }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Корневой `where` выбирает записи основной коллекции. `where` внутри связи фильтрует её элементы, сохраняя родительскую запись. Каждый вложенный список обрабатывается отдельно. Фильтрация по связи работает независимо от её включения в ответ.
|
|
206
|
+
|
|
207
|
+
`order` — массив правил `{ "field": "fullName", "direction": "ASC" }`. Первое правило приоритетнее; при равных значениях сохраняется порядок хранения. `null` и отсутствующее значение сравниваются одинаково. REST использует путь `profile.name`, GraphQL — enum `profile_name`. Неоднозначные имена enum вызывают ошибку генерации. Сортировка доступна по скалярным полям текущего объекта, включая вложенные поля. Для связанных списков задаётся собственный `order`.
|
|
85
208
|
|
|
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` |
|
|
209
|
+
### REST
|
|
101
210
|
|
|
102
|
-
|
|
211
|
+
| Метод | Путь | Операция |
|
|
212
|
+
|---|---|---|
|
|
213
|
+
| GET | `/users` | `userList` |
|
|
214
|
+
| GET | `/users/{id}` | `user` |
|
|
215
|
+
| POST | `/users` | `userCreate` |
|
|
216
|
+
| PUT | `/users/{id}` | `userReplace` |
|
|
217
|
+
| PATCH | `/users/{id}` | `userUpdate` |
|
|
218
|
+
| DELETE | `/users/{id}` | `userDelete` |
|
|
103
219
|
|
|
104
|
-
|
|
220
|
+
Имя параметра пути соответствует первичному ключу. POST, PUT и PATCH принимают JSON-объект записи. PUT заменяет запись с сохранением ключа и серверных полей. PATCH объединяет поля на верхнем уровне; переданные вложенные объекты заменяются с сохранением их полей `readOnly`. Создание и замена требуют всех обязательных полей. При обновлении проверяются переданные значения и итоговая запись. Отсутствующая запись — `404`, конфликт — `409`. DELETE возвращает удалённую запись.
|
|
105
221
|
|
|
106
|
-
|
|
222
|
+
Параметры `where`, `order`, `pager`, `nested` содержат JSON; `scope` — строку выбора. Пример до URL-кодирования:
|
|
223
|
+
|
|
224
|
+
```text
|
|
225
|
+
GET /users?where={"fullName":{"contains":"Мира"}}&order=[{"field":"fullName","direction":"ASC"}]&pager={"page":1,"pageSize":20}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Кодирование через `URLSearchParams`:
|
|
107
229
|
|
|
108
230
|
```js
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
files: {
|
|
115
|
-
data: [{ content: new Uint8Array([1, 2, 3]), directory: 'examples', mimeType: 'application/octet-stream', name: 'example.bin' }],
|
|
116
|
-
},
|
|
117
|
-
};
|
|
231
|
+
const params = new URLSearchParams({
|
|
232
|
+
scope: 'id,fullName,movies(id,title)',
|
|
233
|
+
nested: JSON.stringify({ movies: { order: [{ field: 'title', direction: 'ASC' }], pager: { page: 1, pageSize: 5 } } }),
|
|
234
|
+
});
|
|
235
|
+
const response = await fetch(`/users?${params}`);
|
|
118
236
|
```
|
|
119
237
|
|
|
120
|
-
|
|
238
|
+
`scope=*,actors(user(id,fullName),genres(*))` выбирает собственные поля и указанные связи. `*` включает собственные поля текущего объекта и хранимые ключи, кроме полей `writeOnly`. Связи перечисляются явно. По умолчанию выбираются собственные поля. Списки сохраняют структуру ответа `{ data, total }`.
|
|
121
239
|
|
|
122
|
-
|
|
240
|
+
`nested` сопоставляет полные пути ответа и настройки списков:
|
|
123
241
|
|
|
124
242
|
```json
|
|
125
243
|
{
|
|
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"
|
|
244
|
+
"actors": { "pager": { "pageSize": 5 } },
|
|
245
|
+
"actors.genres": {
|
|
246
|
+
"where": { "id": { "in": ["2", "3"] } },
|
|
247
|
+
"order": [{ "field": "name", "direction": "ASC" }]
|
|
132
248
|
}
|
|
133
249
|
}
|
|
134
250
|
```
|
|
135
251
|
|
|
136
|
-
|
|
252
|
+
Путь в `nested` должен быть выбран через `scope` и вести к списку объектов. Маршруты отдельных записей и мутации принимают `scope` и `nested`; корневые `where`, `order` и `pager` применяются к GET коллекции. Неизвестные имена и небезопасные пути возвращают `400`.
|
|
253
|
+
|
|
254
|
+
### GraphQL
|
|
255
|
+
|
|
256
|
+
```graphql
|
|
257
|
+
query {
|
|
258
|
+
userList(
|
|
259
|
+
order: [{ field: fullName, direction: ASC }]
|
|
260
|
+
pager: { page: 1, pageSize: 20 }
|
|
261
|
+
) {
|
|
262
|
+
total
|
|
263
|
+
data {
|
|
264
|
+
id
|
|
265
|
+
fullName
|
|
266
|
+
movies(order: [{ field: title, direction: ASC }], pager: { pageSize: 5 }) {
|
|
267
|
+
total
|
|
268
|
+
data { id title }
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
137
274
|
|
|
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 с файловыми маршрутами и завершает работу |
|
|
275
|
+
Запрос `user(id: ...)` возвращает одну запись или `null`, если она отсутствует. Мутации: `userCreate(data: ...)`, `userReplace(id: ..., data: ...)`, `userUpdate(id: ..., data: ...)`, `userDelete(id: ...)`. Для модели, состоящей из генерируемых полей, мутация создания вызывается без аргумента `data`. Правила записи и валидации совпадают с REST. Связи, выбранные в результате мутации, определяют содержимое ответа.
|
|
146
276
|
|
|
147
|
-
|
|
277
|
+
Строковый первичный ключ представлен типом GraphQL `ID`, обычные строки — `String`, числа — `Float`, параметры пагинации — `Int`. Enum сохраняет допустимые строковые имена; остальные значения получают имена `VALUE_0`, `VALUE_1` и т. д. Длины строк, форматы и другие ограничения модели проверяются сервером при выполнении запроса. Для просмотра схемы доступна интроспекция. Параметры выбранных списков проверяются до выполнения мутаций. Ошибки содержат `extensions.code`: `INVALID_INPUT`, `INVALID_QUERY`, `NOT_FOUND`, `CONFLICT` или `INTERNAL_ERROR`.
|
|
148
278
|
|
|
149
279
|
## Пример базы данных
|
|
150
280
|
|
|
151
|
-
Ниже приведён пример каталога фильмов с тестовыми данными. `Гангстер` связан с родительским жанром `Криминал`.
|
|
152
|
-
|
|
153
281
|
```json
|
|
154
282
|
{
|
|
155
283
|
"countries": [
|
|
156
|
-
{
|
|
157
|
-
|
|
284
|
+
{
|
|
285
|
+
"id": "1",
|
|
286
|
+
"isArchived": false,
|
|
287
|
+
"name": "Ардения"
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"id": "2",
|
|
291
|
+
"isArchived": false,
|
|
292
|
+
"name": "Велория"
|
|
293
|
+
}
|
|
158
294
|
],
|
|
159
295
|
"genres": [
|
|
160
|
-
{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
296
|
+
{
|
|
297
|
+
"id": "1",
|
|
298
|
+
"isArchived": false,
|
|
299
|
+
"name": "Криминал",
|
|
300
|
+
"parentIds": []
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"id": "2",
|
|
304
|
+
"isArchived": false,
|
|
305
|
+
"name": "Гангстер",
|
|
306
|
+
"parentIds": ["1"]
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"id": "3",
|
|
310
|
+
"isArchived": false,
|
|
311
|
+
"name": "Драма",
|
|
312
|
+
"parentIds": []
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"id": "4",
|
|
316
|
+
"isArchived": false,
|
|
317
|
+
"name": "Комедия",
|
|
318
|
+
"parentIds": []
|
|
319
|
+
}
|
|
164
320
|
],
|
|
165
321
|
"movies": [
|
|
166
322
|
{
|
|
167
323
|
"actors": [
|
|
168
|
-
{
|
|
169
|
-
|
|
324
|
+
{
|
|
325
|
+
"genreIds": ["2", "3"],
|
|
326
|
+
"id": "movie-1-actor-1",
|
|
327
|
+
"userId": "1"
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"genreIds": ["3"],
|
|
331
|
+
"id": "movie-1-actor-2",
|
|
332
|
+
"userId": "2"
|
|
333
|
+
}
|
|
170
334
|
],
|
|
171
335
|
"coverSrc": "https://example.com/covers/shadows-of-ardenia.jpg",
|
|
172
336
|
"description": "Наследница портового города раскрывает заговор двух соперничающих семей.",
|
|
@@ -186,8 +350,16 @@ export default {
|
|
|
186
350
|
}
|
|
187
351
|
],
|
|
188
352
|
"publishers": [
|
|
189
|
-
{
|
|
190
|
-
|
|
353
|
+
{
|
|
354
|
+
"id": "1",
|
|
355
|
+
"isArchived": false,
|
|
356
|
+
"name": "Northlight Studio"
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
"id": "2",
|
|
360
|
+
"isArchived": false,
|
|
361
|
+
"name": "Aurora Pictures"
|
|
362
|
+
}
|
|
191
363
|
],
|
|
192
364
|
"users": [
|
|
193
365
|
{
|
|
@@ -208,150 +380,12 @@ export default {
|
|
|
208
380
|
}
|
|
209
381
|
```
|
|
210
382
|
|
|
211
|
-
Каждый массив верхнего уровня становится REST-ресурсом:
|
|
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
|
-
```
|
|
221
|
-
|
|
222
|
-
`POST` генерирует строковый ID. `PUT` полностью заменяет выбранную запись, а `PATCH` изменяет только переданные поля; обе операции сохраняют существующий ID и его тип. Поле `id` в теле любого запроса не может переопределить ID, которым управляет сервер. Все операции записи — `POST`, `PUT`, `PATCH` и `DELETE` — выполняются последовательно; дисковое хранилище записывает их в JSON, а хранилище в памяти сохраняет до завершения процесса.
|
|
223
|
-
|
|
224
|
-
Файл базы должен существовать до запуска. Имена ресурсов могут содержать латинские буквы, цифры, `_` и `-` и должны начинаться с буквы. Каждый ресурс является массивом JSON-объектов. У каждой записи должен быть непустой строковый или конечный числовой `id`; ID должны быть уникальны внутри ресурса при сравнении как строки, поэтому `1` и `"1"` не могут существовать одновременно. Все вложенные значения должны быть совместимы с JSON: конечные числа, строки, логические значения, `null`, массивы и обычные объекты. Перед каждым GET-запросом к ресурсу и изменением сервер заново читает файл, поэтому корректные правки существующих ресурсов становятся видны сразу. Имена ресурсов и маршруты определяются при запуске; после добавления, удаления или переименования массива верхнего уровня перезапустите сервер.
|
|
225
|
-
|
|
226
|
-
Успешная операция записи возвращает созданную, заменённую, обновлённую или удалённую запись. Ошибки используют подходящий HTTP-статус и следующий JSON-формат:
|
|
227
|
-
|
|
228
|
-
```json
|
|
229
|
-
{ "error": "..." }
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
## Пагинация и сортировка
|
|
233
|
-
|
|
234
|
-
```http
|
|
235
|
-
GET /movies?_page=1&_perPage=10&_sort=-id,title
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
GET-запрос к коллекции всегда возвращает объект с массивом текущей страницы и общим количеством записей после фильтрации. По умолчанию `_page` равен `1`, а `_perPage` — `10`:
|
|
239
|
-
|
|
240
|
-
```json
|
|
241
|
-
{
|
|
242
|
-
"data": [],
|
|
243
|
-
"total": 0
|
|
244
|
-
}
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
`data` содержит записи только запрошенной страницы. `total` содержит количество всех записей, соответствующих фильтру, до применения пагинации. Номер последней страницы при необходимости вычисляется на клиенте как `Math.max(1, Math.ceil(total / pageSize))`.
|
|
248
|
-
|
|
249
|
-
Оба параметра пагинации должны быть положительными целыми числами. По умолчанию `_perPage` не может превышать `1000`; лимит меняется через `server.maxPageSize` в конфиге, переданном CLI или `createServer()`. При некорректном значении сервер возвращает `400`, а не исправляет его автоматически. Страница после последней возвращает пустой массив `data`, но сохраняет фактическое значение `total`.
|
|
250
|
-
|
|
251
|
-
`_sort` принимает пути полей через запятую. Правила применяются слева направо; префикс `-` включает сортировку по убыванию. Через точку можно обратиться к полю вложенного объекта, включая поля, добавленные через `_embed`, например `GET /users?_embed=country&_sort=country.name,-id`. Неизвестные и небезопасные поля сортировки возвращают `400`.
|
|
252
|
-
|
|
253
|
-
## Фильтры
|
|
254
|
-
|
|
255
|
-
Передайте JSON-объект через `_where`:
|
|
256
|
-
|
|
257
|
-
```http
|
|
258
|
-
GET /movies?_where={"title":{"contains":"тени"}}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Можно фильтровать вложенные объекты и массивы на любой глубине. Условия внутри одного объекта по умолчанию объединяются через `AND`:
|
|
262
|
-
|
|
263
|
-
```json
|
|
264
|
-
{
|
|
265
|
-
"actors": { "some": { "userId": { "eq": "1" } } },
|
|
266
|
-
"title": { "contains": "тени" }
|
|
267
|
-
}
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
Для явных логических групп используйте `and`, `or` и `not`:
|
|
271
|
-
|
|
272
|
-
```json
|
|
273
|
-
{
|
|
274
|
-
"and": [
|
|
275
|
-
{
|
|
276
|
-
"or": [
|
|
277
|
-
{ "title": { "contains": "тени" } },
|
|
278
|
-
{ "actors": { "some": { "userId": { "eq": "2" } } } }
|
|
279
|
-
]
|
|
280
|
-
},
|
|
281
|
-
{ "not": { "isArchived": { "eq": true } } }
|
|
282
|
-
]
|
|
283
|
-
}
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
Операторы полей:
|
|
287
|
-
|
|
288
|
-
| Оператор | Поведение |
|
|
289
|
-
| --- | --- |
|
|
290
|
-
| `eq`, `ne` | Равенство или неравенство |
|
|
291
|
-
| `contains` | Подстрока без учёта регистра для строк или совпадающий элемент массива |
|
|
292
|
-
| `startsWith`, `endsWith` | Начало или окончание строки без учёта регистра |
|
|
293
|
-
| `gt`, `gte`, `lt`, `lte` | Сравнение значений; строки дат ISO можно сравнивать лексикографически |
|
|
294
|
-
| `in` | Совпадение скалярного значения или элемента массива с одним из переданных значений |
|
|
295
|
-
| `some`, `every`, `none` | Применение вложенного условия к элементам массива |
|
|
296
|
-
| `not` | Отрицание вложенного условия поля |
|
|
297
|
-
|
|
298
|
-
Также можно использовать простые query-параметры:
|
|
299
|
-
|
|
300
|
-
```http
|
|
301
|
-
GET /movies?title:contains=тени
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
В простых фильтрах распознаются JSON-примитивы: числа, `true`, `false` и `null`. Значения с ведущими нулями, например `001`, остаются строками. Неизвестные операторы, некорректные логические условия и отсутствующие в непустом ресурсе пути фильтра возвращают `400`.
|
|
305
|
-
|
|
306
|
-
Разные простые query-фильтры объединяются через `AND`. Повтор одного фильтра равенства выбирает любое из его значений, поэтому `GET /movies?id=1&id=2` равнозначен `GET /movies?id:in=1,2`. Для оператора `in` значения перечисляются через запятую. Для поля-массива `in` означает, что хотя бы один элемент поля совпадает хотя бы с одним переданным значением. `every` для пустого массива возвращает `true`, а `some` — `false`.
|
|
307
|
-
|
|
308
|
-
Если передан `_where`, он становится полным фильтром, а остальные простые параметры фильтрации игнорируются. В примерах JSON оставлен читаемым; при ручном формировании URL HTTP-клиент должен закодировать значение `_where`, например через `encodeURIComponent(JSON.stringify(where))`.
|
|
309
|
-
|
|
310
|
-
Фильтрация выполняется после `_embed`. Поэтому фильтр может обращаться к полям добавленной связи, если в том же запросе указан соответствующий `_embed`; сохранённые поля `...Id` и `...Ids` всегда можно фильтровать напрямую.
|
|
311
|
-
|
|
312
|
-
## Связи
|
|
313
|
-
|
|
314
|
-
Используйте `_embed`, чтобы добавить связанные записи в ответ:
|
|
315
|
-
|
|
316
|
-
```http
|
|
317
|
-
GET /movies/1?_embed=actors.user.country&_embed=actors.genres&_embed=publishers
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
Ответ будет содержать актёров, пользователя и жанры каждого актёра, страну пользователя и издателей. Исходные поля с ID остаются в ответе, а файл базы не изменяется. Сервер не устанавливает фиксированный лимит глубины, но каждый требуемый уровень должен быть явно указан в конечном пути `_embed`:
|
|
321
|
-
|
|
322
|
-
```http
|
|
323
|
-
GET /movies/1?_embed=actors.user.country
|
|
324
|
-
GET /genres/2?_embed=parents.parents
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
Неизвестные и некорректные пути `_embed` возвращают `400`.
|
|
328
|
-
|
|
329
|
-
Параметр `_embed` можно передать несколько раз, как в примере выше, либо перечислить пути через запятую в одном параметре. Пагинация применяется только к запрошенной корневой коллекции; вложенные связанные записи возвращаются полностью.
|
|
330
|
-
|
|
331
|
-
Поддерживаются и обратные связи:
|
|
332
|
-
|
|
333
|
-
```http
|
|
334
|
-
GET /countries/1?_embed=users
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
Связи определяются по именам полей. Поле `<relation>Id` создаёт одиночную связь, а `<relation>Ids` — связь с коллекцией. Имя связи сопоставляется с ресурсом верхнего уровня напрямую или через его форму в единственном числе. Например:
|
|
338
|
-
|
|
339
|
-
- `countryId` ссылается на `countries`;
|
|
340
|
-
- `userId` ссылается на `users`, если запрошена связь `user`;
|
|
341
|
-
- `genreIds` ссылается на `genres`;
|
|
342
|
-
- `publisherIds` ссылается на `publishers`;
|
|
343
|
-
- `parentIds` ссылается на тот же ресурс, если запрошена связь `_embed=parents`; `_embed=children` загружает обратную связь с дочерними записями.
|
|
344
|
-
|
|
345
|
-
Имя обратной связи совпадает с именем исходного ресурса. Например, `_embed=users` у страны находит пользователей, во вложенных данных которых указан соответствующий `countryId`. Сервер получает связи по запросу, но не проверяет ссылочную целостность при записи данных.
|
|
346
|
-
|
|
347
|
-
Явные поля `...Id` и `...Ids` определяют связи. Если в записи также сохранено устаревшее вложенное значение, `_embed` заменяет это свойство ответа актуальной связанной записью. Если связанная запись для одиночной связи не найдена, результатом будет `null`; отсутствующие связанные записи коллекции не попадут в итоговый массив. ID-индексы создаются только для ресурсов, используемых при поиске связей в текущем запросе.
|
|
348
|
-
|
|
349
383
|
## Файлы
|
|
350
384
|
|
|
351
|
-
Добавьте `files.directory` и `files.metadata` в
|
|
385
|
+
Добавьте `files.directory` и `files.metadata` в конфигурацию и запустите сервер:
|
|
352
386
|
|
|
353
387
|
```bash
|
|
354
|
-
deep-json-server
|
|
388
|
+
deep-json-server server.config.js
|
|
355
389
|
```
|
|
356
390
|
|
|
357
391
|
Для временных тестов вместо этого используйте `files.data`. Каждая начальная запись содержит `name`, `mimeType`, бинарное `content` в виде `Uint8Array` и необязательный `directory`. Загруженные файлы в таком режиме остаются в памяти до завершения процесса.
|
|
@@ -407,176 +441,52 @@ Content-Type: application/json
|
|
|
407
441
|
|
|
408
442
|
`PATCH` возвращает обновлённые метаданные со статусом `200`; если по новому пути уже существует файл, сервер возвращает `409`. `DELETE` отвечает статусом `204` без тела. Если файл не найден, любая операция по пути возвращает `404`. Пути в URL задаются относительно `files.directory`, а все возвращаемые URL — относительно адреса сервера.
|
|
409
443
|
|
|
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
|
|
444
|
+
При хранении на диске бинарный файл находится по пути `<files.directory>/<directory>/<name>`. Файл метаданных содержит только `directory`, `mimeType` и `name`; `size` считывается у фактического файла, а URL вычисляются. Используйте дисковую базу и файловое хранилище только из одного процесса сервера одновременно и не редактируйте сохранённые файлы или метаданные до его остановки. Сервер автоматически создаёт директории, пути внутри `files.directory` не могут содержать символические ссылки, а имена файлов ограничены переносимыми между поддерживаемыми операционными системами значениями. Изначально файл метаданных может отсутствовать: он создаётся при первой загрузке.
|
|
415
445
|
|
|
416
|
-
|
|
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
|
-
Настройки схемы:
|
|
443
|
-
|
|
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-совместимые схемы полей, объединяемые с автоматически найденными |
|
|
451
|
-
|
|
452
|
-
`formats` — сокращённая запись для назначения `format` уже существующему строковому полю. `properties` позволяет полностью описать поле, в том числе его `type`, `format`, ограничения и вложенные свойства, либо добавить поле, которого нет в данных. Если одно поле получает `format` через оба механизма, значение из `formats` применяется последним.
|
|
453
|
-
|
|
454
|
-
Укажите `openapi.path` в конфиге сервера, затем сгенерируйте OpenAPI 3.0.3 и завершите работу:
|
|
455
|
-
|
|
456
|
-
```bash
|
|
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()` использует значения конфига или тот же адрес по умолчанию.
|
|
485
|
-
|
|
486
|
-
Используйте `name`, если ресурсу нужно явно задать имя схемы вместо автоматически полученного имени в единственном числе:
|
|
487
|
-
|
|
488
|
-
```json
|
|
489
|
-
{
|
|
490
|
-
"$schema": {
|
|
491
|
-
"equipment": {
|
|
492
|
-
"name": "Equipment"
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
В сгенерированном документе описаны CRUD-маршруты, пагинация, сортировка, фильтрация вложенных данных, `_embed`, а также прямые и обратные связи в ответах, определённые по полям `...Id` и `...Ids`. При наличии `--files` в него также добавляются маршруты бинарной загрузки, получения и удаления файлов с поддержкой произвольных MIME-типов. Числовой ID базы описывается как `integer | string`, поскольку последующий `POST` создаст строковый ID в том же ресурсе. До создания документа генератор отклоняет повторяющиеся имена схем и `operationId`, а также некорректные переопределения схем. Файл можно передать, например, в RTK Query OpenAPI Codegen. OpenAPI создаётся только с параметром `--openapi` или `--openapi-only`; обычный запуск сервера файл не перезаписывает.
|
|
499
|
-
|
|
500
|
-
При обычном запуске тела запросов проверяются по тем же автоматически выведенным и настроенным схемам. Для `POST` и `PUT` проверяются обязательные поля из `required`; `PATCH` проверяет только фактически переданные поля. Настройки `formats` и `properties` применяются ко всем трём методам. Не перечисленные дополнительные поля объекта остаются разрешёнными. Некорректные тела возвращают `400`.
|
|
446
|
+
Отправляйте файл как бинарное тело запроса. В браузере для этого можно использовать `xhr.send(file)`, а прогресс отслеживать через `XMLHttpRequest.upload.onprogress`. Максимальный размер по умолчанию равен 100 МиБ и настраивается через `server.maxFileSize`. Отсутствующие или небезопасные заголовки и пути возвращают `400`, превышение лимита — `413`, а отсутствующий, некорректный или не поддерживаемый Fastify `Content-Type` — `400` либо `415` в зависимости от этапа проверки.
|
|
501
447
|
|
|
502
448
|
## Программный API
|
|
503
449
|
|
|
504
450
|
```js
|
|
505
451
|
import { createServer } from '@kollors/deep-json-server';
|
|
452
|
+
import config from './server.config.js';
|
|
453
|
+
|
|
454
|
+
const facade = await createServer(config);
|
|
455
|
+
const openapi = await facade.openapi();
|
|
456
|
+
const sdl = await facade.graphql();
|
|
457
|
+
const server = facade.fastify();
|
|
458
|
+
await server.listen();
|
|
459
|
+
// await server.close();
|
|
460
|
+
```
|
|
506
461
|
|
|
507
|
-
|
|
508
|
-
database: {
|
|
509
|
-
path: 'mock/database.json',
|
|
510
|
-
schema: 'mock/database-schema.json',
|
|
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();
|
|
462
|
+
Методы `openapi()` и `graphql()` возвращают схемы. `fastify()` возвращает экземпляр сервера для настройки и запуска. База и включённые сервисы инициализируются при `ready()`, `listen()` или первом `inject()`; ошибка инициализации останавливает запуск. Возможности сервера можно переопределить через `createServer(config, { files: false, graphql: true, openapi: true })`.
|
|
538
463
|
|
|
539
|
-
|
|
464
|
+
Генераторы можно использовать отдельно:
|
|
540
465
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const runningFastify = runningServer.fastify();
|
|
466
|
+
```js
|
|
467
|
+
import { generateOpenapi, generateGraphql, writeOpenapi, writeGraphql } from '@kollors/deep-json-server';
|
|
544
468
|
|
|
545
|
-
await
|
|
469
|
+
const document = await generateOpenapi('./schema.json', { files: true });
|
|
470
|
+
const sdl = await generateGraphql('./schema.json');
|
|
471
|
+
await writeOpenapi(document, './generated/openapi.yaml');
|
|
472
|
+
await writeGraphql(sdl, './generated/schema.graphql');
|
|
473
|
+
```
|
|
546
474
|
|
|
547
|
-
|
|
548
|
-
await runningFastify.close();
|
|
475
|
+
`generateOpenapi()` также принимает `host`, `port`, `pageSize`, `maxPageSize` и `info`. Вместо пути можно передать объект схемы. Сервер и генераторы работают с собственной копией модели.
|
|
549
476
|
|
|
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
|
-
});
|
|
477
|
+
## Хранение и разработка
|
|
560
478
|
|
|
561
|
-
|
|
562
|
-
const memoryResponse = await memoryFastify.inject({ method: 'GET', url: '/movies/1' });
|
|
479
|
+
Изменения выполняются последовательно внутри экземпляра сервера и проверяются на копии данных до сохранения. Для одного файла базы используйте один процесс сервера. Счётчики `increment` хранятся рядом с базой в `<путь базы>.counters.json`; сохраняйте этот файл вместе с базой. Номера резервируются до записи данных: сбой может оставить пропуск, но не приводит к повторной выдаче номера.
|
|
563
480
|
|
|
564
|
-
|
|
481
|
+
Сервер предназначен для имитации API. Авторизацию и хеширование паролей при необходимости реализуйте в приложении.
|
|
565
482
|
|
|
566
|
-
|
|
483
|
+
```sh
|
|
484
|
+
npm ci
|
|
485
|
+
npm run verify
|
|
567
486
|
```
|
|
568
487
|
|
|
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-декларации возвращаемого объекта и всех вариантов конфигурации.
|
|
488
|
+
Команда проверяет типы, стиль кода, покрытие тестами и установку пакета из архива.
|
|
579
489
|
|
|
580
|
-
|
|
490
|
+
Для публикации новой альфы обновите версию в `package.json` и отправьте изменения в `main`. GitHub Actions создаст тег версии и опубликует пакет в канал npm `alpha` через trusted publishing. Уже опубликованная версия пропускается. Если тег создан, а публикация не завершилась, повторный запуск использует этот тег и проверяет соответствие ему файлов пакета. Отправка тега версии также запускает публикацию; стабильные версии публикуются в `latest`.
|
|
581
491
|
|
|
582
|
-
|
|
492
|
+
Лицензия: MIT.
|