@kollors/deep-json-server 0.4.0 → 0.6.0
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 +317 -67
- package/README.ru.md +316 -66
- package/index.js +7 -2
- package/package.json +1 -1
- package/src/cli.js +61 -48
- package/src/config.js +194 -0
- package/src/constants.js +1 -0
- package/src/database.js +31 -2
- package/src/files.js +368 -0
- package/src/openapi/config.js +15 -1
- package/src/openapi/document.js +69 -39
- package/src/openapi/index.js +29 -18
- package/src/query/pagination.js +1 -7
- package/src/server.js +91 -58
- package/types/index.d.ts +11 -2
- package/types/src/config.d.ts +86 -0
- package/types/src/constants.d.ts +1 -0
- package/types/src/database.d.ts +5 -2
- package/types/src/files.d.ts +24 -0
- package/types/src/openapi/config.d.ts +1 -1
- package/types/src/openapi/document.d.ts +7 -7
- package/types/src/openapi/index.d.ts +9 -13
- package/types/src/query/pagination.d.ts +1 -6
- package/types/src/server.d.ts +11 -22
package/README.ru.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[GitHub](https://github.com/kollors/deep-json-server) | [npm](https://www.npmjs.com/package/@kollors/deep-json-server)
|
|
6
6
|
|
|
7
|
-
Небольшой
|
|
7
|
+
Небольшой моковый REST-сервер с CRUD, пагинацией, глубокими фильтрами, рекурсивной загрузкой связей, бинарными файлами и генерацией OpenAPI. Данные могут храниться в JSON-файлах или памяти, а связи определяются по соглашениям о нейминге ключей: `countryId`, `genreIds`, `publisherIds` и так далее.
|
|
8
8
|
|
|
9
9
|
## Установка
|
|
10
10
|
|
|
@@ -14,38 +14,149 @@
|
|
|
14
14
|
npm install --save-dev @kollors/deep-json-server
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## Быстрый старт
|
|
18
|
+
|
|
19
|
+
До запуска создайте файл базы `mock/database.json`:
|
|
18
20
|
|
|
19
21
|
```json
|
|
20
22
|
{
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
}
|
|
23
|
+
"movies": [
|
|
24
|
+
{ "id": "1", "title": "Тени Ардении" }
|
|
25
|
+
]
|
|
25
26
|
}
|
|
26
27
|
```
|
|
27
28
|
|
|
29
|
+
Рядом с `package.json` создайте ESM-модуль `server.config.js`:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
export default {
|
|
33
|
+
database: {
|
|
34
|
+
path: 'mock/database.json',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
28
39
|
Запустите сервер:
|
|
29
40
|
|
|
30
41
|
```bash
|
|
31
|
-
|
|
42
|
+
npx deep-json-server server.config.js
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
По умолчанию API доступен по адресу `http://127.0.0.1:4001`. Например, `GET http://127.0.0.1:4001/movies` вернёт страницу:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"data": [{ "id": "1", "title": "Тени Ардении" }],
|
|
50
|
+
"total": 1
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Конфигурация и запуск
|
|
55
|
+
|
|
56
|
+
Создайте ESM-модуль `server.config.js`. В примере ниже включены настройки всех возможностей:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import process from 'node:process';
|
|
60
|
+
|
|
61
|
+
export default {
|
|
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
|
+
host: '127.0.0.1',
|
|
75
|
+
logger: true,
|
|
76
|
+
maxFileSize: 100 * 1024 * 1024,
|
|
77
|
+
maxPageSize: 1000,
|
|
78
|
+
port: 4001,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
32
81
|
```
|
|
33
82
|
|
|
34
|
-
|
|
83
|
+
Ключи конфигурации:
|
|
84
|
+
|
|
85
|
+
| Ключ | Условие | Назначение |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| `database.path` | Требуется ровно один из `path` или `data` | Существующий JSON-файл базы данных |
|
|
88
|
+
| `database.data` | Требуется ровно один из `path` или `data` | Объект базы данных, хранящийся в памяти |
|
|
89
|
+
| `database.schema` | Необязателен | Путь к JSON-настройкам либо объект с настройками проверки запросов и OpenAPI |
|
|
90
|
+
| `files.directory` | Вместе с `files.metadata` | Директория для бинарного содержимого на диске |
|
|
91
|
+
| `files.metadata` | Вместе с `files.directory` | JSON-файл с метаданными файлов на диске |
|
|
92
|
+
| `files.data` | Вместо пары `directory` и `metadata` | Файлы в памяти с содержимым в `Uint8Array` |
|
|
93
|
+
| `openapi.path` | Обязателен для CLI-флагов `--openapi` и `--openapi-only` | Генерируемый YAML-файл OpenAPI; программный API может вернуть документ без этого пути |
|
|
94
|
+
| `server.host` | Необязателен | Адрес для CLI, `server.openapi()` и вызова `server.fastify().listen()` без аргументов; по умолчанию `127.0.0.1` |
|
|
95
|
+
| `server.logger` | Необязателен | Настройки логгера Fastify; по умолчанию `true` |
|
|
96
|
+
| `server.maxFileSize` | Необязателен | Максимальный размер загружаемого файла в байтах при включённых файловых маршрутах; по умолчанию 100 МиБ |
|
|
97
|
+
| `server.maxPageSize` | Необязателен | Максимальное значение `_perPage` в API и OpenAPI; по умолчанию `1000` |
|
|
98
|
+
| `server.port` | Необязателен | Порт для CLI, `server.openapi()` и вызова `server.fastify().listen()` без аргументов; по умолчанию `4001` |
|
|
99
|
+
|
|
100
|
+
`server.port` должен быть целым числом от `0` до `65535`. Значение `0` позволяет Fastify выбрать свободный порт при запуске, но не подходит для генерации URL в OpenAPI, где допустимы порты от `1` до `65535`. `server.maxFileSize` и `server.maxPageSize` должны быть положительными целыми числами.
|
|
101
|
+
|
|
102
|
+
Все относительные пути вычисляются от директории с `server.config.js`, а не от текущей рабочей директории. Неизвестные ключи, пустые пути и значения некорректных типов отклоняются до запуска. Конфиг является исполняемым JavaScript: в нём можно читать переменные окружения, импортировать другие модули и вычислять значения перед экспортом объекта. Для `.js`-конфига с `export default` проект должен быть ESM (`"type": "module"`); в CommonJS-проекте сохраните тот же конфиг как `server.config.mjs`.
|
|
103
|
+
|
|
104
|
+
В том же конфиге все данные можно разместить в памяти. `database.path` и `database.data` взаимоисключающие, а `database.schema` принимает путь или объект. Аналогично, `files.data` нельзя сочетать с `files.directory` или `files.metadata`:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
export default {
|
|
108
|
+
database: {
|
|
109
|
+
data: { movies: [{ id: '1', title: 'Тени Ардении' }] },
|
|
110
|
+
schema: { $info: { title: 'API фильмов', version: '1.0.0' } },
|
|
111
|
+
},
|
|
112
|
+
files: {
|
|
113
|
+
data: [{ content: new Uint8Array([1, 2, 3]), id: 'file-1', mimeType: 'application/octet-stream', name: 'example.bin' }],
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Значения в памяти клонируются при инициализации. Поэтому операции с CRUD и файлами не изменяют экспортированный объект конфига, а их результаты исчезают после завершения процесса.
|
|
119
|
+
|
|
120
|
+
Добавьте нужные команды в `package.json`. Здесь `mock:openapi:files` сначала обновляет OpenAPI, а затем оставляет сервер запущенным с файловыми маршрутами:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"scripts": {
|
|
125
|
+
"mock": "deep-json-server server.config.js",
|
|
126
|
+
"mock:files": "deep-json-server --files server.config.js",
|
|
127
|
+
"mock:openapi:files": "deep-json-server --files --openapi server.config.js",
|
|
128
|
+
"openapi": "deep-json-server --openapi-only server.config.js",
|
|
129
|
+
"openapi:files": "deep-json-server --files --openapi-only server.config.js"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Режимы CLI:
|
|
135
|
+
|
|
136
|
+
| Команда | Поведение |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| `deep-json-server server.config.js` | Запускает CRUD-сервер без файловых маршрутов |
|
|
139
|
+
| `deep-json-server --files server.config.js` | Запускает CRUD-сервер с файловыми маршрутами |
|
|
140
|
+
| `deep-json-server --openapi server.config.js` | Генерирует OpenAPI и запускает CRUD-сервер |
|
|
141
|
+
| `deep-json-server --files --openapi server.config.js` | Генерирует OpenAPI с файловыми маршрутами и запускает сервер с ними |
|
|
142
|
+
| `deep-json-server --openapi-only server.config.js` | Генерирует OpenAPI и завершает работу |
|
|
143
|
+
| `deep-json-server --files --openapi-only server.config.js` | Генерирует OpenAPI с файловыми маршрутами и завершает работу |
|
|
144
|
+
|
|
145
|
+
Флаг `--files` независим: без него файловые маршруты не регистрируются и не добавляются в OpenAPI, даже если секция `files` присутствует в конфиге. Параметры `--openapi` и `--openapi-only` взаимоисключающие. Команда `deep-json-server --help` выводит краткую справку по CLI.
|
|
35
146
|
|
|
36
147
|
## Пример базы данных
|
|
37
148
|
|
|
38
|
-
|
|
149
|
+
Ниже приведён пример каталога фильмов с тестовыми данными. `Гангстер` связан с родительским жанром `Криминал`.
|
|
39
150
|
|
|
40
151
|
```json
|
|
41
152
|
{
|
|
42
153
|
"countries": [
|
|
43
|
-
{ "id": "1", "isArchived": false, "name": "
|
|
44
|
-
{ "id": "2", "isArchived": false, "name": "
|
|
154
|
+
{ "id": "1", "isArchived": false, "name": "Ардения" },
|
|
155
|
+
{ "id": "2", "isArchived": false, "name": "Велория" }
|
|
45
156
|
],
|
|
46
157
|
"genres": [
|
|
47
158
|
{ "id": "1", "isArchived": false, "name": "Криминал", "parentIds": [] },
|
|
48
|
-
{ "id": "2", "isArchived": false, "name": "
|
|
159
|
+
{ "id": "2", "isArchived": false, "name": "Гангстер", "parentIds": ["1"] },
|
|
49
160
|
{ "id": "3", "isArchived": false, "name": "Драма", "parentIds": [] },
|
|
50
161
|
{ "id": "4", "isArchived": false, "name": "Комедия", "parentIds": [] }
|
|
51
162
|
],
|
|
@@ -55,39 +166,39 @@ npm run mock
|
|
|
55
166
|
{ "genreIds": ["2", "3"], "id": "movie-1-actor-1", "userId": "1" },
|
|
56
167
|
{ "genreIds": ["3"], "id": "movie-1-actor-2", "userId": "2" }
|
|
57
168
|
],
|
|
58
|
-
"coverSrc": "https://
|
|
59
|
-
"description": "
|
|
169
|
+
"coverSrc": "https://example.com/covers/shadows-of-ardenia.jpg",
|
|
170
|
+
"description": "Наследница портового города раскрывает заговор двух соперничающих семей.",
|
|
60
171
|
"id": "1",
|
|
61
172
|
"isArchived": false,
|
|
62
173
|
"publisherIds": ["2"],
|
|
63
|
-
"title": "
|
|
174
|
+
"title": "Тени Ардении"
|
|
64
175
|
},
|
|
65
176
|
{
|
|
66
177
|
"actors": [],
|
|
67
|
-
"coverSrc": "https://
|
|
68
|
-
"description": "
|
|
178
|
+
"coverSrc": "https://example.com/covers/northern-star.jpg",
|
|
179
|
+
"description": "Ночной администратор старого отеля случайно становится участником поисков пропавшей картины.",
|
|
69
180
|
"id": "2",
|
|
70
181
|
"isArchived": false,
|
|
71
182
|
"publisherIds": ["1"],
|
|
72
|
-
"title": "
|
|
183
|
+
"title": "Полночь в «Северной звезде»"
|
|
73
184
|
}
|
|
74
185
|
],
|
|
75
186
|
"publishers": [
|
|
76
|
-
{ "id": "1", "isArchived": false, "name": "
|
|
77
|
-
{ "id": "2", "isArchived": false, "name": "
|
|
187
|
+
{ "id": "1", "isArchived": false, "name": "Northlight Studio" },
|
|
188
|
+
{ "id": "2", "isArchived": false, "name": "Aurora Pictures" }
|
|
78
189
|
],
|
|
79
190
|
"users": [
|
|
80
191
|
{
|
|
81
|
-
"bornAt": "
|
|
192
|
+
"bornAt": "1988-03-14",
|
|
82
193
|
"countryId": "1",
|
|
83
|
-
"fullName": "
|
|
194
|
+
"fullName": "Мира Волкова",
|
|
84
195
|
"id": "1",
|
|
85
196
|
"isArchived": false
|
|
86
197
|
},
|
|
87
198
|
{
|
|
88
|
-
"bornAt": "
|
|
89
|
-
"countryId": "
|
|
90
|
-
"fullName": "
|
|
199
|
+
"bornAt": "1991-11-02",
|
|
200
|
+
"countryId": "2",
|
|
201
|
+
"fullName": "Леон Ветров",
|
|
91
202
|
"id": "2",
|
|
92
203
|
"isArchived": false
|
|
93
204
|
}
|
|
@@ -106,9 +217,15 @@ PATCH /movies/:id
|
|
|
106
217
|
DELETE /movies/:id
|
|
107
218
|
```
|
|
108
219
|
|
|
109
|
-
`POST` генерирует строковый ID
|
|
220
|
+
`POST` генерирует строковый ID. `PUT` полностью заменяет выбранную запись, а `PATCH` изменяет только переданные поля; обе операции сохраняют существующий ID и его тип. Поле `id` в теле любого запроса не может переопределить ID, которым управляет сервер. Все операции записи — `POST`, `PUT`, `PATCH` и `DELETE` — выполняются последовательно; дисковое хранилище записывает их в JSON, а хранилище в памяти сохраняет до завершения процесса.
|
|
110
221
|
|
|
111
|
-
Файл базы должен существовать до запуска. Имена ресурсов могут содержать латинские буквы, цифры, `_` и `-` и должны начинаться с буквы. Каждый ресурс является массивом JSON-объектов. У каждой записи должен быть непустой строковый или конечный числовой `id`; ID должны быть уникальны внутри ресурса при сравнении как строки, поэтому `1` и `"1"` не могут существовать одновременно.
|
|
222
|
+
Файл базы должен существовать до запуска. Имена ресурсов могут содержать латинские буквы, цифры, `_` и `-` и должны начинаться с буквы. Каждый ресурс является массивом JSON-объектов. У каждой записи должен быть непустой строковый или конечный числовой `id`; ID должны быть уникальны внутри ресурса при сравнении как строки, поэтому `1` и `"1"` не могут существовать одновременно. Перед каждым GET-запросом и изменением сервер заново читает файл, поэтому корректные внешние правки становятся видны без перезапуска.
|
|
223
|
+
|
|
224
|
+
Успешная операция записи возвращает созданную, заменённую, обновлённую или удалённую запись. Ошибки используют подходящий HTTP-статус и следующий JSON-формат:
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{ "error": "..." }
|
|
228
|
+
```
|
|
112
229
|
|
|
113
230
|
## Пагинация и сортировка
|
|
114
231
|
|
|
@@ -116,30 +233,27 @@ DELETE /movies/:id
|
|
|
116
233
|
GET /movies?_page=1&_perPage=10&_sort=-id,title
|
|
117
234
|
```
|
|
118
235
|
|
|
119
|
-
GET-запрос к коллекции всегда возвращает объект
|
|
236
|
+
GET-запрос к коллекции всегда возвращает объект с массивом текущей страницы и общим количеством записей после фильтрации. По умолчанию `_page` равен `1`, а `_perPage` — `10`:
|
|
120
237
|
|
|
121
238
|
```json
|
|
122
239
|
{
|
|
123
240
|
"data": [],
|
|
124
|
-
"
|
|
125
|
-
"items": 0,
|
|
126
|
-
"last": 1,
|
|
127
|
-
"next": null,
|
|
128
|
-
"pages": 1,
|
|
129
|
-
"prev": null
|
|
241
|
+
"total": 0
|
|
130
242
|
}
|
|
131
243
|
```
|
|
132
244
|
|
|
133
|
-
|
|
245
|
+
`data` содержит записи только запрошенной страницы. `total` содержит количество всех записей, соответствующих фильтру, до применения пагинации. Номер последней страницы при необходимости вычисляется на клиенте как `Math.max(1, Math.ceil(total / pageSize))`.
|
|
246
|
+
|
|
247
|
+
Оба параметра пагинации должны быть положительными целыми числами. По умолчанию `_perPage` не может превышать `1000`; лимит меняется через `server.maxPageSize` в конфиге, переданном CLI или `createServer()`. При некорректном значении сервер возвращает `400`, а не исправляет его автоматически. Страница после последней возвращает пустой массив `data`, но сохраняет фактическое значение `total`.
|
|
134
248
|
|
|
135
|
-
|
|
249
|
+
`_sort` принимает пути полей через запятую. Правила применяются слева направо; префикс `-` включает сортировку по убыванию. Через точку можно обратиться к полю вложенного объекта, включая поля, добавленные через `_embed`, например `GET /users?_embed=country&_sort=country.name,-id`. Неизвестные и небезопасные поля сортировки возвращают `400`.
|
|
136
250
|
|
|
137
251
|
## Фильтры
|
|
138
252
|
|
|
139
253
|
Передайте JSON-объект через `_where`:
|
|
140
254
|
|
|
141
255
|
```http
|
|
142
|
-
GET /movies?_where={"title":{"contains":"
|
|
256
|
+
GET /movies?_where={"title":{"contains":"тени"}}
|
|
143
257
|
```
|
|
144
258
|
|
|
145
259
|
Можно фильтровать вложенные объекты и массивы на любой глубине. Условия внутри одного объекта по умолчанию объединяются через `AND`:
|
|
@@ -147,40 +261,61 @@ GET /movies?_where={"title":{"contains":"отец"}}
|
|
|
147
261
|
```json
|
|
148
262
|
{
|
|
149
263
|
"actors": { "some": { "userId": { "eq": "1" } } },
|
|
150
|
-
"title": { "contains": "
|
|
264
|
+
"title": { "contains": "тени" }
|
|
151
265
|
}
|
|
152
266
|
```
|
|
153
267
|
|
|
154
|
-
|
|
268
|
+
Для явных логических групп используйте `and`, `or` и `not`:
|
|
155
269
|
|
|
156
270
|
```json
|
|
157
271
|
{
|
|
158
|
-
"
|
|
159
|
-
{
|
|
160
|
-
|
|
272
|
+
"and": [
|
|
273
|
+
{
|
|
274
|
+
"or": [
|
|
275
|
+
{ "title": { "contains": "тени" } },
|
|
276
|
+
{ "actors": { "some": { "userId": { "eq": "2" } } } }
|
|
277
|
+
]
|
|
278
|
+
},
|
|
279
|
+
{ "not": { "isArchived": { "eq": true } } }
|
|
161
280
|
]
|
|
162
281
|
}
|
|
163
282
|
```
|
|
164
283
|
|
|
165
|
-
|
|
284
|
+
Операторы полей:
|
|
285
|
+
|
|
286
|
+
| Оператор | Поведение |
|
|
287
|
+
| --- | --- |
|
|
288
|
+
| `eq`, `ne` | Равенство или неравенство |
|
|
289
|
+
| `contains` | Подстрока без учёта регистра для строк или совпадающий элемент массива |
|
|
290
|
+
| `startsWith`, `endsWith` | Начало или окончание строки без учёта регистра |
|
|
291
|
+
| `gt`, `gte`, `lt`, `lte` | Сравнение значений; строки дат ISO можно сравнивать лексикографически |
|
|
292
|
+
| `in` | Совпадение скалярного значения или элемента массива с одним из переданных значений |
|
|
293
|
+
| `some`, `every`, `none` | Применение вложенного условия к элементам массива |
|
|
294
|
+
| `not` | Отрицание вложенного условия поля |
|
|
166
295
|
|
|
167
296
|
Также можно использовать простые query-параметры:
|
|
168
297
|
|
|
169
298
|
```http
|
|
170
|
-
GET /movies?title:contains
|
|
299
|
+
GET /movies?title:contains=тени
|
|
171
300
|
```
|
|
172
301
|
|
|
173
302
|
В простых фильтрах распознаются JSON-примитивы: числа, `true`, `false` и `null`. Значения с ведущими нулями, например `001`, остаются строками. Неизвестные операторы, некорректные логические условия и отсутствующие в непустом ресурсе пути фильтра возвращают `400`.
|
|
174
303
|
|
|
304
|
+
Несколько простых query-фильтров объединяются через `AND`. Для оператора `in` перечислите значения через запятую: `GET /movies?id:in=1,2`. Для поля-массива `in` означает, что хотя бы один элемент поля совпадает хотя бы с одним переданным значением. `every` для пустого массива возвращает `true`, а `some` — `false`.
|
|
305
|
+
|
|
306
|
+
Если передан `_where`, он становится полным фильтром, а остальные простые параметры фильтрации игнорируются. В примерах JSON оставлен читаемым; при ручном формировании URL HTTP-клиент должен закодировать значение `_where`, например через `encodeURIComponent(JSON.stringify(where))`.
|
|
307
|
+
|
|
308
|
+
Фильтрация выполняется после `_embed`. Поэтому фильтр может обращаться к полям добавленной связи, если в том же запросе указан соответствующий `_embed`; сохранённые поля `...Id` и `...Ids` всегда можно фильтровать напрямую.
|
|
309
|
+
|
|
175
310
|
## Связи
|
|
176
311
|
|
|
177
|
-
Используйте `_embed`, чтобы
|
|
312
|
+
Используйте `_embed`, чтобы добавить связанные записи в ответ:
|
|
178
313
|
|
|
179
314
|
```http
|
|
180
315
|
GET /movies/1?_embed=actors.user.country&_embed=actors.genres&_embed=publishers
|
|
181
316
|
```
|
|
182
317
|
|
|
183
|
-
Ответ будет содержать актёров, пользователя и жанры каждого актёра, страну пользователя и издателей.
|
|
318
|
+
Ответ будет содержать актёров, пользователя и жанры каждого актёра, страну пользователя и издателей. Исходные поля с ID остаются в ответе, а файл базы не изменяется. Сервер не устанавливает фиксированный лимит глубины, но каждый требуемый уровень должен быть явно указан в конечном пути `_embed`:
|
|
184
319
|
|
|
185
320
|
```http
|
|
186
321
|
GET /movies/1?_embed=actors.user.country
|
|
@@ -189,27 +324,65 @@ GET /genres/2?_embed=parents.parents
|
|
|
189
324
|
|
|
190
325
|
Неизвестные и некорректные пути `_embed` возвращают `400`.
|
|
191
326
|
|
|
327
|
+
Параметр `_embed` можно передать несколько раз, как в примере выше, либо перечислить пути через запятую в одном параметре. Пагинация применяется только к запрошенной корневой коллекции; вложенные связанные записи возвращаются полностью.
|
|
328
|
+
|
|
192
329
|
Поддерживаются и обратные связи:
|
|
193
330
|
|
|
194
331
|
```http
|
|
195
332
|
GET /countries/1?_embed=users
|
|
196
333
|
```
|
|
197
334
|
|
|
198
|
-
Связи определяются по
|
|
335
|
+
Связи определяются по неймингу. Поле `<relation>Id` создаёт одиночную связь, а `<relation>Ids` — связь с коллекцией. Имя связи сопоставляется с ресурсом верхнего уровня напрямую или через его форму в единственном числе. Например:
|
|
199
336
|
|
|
200
337
|
- `countryId` ссылается на `countries`;
|
|
201
338
|
- `userId` ссылается на `users`, если запрошена связь `user`;
|
|
202
339
|
- `genreIds` ссылается на `genres`;
|
|
203
340
|
- `publisherIds` ссылается на `publishers`;
|
|
204
|
-
- `parentIds` ссылается на тот же ресурс, если запрошена связь `_embed=parents
|
|
341
|
+
- `parentIds` ссылается на тот же ресурс, если запрошена связь `_embed=parents`; `_embed=children` загружает обратную связь с дочерними записями.
|
|
342
|
+
|
|
343
|
+
Имя обратной связи совпадает с именем исходного ресурса. Например, `_embed=users` у страны находит пользователей, во вложенных данных которых указан соответствующий `countryId`. Это мягкие ссылки: сервер загружает их по запросу, но не проверяет ссылочную целостность при записи данных.
|
|
344
|
+
|
|
345
|
+
Явное поле `...Id` или `...Ids` считается источником истины. Если в записи также сохранено устаревшее вложенное значение, `_embed` заменяет это свойство ответа актуальной связанной записью. Если цель одиночной связи не найдена, результатом будет `null`; отсутствующие цели связи с коллекцией не попадут в итоговый массив. Для поиска связей лениво создаются ID-индексы только используемых в текущем запросе ресурсов.
|
|
346
|
+
|
|
347
|
+
## Файлы
|
|
348
|
+
|
|
349
|
+
Добавьте `files.directory` и `files.metadata` в конфиг сервера, затем передайте `--files`, чтобы включить загрузку бинарных файлов:
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
deep-json-server --files server.config.js
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Для временных тестов вместо этого используйте `files.data`. Каждая начальная запись содержит `id`, `name`, `mimeType` и бинарное `content` в виде `Uint8Array`; `size` и `url` вычисляются автоматически. Загруженные файлы в таком режиме остаются в памяти до завершения процесса.
|
|
356
|
+
|
|
357
|
+
Один файл отправляется непосредственно в теле запроса. Оба заголовка обязательны: `Content-Name` содержит относительное логическое имя, закодированное через `encodeURIComponent`, а `Content-Type` — MIME-тип файла:
|
|
358
|
+
|
|
359
|
+
```http
|
|
360
|
+
POST /_files
|
|
361
|
+
Content-Name: posters%2Fshadows-of-ardenia.jpg
|
|
362
|
+
Content-Type: image/jpeg
|
|
363
|
+
|
|
364
|
+
<binary body>
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Успешная загрузка возвращает статус `201`, метаданные и постоянный URL:
|
|
368
|
+
|
|
369
|
+
```json
|
|
370
|
+
{
|
|
371
|
+
"id": "generated-id",
|
|
372
|
+
"mimeType": "image/jpeg",
|
|
373
|
+
"name": "posters/shadows-of-ardenia.jpg",
|
|
374
|
+
"size": 182340,
|
|
375
|
+
"url": "/_files/generated-id"
|
|
376
|
+
}
|
|
377
|
+
```
|
|
205
378
|
|
|
206
|
-
|
|
379
|
+
`GET /_files/:id` возвращает исходные байты со статусом `200`, а `DELETE /_files/:id` удаляет бинарное содержимое вместе с метаданными и возвращает удалённые метаданные со статусом `200`. Возвращаемый `url` задаётся относительно адреса mock-сервера. Сервер автоматически создаёт настроенные директории, хранит бинарное содержимое под сгенерированными ID без привязки к исходному имени и записывает логические имена и остальные метаданные в `files.metadata`. Изначально файл метаданных может отсутствовать: он создаётся при первой загрузке. Не редактируйте его во время работы сервера.
|
|
207
380
|
|
|
208
|
-
|
|
381
|
+
Используется бинарное тело запроса, а не `multipart/form-data`, поэтому `XMLHttpRequest.upload.onprogress` может показывать прогресс при непосредственной отправке `File` через `xhr.send(file)`. Максимальный размер по умолчанию равен 100 МиБ и настраивается через `server.maxFileSize`. Отсутствующий или небезопасный `Content-Name` возвращает `400`, превышение лимита — `413`, а отсутствующий, некорректный или не поддерживаемый Fastify `Content-Type` — `400` либо `415` в зависимости от этапа проверки.
|
|
209
382
|
|
|
210
|
-
##
|
|
383
|
+
## Схема базы данных и генерация OpenAPI
|
|
211
384
|
|
|
212
|
-
|
|
385
|
+
Необязательный путь или объект в `database.schema` настраивает автоматически выведенные схемы. Это собственный формат настроек Deep JSON Server, а не стандартный документ JSON Schema: `$schema` здесь является объектом с настройками ресурсов. Например, файл `mock/database-schema.json` может содержать:
|
|
213
386
|
|
|
214
387
|
```json
|
|
215
388
|
{
|
|
@@ -235,13 +408,27 @@ GET /countries/1?_embed=users
|
|
|
235
408
|
}
|
|
236
409
|
```
|
|
237
410
|
|
|
238
|
-
|
|
411
|
+
Настройки схемы:
|
|
412
|
+
|
|
413
|
+
| Ключ | Назначение |
|
|
414
|
+
| --- | --- |
|
|
415
|
+
| `$info` | Объект `info` в OpenAPI; если он указан, обязательны непустые `title` и `version` |
|
|
416
|
+
| `$schema.<resource>.name` | Явное имя компонента, если автоматическое образование единственного числа не подходит или создаёт коллизию |
|
|
417
|
+
| `$schema.<resource>.required` | Пути обязательных полей; вложенные пути записываются через точку, например `actors.userId` |
|
|
418
|
+
| `$schema.<resource>.formats` | Форматы OpenAPI для автоматически найденных или явно описанных строковых полей, например `date`, `date-time` или `uri` |
|
|
419
|
+
| `$schema.<resource>.properties` | Рекурсивные OpenAPI-совместимые схемы полей, объединяемые с автоматически найденными |
|
|
420
|
+
|
|
421
|
+
`formats` — сокращённая запись для назначения `format` уже существующему строковому полю. `properties` позволяет полностью описать поле, в том числе его `type`, `format`, ограничения и вложенные свойства, либо добавить поле, которого нет в данных. Если одно поле получает `format` через оба механизма, значение из `formats` применяется последним.
|
|
422
|
+
|
|
423
|
+
Укажите `openapi.path` в конфиге сервера, затем сгенерируйте OpenAPI 3.0.3 и завершите работу:
|
|
239
424
|
|
|
240
425
|
```bash
|
|
241
|
-
deep-json-server
|
|
426
|
+
deep-json-server --openapi-only server.config.js
|
|
242
427
|
```
|
|
243
428
|
|
|
244
|
-
|
|
429
|
+
Чтобы включить в документ файловые маршруты, настройте секцию `files` и добавьте флаг `--files`: `deep-json-server --files --openapi-only server.config.js`.
|
|
430
|
+
|
|
431
|
+
Генератор определяет ресурсы и типы полей по всем записям базы. По умолчанию все найденные поля необязательные, а поле `id` верхнего уровня всегда обязательное в схемах ответа и исключается из схем создания и обновления. Остальные обязательные поля перечисляются в `required`. Вложенный обязательный путь делает обязательным само вложенное свойство, но не все его родительские пути; при необходимости родителя нужно перечислить отдельно.
|
|
245
432
|
|
|
246
433
|
Разные типы значений определяются независимо и объединяются через `oneOf`. Перед генерацией проверяются `$info`, имена ресурсов и схем, а также структура `properties`; пути из `required` и `formats` должны существовать в итоговой схеме.
|
|
247
434
|
|
|
@@ -261,9 +448,9 @@ deep-json-server mock/database.json --generate mock/database-schema.json mock/op
|
|
|
261
448
|
}
|
|
262
449
|
```
|
|
263
450
|
|
|
264
|
-
Пустой ресурс всё равно получает обязательное строковое поле `id`, поскольку сервер создаёт строковые ID. При совпадении имён схем или `operationId` генерация завершается понятной ошибкой; коллизию имён схем можно устранить с помощью явного `name`.
|
|
451
|
+
Пустой ресурс всё равно получает обязательное строковое поле `id`, поскольку сервер создаёт строковые ID. При совпадении имён схем или `operationId` генерация завершается понятной ошибкой; коллизию имён схем можно устранить с помощью явного `name`. Директория результата создаётся автоматически, а настроенный YAML-файл заменяется при каждой генерации.
|
|
265
452
|
|
|
266
|
-
`$info` становится объектом `info` в OpenAPI, а настройки ресурсов находятся внутри `$schema`.
|
|
453
|
+
`$info` становится объектом `info` в OpenAPI, а настройки ресурсов находятся внутри `$schema`. В CLI поле `servers` формируется из `server.host` и `server.port`, затем из резервных переменных окружения `HOST` и `PORT`, а при их отсутствии используется `http://127.0.0.1:4001`. При прямом вызове `createServer()` переменные окружения автоматически не читаются: `server.openapi()` использует значения конфига или тот же адрес по умолчанию.
|
|
267
454
|
|
|
268
455
|
Используйте `name`, если ресурсу нужно явно задать имя схемы вместо автоматически полученного имени в единственном числе:
|
|
269
456
|
|
|
@@ -277,24 +464,87 @@ deep-json-server mock/database.json --generate mock/database-schema.json mock/op
|
|
|
277
464
|
}
|
|
278
465
|
```
|
|
279
466
|
|
|
280
|
-
В сгенерированном документе описаны CRUD, пагинация, сортировка, глубокие фильтры, `_embed`, а также прямые и обратные связи в ответах, определённые по полям `...Id` и `...Ids`. Числовой ID базы описывается как `integer | string`, поскольку последующий `POST` создаст строковый ID в том же ресурсе. Файл можно передать, например, в RTK Query OpenAPI Codegen. OpenAPI создаётся только с параметром `--
|
|
467
|
+
В сгенерированном документе описаны CRUD, пагинация, сортировка, глубокие фильтры, `_embed`, а также прямые и обратные связи в ответах, определённые по полям `...Id` и `...Ids`. При наличии `--files` в него также добавляются маршруты бинарной загрузки, получения и удаления файлов. Числовой ID базы описывается как `integer | string`, поскольку последующий `POST` создаст строковый ID в том же ресурсе. Файл можно передать, например, в RTK Query OpenAPI Codegen. OpenAPI создаётся только с параметром `--openapi` или `--openapi-only`; обычный запуск сервера файл не перезаписывает.
|
|
281
468
|
|
|
282
|
-
При обычном запуске тела запросов проверяются по автоматически выведенным
|
|
469
|
+
При обычном запуске тела запросов проверяются по тем же автоматически выведенным и настроенным схемам. Для `POST` и `PUT` проверяются обязательные поля из `required`; `PATCH` проверяет только фактически переданные поля. Настройки `formats` и `properties` применяются ко всем трём методам. Не перечисленные дополнительные поля объекта остаются разрешёнными. Некорректные тела возвращают `400`.
|
|
283
470
|
|
|
284
471
|
## Программный API
|
|
285
472
|
|
|
286
473
|
```js
|
|
287
|
-
import { createServer
|
|
474
|
+
import { createServer } from '@kollors/deep-json-server';
|
|
475
|
+
|
|
476
|
+
const config = {
|
|
477
|
+
database: {
|
|
478
|
+
path: 'mock/database.json',
|
|
479
|
+
schema: 'mock/database-schema.json',
|
|
480
|
+
},
|
|
481
|
+
files: {
|
|
482
|
+
directory: 'mock/files',
|
|
483
|
+
metadata: 'mock/files/_database.json',
|
|
484
|
+
},
|
|
485
|
+
openapi: {
|
|
486
|
+
path: 'mock/openapi-schema.yaml',
|
|
487
|
+
},
|
|
488
|
+
server: {
|
|
489
|
+
host: '127.0.0.1',
|
|
490
|
+
logger: false,
|
|
491
|
+
maxFileSize: 100 * 1024 * 1024,
|
|
492
|
+
maxPageSize: 1000,
|
|
493
|
+
port: 4001,
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// Запрос без открытия сетевого порта — удобно для автоматических тестов.
|
|
498
|
+
const server = await createServer(config);
|
|
499
|
+
const fastify = server.fastify();
|
|
500
|
+
const response = await fastify.inject({ method: 'GET', url: '/movies' });
|
|
288
501
|
|
|
289
|
-
|
|
502
|
+
console.log(response.json());
|
|
290
503
|
|
|
291
|
-
|
|
504
|
+
// Возвращает документ и записывает его в config.openapi.path.
|
|
505
|
+
const document = await server.openapi();
|
|
292
506
|
|
|
293
|
-
await
|
|
507
|
+
await fastify.close();
|
|
294
508
|
|
|
295
|
-
|
|
509
|
+
// Запуск сетевого сервера. Вызов без аргументов использует server.host и server.port.
|
|
510
|
+
const runningServer = await createServer(config);
|
|
511
|
+
const runningFastify = runningServer.fastify();
|
|
296
512
|
|
|
297
|
-
await
|
|
513
|
+
await runningFastify.listen();
|
|
514
|
+
|
|
515
|
+
// Позже, при завершении приложения:
|
|
516
|
+
await runningFastify.close();
|
|
517
|
+
|
|
518
|
+
// База, схема и файлы полностью в памяти.
|
|
519
|
+
const memoryServer = await createServer({
|
|
520
|
+
database: {
|
|
521
|
+
data: { movies: [{ id: '1', title: 'Тени Ардении' }] },
|
|
522
|
+
schema: { $info: { title: 'API фильмов', version: '1.0.0' } },
|
|
523
|
+
},
|
|
524
|
+
files: {
|
|
525
|
+
data: [{ content: new Uint8Array([1, 2, 3]), id: 'file-1', mimeType: 'application/octet-stream', name: 'example.bin' }],
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const memoryFastify = memoryServer.fastify();
|
|
530
|
+
const memoryResponse = await memoryFastify.inject({ method: 'GET', url: '/movies/1' });
|
|
531
|
+
|
|
532
|
+
console.log(memoryResponse.json());
|
|
533
|
+
|
|
534
|
+
await memoryFastify.close();
|
|
298
535
|
```
|
|
299
536
|
|
|
300
|
-
`createServer()`
|
|
537
|
+
`createServer()` принимает точно ту же структуру конфига, что и `server.config.js`. Функция загружает и клонирует настроенные источники, а затем возвращает фасад с двумя операциями:
|
|
538
|
+
|
|
539
|
+
| Член | Назначение |
|
|
540
|
+
| --- | --- | --- |
|
|
541
|
+
| `server.fastify()` | Лениво создаёт и кэширует настоящий экземпляр Fastify; все нативные методы доступны, а `listen()` без аргументов использует `server.host` и `server.port` |
|
|
542
|
+
| `server.openapi()` | Возвращает документ OpenAPI и дополнительно записывает его, если настроен `openapi.path` |
|
|
543
|
+
|
|
544
|
+
При программном использовании файловые маршруты включаются при наличии секции `files`. Сигнатура второго аргумента — `{ files?: boolean }`: передайте `{ files: false }`, чтобы оставить настроенное хранилище выключенным, или `{ files: true }`, чтобы потребовать секцию `files` и включить маршруты. `server.openapi()` использует то же состояние возможности, что и `server.fastify()`.
|
|
545
|
+
|
|
546
|
+
Вызов `server.fastify().listen()` без аргументов использует `server.host` и `server.port`, а при их отсутствии — `127.0.0.1:4001`. Явные параметры `listen(options)` имеют приоритет. Относительные пути, переданные напрямую в `createServer()`, вычисляются от текущей рабочей директории; пути из `server.config.js` — от директории конфига. Пакет содержит сгенерированные TypeScript-декларации фасада и всех вариантов конфигурации.
|
|
547
|
+
|
|
548
|
+
## Назначение и безопасность
|
|
549
|
+
|
|
550
|
+
Deep JSON Server предназначен для локальной разработки и автоматических тестов. В нём нет аутентификации и авторизации, CORS разрешён для любого источника, при дисковом хранилище принятые изменения сохраняются в настроенные файлы, а ссылочная целостность не проверяется. Оставляйте адрес loopback по умолчанию, если внешняя среда не предоставляет собственный контроль доступа; не открывайте сервер и файловые маршруты для недоверенной сети.
|
package/index.js
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/** @typedef {import('./src/config.js').DeepJsonServerConfig} DeepJsonServerConfig */
|
|
2
|
+
/** @typedef {import('./src/config.js').DatabaseConfig} DatabaseConfig */
|
|
3
|
+
/** @typedef {import('./src/config.js').FilesConfig} FilesConfig */
|
|
4
|
+
/** @typedef {import('./src/config.js').MemoryFile} MemoryFile */
|
|
5
|
+
/** @typedef {import('./src/server.js').OpenapiDocument} OpenapiDocument */
|
|
6
|
+
|
|
7
|
+
export { createServer } from './src/server.js';
|