@kollors/deep-json-server 0.4.0 → 0.5.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 +201 -38
- package/README.ru.md +201 -38
- package/package.json +1 -1
- package/src/cli.js +63 -51
- package/src/config.js +97 -0
- package/src/constants.js +1 -0
- package/src/files.js +278 -0
- package/src/openapi/document.js +52 -8
- package/src/openapi/index.js +3 -3
- package/src/server.js +30 -7
- package/types/src/constants.d.ts +1 -0
- package/types/src/files.d.ts +5 -0
- package/types/src/openapi/document.d.ts +3 -2
- package/types/src/openapi/index.d.ts +4 -3
- package/types/src/server.d.ts +8 -2
package/README.ru.md
CHANGED
|
@@ -14,24 +14,67 @@
|
|
|
14
14
|
npm install --save-dev @kollors/deep-json-server
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## Конфигурация и запуск
|
|
18
|
+
|
|
19
|
+
Создайте ESM-модуль `server.config.js`. В примере ниже включены настройки всех возможностей:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import process from 'node:process';
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
database: {
|
|
26
|
+
path: process.env.DATABASE_PATH ?? 'mock/database.json',
|
|
27
|
+
schema: 'mock/database-schema.json',
|
|
28
|
+
},
|
|
29
|
+
files: {
|
|
30
|
+
directory: 'mock/files',
|
|
31
|
+
metadata: 'mock/files/_database.json',
|
|
32
|
+
},
|
|
33
|
+
openapi: {
|
|
34
|
+
path: 'mock/openapi-schema.yaml',
|
|
35
|
+
},
|
|
36
|
+
server: {
|
|
37
|
+
host: '127.0.0.1',
|
|
38
|
+
port: 4001,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Ключи конфигурации:
|
|
44
|
+
|
|
45
|
+
| Ключ | Когда обязателен | Назначение |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| `database.path` | Всегда | Существующий JSON-файл базы данных |
|
|
48
|
+
| `database.schema` | Необязателен | JSON-настройки проверки запросов и OpenAPI-схем |
|
|
49
|
+
| `files.directory` | С `--files` | Директория для бинарного содержимого |
|
|
50
|
+
| `files.metadata` | С `--files` | JSON-файл с метаданными загруженных файлов |
|
|
51
|
+
| `openapi.path` | С `--openapi` | Генерируемый YAML-файл OpenAPI |
|
|
52
|
+
| `server.host` | Необязателен | Адрес прослушивания; затем используются `HOST` и `127.0.0.1` |
|
|
53
|
+
| `server.port` | Необязателен | Порт; затем используются `PORT` и `4001` |
|
|
54
|
+
|
|
55
|
+
Все относительные пути вычисляются от директории с `server.config.js`, а не от текущей рабочей директории. Неизвестные ключи, пустые пути и значения некорректных типов отклоняются до запуска. Конфиг является исполняемым JavaScript: в нём можно читать переменные окружения, импортировать другие модули и вычислять значения перед экспортом объекта. Для `.js`-конфига с `export default` проект должен быть ESM (`"type": "module"`); в CommonJS-проекте сохраните тот же конфиг как `server.config.mjs`.
|
|
56
|
+
|
|
57
|
+
Добавьте нужные команды в `package.json`:
|
|
18
58
|
|
|
19
59
|
```json
|
|
20
60
|
{
|
|
21
61
|
"scripts": {
|
|
22
|
-
"mock": "deep-json-server
|
|
23
|
-
"openapi": "deep-json-server
|
|
62
|
+
"mock": "deep-json-server --files server.config.js",
|
|
63
|
+
"openapi": "deep-json-server --openapi --files server.config.js"
|
|
24
64
|
}
|
|
25
65
|
}
|
|
26
66
|
```
|
|
27
67
|
|
|
28
|
-
|
|
68
|
+
Режимы CLI:
|
|
29
69
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
70
|
+
| Команда | Поведение |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| `deep-json-server server.config.js` | Запускает CRUD-сервер без файловых маршрутов |
|
|
73
|
+
| `deep-json-server --files server.config.js` | Запускает CRUD-сервер с файловыми маршрутами |
|
|
74
|
+
| `deep-json-server --openapi server.config.js` | Генерирует OpenAPI и завершает работу |
|
|
75
|
+
| `deep-json-server --openapi --files server.config.js` | Генерирует OpenAPI с файловыми маршрутами и завершает работу |
|
|
33
76
|
|
|
34
|
-
|
|
77
|
+
`--openapi` никогда не запускает HTTP-сервер. Флаг `--files` независим: без него файловые маршруты не регистрируются и не добавляются в OpenAPI. Команда `deep-json-server --help` выводит краткую справку по CLI.
|
|
35
78
|
|
|
36
79
|
## Пример базы данных
|
|
37
80
|
|
|
@@ -106,9 +149,15 @@ PATCH /movies/:id
|
|
|
106
149
|
DELETE /movies/:id
|
|
107
150
|
```
|
|
108
151
|
|
|
109
|
-
`POST` генерирует строковый ID
|
|
152
|
+
`POST` генерирует строковый ID. `PUT` полностью заменяет выбранную запись, а `PATCH` изменяет только переданные поля; обе операции сохраняют существующий ID и его тип. Поле `id` в теле любого запроса не может переопределить ID, которым управляет сервер. Все операции записи — `POST`, `PUT`, `PATCH` и `DELETE` — выполняются последовательно и сохраняются в JSON-файле.
|
|
110
153
|
|
|
111
|
-
Файл базы должен существовать до запуска. Имена ресурсов могут содержать латинские буквы, цифры, `_` и `-` и должны начинаться с буквы. Каждый ресурс является массивом JSON-объектов. У каждой записи должен быть непустой строковый или конечный числовой `id`; ID должны быть уникальны внутри ресурса при сравнении как строки, поэтому `1` и `"1"` не могут существовать одновременно.
|
|
154
|
+
Файл базы должен существовать до запуска. Имена ресурсов могут содержать латинские буквы, цифры, `_` и `-` и должны начинаться с буквы. Каждый ресурс является массивом JSON-объектов. У каждой записи должен быть непустой строковый или конечный числовой `id`; ID должны быть уникальны внутри ресурса при сравнении как строки, поэтому `1` и `"1"` не могут существовать одновременно. Перед каждым GET-запросом и изменением сервер заново читает файл, поэтому корректные внешние правки становятся видны без перезапуска.
|
|
155
|
+
|
|
156
|
+
Успешная операция записи возвращает созданную, заменённую, обновлённую или удалённую запись. Ошибки используют подходящий HTTP-статус и следующий JSON-формат:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{ "error": "Понятное описание ошибки" }
|
|
160
|
+
```
|
|
112
161
|
|
|
113
162
|
## Пагинация и сортировка
|
|
114
163
|
|
|
@@ -132,7 +181,7 @@ GET-запрос к коллекции всегда возвращает объ
|
|
|
132
181
|
|
|
133
182
|
Оба параметра пагинации должны быть положительными целыми числами. По умолчанию `_perPage` не может превышать `1000`; лимит можно изменить программным параметром `maxPageSize`. При некорректном значении сервер возвращает `400`, а не исправляет его автоматически. Страница после последней возвращает пустой массив `data`, а `prev` указывает на последнюю доступную страницу.
|
|
134
183
|
|
|
135
|
-
|
|
184
|
+
`_sort` принимает пути полей через запятую. Правила применяются слева направо; префикс `-` включает сортировку по убыванию. Через точку можно обратиться к полю вложенного объекта, включая поля, добавленные через `_embed`, например `GET /users?_embed=country&_sort=country.name,-id`. Неизвестные и небезопасные поля сортировки возвращают `400`.
|
|
136
185
|
|
|
137
186
|
## Фильтры
|
|
138
187
|
|
|
@@ -151,18 +200,33 @@ GET /movies?_where={"title":{"contains":"отец"}}
|
|
|
151
200
|
}
|
|
152
201
|
```
|
|
153
202
|
|
|
154
|
-
|
|
203
|
+
Для явных логических групп используйте `and`, `or` и `not`:
|
|
155
204
|
|
|
156
205
|
```json
|
|
157
206
|
{
|
|
158
|
-
"
|
|
159
|
-
{
|
|
160
|
-
|
|
207
|
+
"and": [
|
|
208
|
+
{
|
|
209
|
+
"or": [
|
|
210
|
+
{ "title": { "contains": "отец" } },
|
|
211
|
+
{ "actors": { "some": { "userId": { "eq": "2" } } } }
|
|
212
|
+
]
|
|
213
|
+
},
|
|
214
|
+
{ "not": { "isArchived": { "eq": true } } }
|
|
161
215
|
]
|
|
162
216
|
}
|
|
163
217
|
```
|
|
164
218
|
|
|
165
|
-
|
|
219
|
+
Операторы полей:
|
|
220
|
+
|
|
221
|
+
| Оператор | Поведение |
|
|
222
|
+
| --- | --- |
|
|
223
|
+
| `eq`, `ne` | Равенство или неравенство |
|
|
224
|
+
| `contains` | Подстрока без учёта регистра для строк или совпадающий элемент массива |
|
|
225
|
+
| `startsWith`, `endsWith` | Начало или окончание строки без учёта регистра |
|
|
226
|
+
| `gt`, `gte`, `lt`, `lte` | Сравнение значений; строки дат ISO можно сравнивать лексикографически |
|
|
227
|
+
| `in` | Совпадение скалярного значения или элемента массива с одним из переданных значений |
|
|
228
|
+
| `some`, `every`, `none` | Применение вложенного условия к элементам массива |
|
|
229
|
+
| `not` | Отрицание вложенного условия поля |
|
|
166
230
|
|
|
167
231
|
Также можно использовать простые query-параметры:
|
|
168
232
|
|
|
@@ -172,15 +236,19 @@ GET /movies?title:contains=отец
|
|
|
172
236
|
|
|
173
237
|
В простых фильтрах распознаются JSON-примитивы: числа, `true`, `false` и `null`. Значения с ведущими нулями, например `001`, остаются строками. Неизвестные операторы, некорректные логические условия и отсутствующие в непустом ресурсе пути фильтра возвращают `400`.
|
|
174
238
|
|
|
239
|
+
Для простого фильтра `in` перечислите значения через запятую: `GET /movies?id:in=1,2`. Если передан `_where`, он становится полным фильтром, а остальные простые параметры фильтрации игнорируются. В примерах JSON оставлен читаемым; при ручном формировании URL HTTP-клиент должен закодировать значение `_where`.
|
|
240
|
+
|
|
241
|
+
Фильтрация выполняется после `_embed`. Поэтому фильтр может обращаться к полям добавленной связи, если в том же запросе указан соответствующий `_embed`; сохранённые поля `...Id` и `...Ids` всегда можно фильтровать напрямую.
|
|
242
|
+
|
|
175
243
|
## Связи
|
|
176
244
|
|
|
177
|
-
Используйте `_embed`, чтобы
|
|
245
|
+
Используйте `_embed`, чтобы добавить связанные записи в ответ:
|
|
178
246
|
|
|
179
247
|
```http
|
|
180
248
|
GET /movies/1?_embed=actors.user.country&_embed=actors.genres&_embed=publishers
|
|
181
249
|
```
|
|
182
250
|
|
|
183
|
-
Ответ будет содержать актёров, пользователя и жанры каждого актёра, страну пользователя и издателей. Глубина вложения не ограничена:
|
|
251
|
+
Ответ будет содержать актёров, пользователя и жанры каждого актёра, страну пользователя и издателей. Исходные поля с ID остаются в ответе, а файл базы не изменяется. Глубина вложения не ограничена:
|
|
184
252
|
|
|
185
253
|
```http
|
|
186
254
|
GET /movies/1?_embed=actors.user.country
|
|
@@ -189,27 +257,63 @@ GET /genres/2?_embed=parents.parents
|
|
|
189
257
|
|
|
190
258
|
Неизвестные и некорректные пути `_embed` возвращают `400`.
|
|
191
259
|
|
|
260
|
+
Параметр `_embed` можно передать несколько раз, как в примере выше, либо перечислить пути через запятую в одном параметре. Пагинация применяется только к запрошенной корневой коллекции; вложенные связанные записи возвращаются полностью.
|
|
261
|
+
|
|
192
262
|
Поддерживаются и обратные связи:
|
|
193
263
|
|
|
194
264
|
```http
|
|
195
265
|
GET /countries/1?_embed=users
|
|
196
266
|
```
|
|
197
267
|
|
|
198
|
-
Связи определяются по
|
|
268
|
+
Связи определяются по неймингу. Поле `<relation>Id` создаёт одиночную связь, а `<relation>Ids` — связь с коллекцией. Имя связи сопоставляется с ресурсом верхнего уровня напрямую или через его форму в единственном числе. Например:
|
|
199
269
|
|
|
200
270
|
- `countryId` ссылается на `countries`;
|
|
201
271
|
- `userId` ссылается на `users`, если запрошена связь `user`;
|
|
202
272
|
- `genreIds` ссылается на `genres`;
|
|
203
273
|
- `publisherIds` ссылается на `publishers`;
|
|
204
|
-
- `parentIds` ссылается на тот же ресурс, если запрошена связь `_embed=parents
|
|
274
|
+
- `parentIds` ссылается на тот же ресурс, если запрошена связь `_embed=parents`; `_embed=children` загружает обратную связь с дочерними записями.
|
|
205
275
|
|
|
206
|
-
Это мягкие ссылки: сервер загружает их по запросу, но не проверяет ссылочную целостность при записи данных.
|
|
276
|
+
Имя обратной связи совпадает с именем исходного ресурса. Например, `_embed=users` у страны находит пользователей, во вложенных данных которых указан соответствующий `countryId`. Это мягкие ссылки: сервер загружает их по запросу, но не проверяет ссылочную целостность при записи данных.
|
|
207
277
|
|
|
208
|
-
Явное поле `...Id` или `...Ids` считается источником истины. Если в записи также сохранено устаревшее вложенное значение, `_embed` заменяет
|
|
278
|
+
Явное поле `...Id` или `...Ids` считается источником истины. Если в записи также сохранено устаревшее вложенное значение, `_embed` заменяет это свойство ответа актуальной связанной записью. Если цель одиночной связи не найдена, результатом будет `null`; отсутствующие цели связи с коллекцией не попадут в итоговый массив. Для поиска связей лениво создаются ID-индексы только используемых в текущем запросе ресурсов.
|
|
209
279
|
|
|
210
|
-
##
|
|
280
|
+
## Файлы
|
|
211
281
|
|
|
212
|
-
|
|
282
|
+
Добавьте `files.directory` и `files.metadata` в конфиг сервера, затем передайте `--files`, чтобы включить загрузку бинарных файлов:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
deep-json-server --files server.config.js
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Один файл отправляется непосредственно в теле запроса. Оба заголовка обязательны: `Content-Name` содержит относительное логическое имя, закодированное через `encodeURIComponent`, а `Content-Type` — MIME-тип файла:
|
|
289
|
+
|
|
290
|
+
```http
|
|
291
|
+
POST /_files
|
|
292
|
+
Content-Name: posters%2Fthe-godfather.jpg
|
|
293
|
+
Content-Type: image/jpeg
|
|
294
|
+
|
|
295
|
+
<binary body>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Ответ содержит метаданные и постоянный URL:
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"id": "generated-id",
|
|
303
|
+
"mimeType": "image/jpeg",
|
|
304
|
+
"name": "posters/the-godfather.jpg",
|
|
305
|
+
"size": 182340,
|
|
306
|
+
"url": "/_files/generated-id"
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`GET /_files/:id` возвращает исходные байты, а `DELETE /_files/:id` удаляет бинарное содержимое вместе с метаданными. Возвращаемый `url` задаётся относительно адреса mock-сервера. Сервер автоматически создаёт настроенные директории, хранит бинарное содержимое под сгенерированными ID без привязки к исходному имени и записывает логические имена и остальные метаданные в `files.metadata`. Изначально файл метаданных может отсутствовать: он создаётся при первой загрузке. Не редактируйте его во время работы сервера.
|
|
311
|
+
|
|
312
|
+
Используется бинарное тело запроса, а не `multipart/form-data`, поэтому `XMLHttpRequest.upload.onprogress` может показывать прогресс при непосредственной отправке `File` через `xhr.send(file)`. Максимальный размер по умолчанию равен 100 МиБ и настраивается программным параметром `maxFileSize`. Небезопасный или абсолютный путь в `Content-Name` возвращает `400`, превышение лимита — `413`, а некорректный или неподдерживаемый `Content-Type` — `400` или `415`.
|
|
313
|
+
|
|
314
|
+
## Схема базы данных и генерация OpenAPI
|
|
315
|
+
|
|
316
|
+
Необязательный JSON-файл, указанный в `database.schema`, например `mock/database-schema.json`, настраивает автоматически выведенные схемы. Он читается как при обычном запуске сервера, так и при генерации OpenAPI:
|
|
213
317
|
|
|
214
318
|
```json
|
|
215
319
|
{
|
|
@@ -235,13 +339,23 @@ GET /countries/1?_embed=users
|
|
|
235
339
|
}
|
|
236
340
|
```
|
|
237
341
|
|
|
238
|
-
|
|
342
|
+
Настройки схемы:
|
|
343
|
+
|
|
344
|
+
| Ключ | Назначение |
|
|
345
|
+
| --- | --- |
|
|
346
|
+
| `$info` | Объект `info` в OpenAPI; если он указан, обязательны непустые `title` и `version` |
|
|
347
|
+
| `$schema.<resource>.name` | Явное имя компонента, если автоматическое образование единственного числа не подходит или создаёт коллизию |
|
|
348
|
+
| `$schema.<resource>.required` | Пути обязательных полей; вложенные пути записываются через точку, например `actors.userId` |
|
|
349
|
+
| `$schema.<resource>.formats` | Форматы OpenAPI для автоматически найденных или явно описанных строковых полей, например `date`, `date-time` или `uri` |
|
|
350
|
+
| `$schema.<resource>.properties` | Рекурсивные OpenAPI-совместимые схемы полей, объединяемые с автоматически найденными |
|
|
351
|
+
|
|
352
|
+
Укажите `openapi.path` в конфиге сервера, затем сгенерируйте OpenAPI 3.0.3 и завершите работу:
|
|
239
353
|
|
|
240
354
|
```bash
|
|
241
|
-
deep-json-server
|
|
355
|
+
deep-json-server --openapi --files server.config.js
|
|
242
356
|
```
|
|
243
357
|
|
|
244
|
-
Генератор определяет ресурсы и типы полей по всем записям базы. По умолчанию все найденные поля необязательные, а поле `id` верхнего
|
|
358
|
+
Генератор определяет ресурсы и типы полей по всем записям базы. По умолчанию все найденные поля необязательные, а поле `id` верхнего уровня всегда обязательное в схемах ответа и исключается из схем создания и обновления. Остальные обязательные поля перечисляются в `required`. Вложенный обязательный путь делает обязательным само вложенное свойство, но не все его родительские пути; при необходимости родителя нужно перечислить отдельно.
|
|
245
359
|
|
|
246
360
|
Разные типы значений определяются независимо и объединяются через `oneOf`. Перед генерацией проверяются `$info`, имена ресурсов и схем, а также структура `properties`; пути из `required` и `formats` должны существовать в итоговой схеме.
|
|
247
361
|
|
|
@@ -261,9 +375,9 @@ deep-json-server mock/database.json --generate mock/database-schema.json mock/op
|
|
|
261
375
|
}
|
|
262
376
|
```
|
|
263
377
|
|
|
264
|
-
Пустой ресурс всё равно получает обязательное строковое поле `id`, поскольку сервер создаёт строковые ID. При совпадении имён схем или `operationId` генерация завершается понятной ошибкой; коллизию имён схем можно устранить с помощью явного `name`.
|
|
378
|
+
Пустой ресурс всё равно получает обязательное строковое поле `id`, поскольку сервер создаёт строковые ID. При совпадении имён схем или `operationId` генерация завершается понятной ошибкой; коллизию имён схем можно устранить с помощью явного `name`. Директория результата создаётся автоматически, а настроенный YAML-файл заменяется при каждой генерации.
|
|
265
379
|
|
|
266
|
-
`$info` становится объектом `info` в OpenAPI, а настройки ресурсов находятся внутри `$schema`. Поле `servers` в OpenAPI формируется автоматически из
|
|
380
|
+
`$info` становится объектом `info` в OpenAPI, а настройки ресурсов находятся внутри `$schema`. Поле `servers` в OpenAPI формируется автоматически из `server.host` и `server.port`, резервных переменных окружения `HOST` и `PORT` или адреса по умолчанию `http://127.0.0.1:4001`.
|
|
267
381
|
|
|
268
382
|
Используйте `name`, если ресурсу нужно явно задать имя схемы вместо автоматически полученного имени в единственном числе:
|
|
269
383
|
|
|
@@ -277,24 +391,73 @@ deep-json-server mock/database.json --generate mock/database-schema.json mock/op
|
|
|
277
391
|
}
|
|
278
392
|
```
|
|
279
393
|
|
|
280
|
-
В сгенерированном документе описаны CRUD, пагинация, сортировка, глубокие фильтры, `_embed`, а также прямые и обратные связи в ответах, определённые по полям `...Id` и `...Ids`. Числовой ID базы описывается как `integer | string`, поскольку последующий `POST` создаст строковый ID в том же ресурсе. Файл можно передать, например, в RTK Query OpenAPI Codegen. OpenAPI создаётся только с параметром `--
|
|
394
|
+
В сгенерированном документе описаны CRUD, пагинация, сортировка, глубокие фильтры, `_embed`, а также прямые и обратные связи в ответах, определённые по полям `...Id` и `...Ids`. При наличии `--files` в него также добавляются маршруты бинарной загрузки, получения и удаления файлов. Числовой ID базы описывается как `integer | string`, поскольку последующий `POST` создаст строковый ID в том же ресурсе. Файл можно передать, например, в RTK Query OpenAPI Codegen. OpenAPI создаётся только с параметром `--openapi`; обычный запуск сервера файл не перезаписывает.
|
|
281
395
|
|
|
282
|
-
При обычном запуске тела запросов проверяются по автоматически выведенным
|
|
396
|
+
При обычном запуске тела запросов проверяются по тем же автоматически выведенным и настроенным схемам. Для `POST` и `PUT` проверяются обязательные поля из `required`; `PATCH` проверяет только фактически переданные поля. Настройки `formats` и `properties` применяются ко всем трём методам. Не перечисленные дополнительные поля объекта остаются разрешёнными. Некорректные тела возвращают `400`.
|
|
397
|
+
|
|
398
|
+
## Назначение и безопасность
|
|
399
|
+
|
|
400
|
+
Deep JSON Server предназначен для локальной разработки и автоматических тестов. В нём нет аутентификации и авторизации, CORS разрешён для любого источника, принятые изменения напрямую сохраняются в настроенные файлы, а ссылочная целостность не проверяется. Оставляйте адрес loopback по умолчанию, если внешняя среда не предоставляет собственный контроль доступа; не открывайте сервер и файловые маршруты для недоверенной сети.
|
|
283
401
|
|
|
284
402
|
## Программный API
|
|
285
403
|
|
|
286
404
|
```js
|
|
287
|
-
import { createServer, generateOpenApi, startServer } from '@kollors/deep-json-server';
|
|
288
|
-
|
|
289
|
-
|
|
405
|
+
import { createOpenApiDocument, createServer, generateOpenApi, startServer } from '@kollors/deep-json-server';
|
|
406
|
+
|
|
407
|
+
// Создание экземпляра без открытия порта, например для тестов.
|
|
408
|
+
const server = await createServer({
|
|
409
|
+
databasePath: 'mock/database.json',
|
|
410
|
+
filesDirectoryPath: 'mock/files',
|
|
411
|
+
filesMetadataPath: 'mock/files/_database.json',
|
|
412
|
+
logger: false,
|
|
413
|
+
maxFileSize: 100 * 1024 * 1024,
|
|
414
|
+
maxPageSize: 1000,
|
|
415
|
+
schemaPath: 'mock/database-schema.json',
|
|
416
|
+
});
|
|
290
417
|
|
|
291
418
|
const response = await server.inject({ method: 'GET', url: '/movies' });
|
|
292
419
|
|
|
293
420
|
await server.close();
|
|
294
421
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
422
|
+
// Создание экземпляра и начало прослушивания.
|
|
423
|
+
const listeningServer = await startServer({
|
|
424
|
+
databasePath: 'mock/database.json',
|
|
425
|
+
host: '127.0.0.1',
|
|
426
|
+
port: 4001,
|
|
427
|
+
schemaPath: 'mock/database-schema.json',
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
await listeningServer.close();
|
|
431
|
+
|
|
432
|
+
// Чтение файлов базы и схемы с последующей записью OpenAPI YAML.
|
|
433
|
+
await generateOpenApi({
|
|
434
|
+
databasePath: 'mock/database.json',
|
|
435
|
+
files: true,
|
|
436
|
+
host: '127.0.0.1',
|
|
437
|
+
outputPath: 'mock/openapi-schema.yaml',
|
|
438
|
+
port: 4001,
|
|
439
|
+
schemaPath: 'mock/database-schema.json',
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Создание такого же OpenAPI-документа полностью в памяти.
|
|
443
|
+
const document = createOpenApiDocument(
|
|
444
|
+
{ movies: [{ id: '1', title: 'Крёстный отец' }] },
|
|
445
|
+
{ $info: { title: 'API фильмов', version: '1.0.0' } },
|
|
446
|
+
{ files: true, host: '127.0.0.1', port: 4001 },
|
|
447
|
+
);
|
|
298
448
|
```
|
|
299
449
|
|
|
300
|
-
|
|
450
|
+
Параметры программного API:
|
|
451
|
+
|
|
452
|
+
| Параметр | Где используется | Назначение |
|
|
453
|
+
| --- | --- | --- |
|
|
454
|
+
| `databasePath` | `createServer`, `startServer`, `generateOpenApi` | Обязательный путь к JSON-базе |
|
|
455
|
+
| `schemaPath` | Те же три функции | Необязательный путь к схеме базы |
|
|
456
|
+
| `filesDirectoryPath`, `filesMetadataPath` | `createServer`, `startServer` | Необязательная пара, включающая файловые маршруты |
|
|
457
|
+
| `files` | `generateOpenApi`, `createOpenApiDocument` | Нужно ли добавлять файловые маршруты в OpenAPI |
|
|
458
|
+
| `host`, `port` | `startServer`, `generateOpenApi`, `createOpenApiDocument` | Адрес прослушивания или URL в поле `servers` |
|
|
459
|
+
| `logger` | `createServer`, `startServer` | Настройки логгера Fastify; значение по умолчанию — `true` |
|
|
460
|
+
| `maxPageSize`, `maxFileSize` | `createServer`, `startServer` | Ограничения сервера; по умолчанию 1000 записей и 100 МиБ |
|
|
461
|
+
| `outputPath` | `generateOpenApi` | Обязательный путь к генерируемому YAML-файлу |
|
|
462
|
+
|
|
463
|
+
`createServer()` возвращает экземпляр Fastify без открытия сетевого порта, поэтому удобен вместе с `server.inject()` в тестах. `startServer()` дополнительно начинает прослушивание. `generateOpenApi()` читает файлы и записывает YAML, а `createOpenApiDocument()` работает с объектами базы и схемы в памяти и ничего не записывает. Эти функции не читают `server.config.js`: параметры нужно передавать явно. Пакет содержит сгенерированные TypeScript-декларации для всех экспортируемых функций.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
|
+
import { readServerConfig } from './config.js';
|
|
2
3
|
import { DEFAULT_HOST, DEFAULT_PORT } from './constants.js';
|
|
3
4
|
import { generateOpenApi } from './openapi/index.js';
|
|
4
5
|
import { startServer } from './server.js';
|
|
@@ -6,73 +7,84 @@ import { startServer } from './server.js';
|
|
|
6
7
|
const HELP_TEXT = `Deep JSON Server
|
|
7
8
|
|
|
8
9
|
Использование:
|
|
9
|
-
deep-json-server
|
|
10
|
-
deep-json-server <database.json> --generate <database-schema.json> <openapi-schema.yaml> [--host <host>] [--port <port>]
|
|
10
|
+
deep-json-server [--openapi] [--files] <server.config.js>
|
|
11
11
|
|
|
12
12
|
Параметры:
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
throw new Error(`Неизвестный параметр: ${
|
|
13
|
+
--openapi Сгенерировать OpenAPI и завершить работу
|
|
14
|
+
--files Добавить файловые маршруты в сервер или OpenAPI
|
|
15
|
+
--help Показать справку`;
|
|
16
|
+
|
|
17
|
+
const parseArguments = (args) => {
|
|
18
|
+
const flags = { files: false, openapi: false };
|
|
19
|
+
let configPath;
|
|
20
|
+
|
|
21
|
+
args.forEach((argument) => {
|
|
22
|
+
if (argument === '--files') {
|
|
23
|
+
flags.files = true;
|
|
24
|
+
} else if (argument === '--openapi') {
|
|
25
|
+
flags.openapi = true;
|
|
26
|
+
} else if (argument.startsWith('-')) {
|
|
27
|
+
throw new Error(`Неизвестный параметр: ${argument}`);
|
|
28
|
+
} else if (configPath == null) {
|
|
29
|
+
configPath = argument;
|
|
30
|
+
} else {
|
|
31
|
+
throw new Error('Можно указать только один файл конфигурации');
|
|
28
32
|
}
|
|
33
|
+
});
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
if (configPath == null) {
|
|
36
|
+
throw new Error('Укажите путь к файлу конфигурации');
|
|
37
|
+
}
|
|
33
38
|
|
|
34
|
-
|
|
39
|
+
return { configPath, ...flags };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const validateModeConfig = (config, { files, openapi }) => {
|
|
43
|
+
if (openapi && config.openapiPath == null) {
|
|
44
|
+
throw new Error('Для --openapi укажите ключ config.openapi.path');
|
|
45
|
+
}
|
|
35
46
|
|
|
36
|
-
|
|
47
|
+
if (files && config.filesDirectoryPath == null) {
|
|
48
|
+
throw new Error('Для --files укажите ключ config.files.directory');
|
|
37
49
|
}
|
|
38
50
|
|
|
39
|
-
|
|
51
|
+
if (files && config.filesMetadataPath == null) {
|
|
52
|
+
throw new Error('Для --files укажите ключ config.files.metadata');
|
|
53
|
+
}
|
|
40
54
|
};
|
|
41
55
|
|
|
42
|
-
export async function runCli(args = process.argv.slice(2)) {
|
|
56
|
+
export async function runCli(args = process.argv.slice(2), services = { generateOpenApi, startServer }) {
|
|
43
57
|
if (args.includes('--help')) {
|
|
44
58
|
process.stdout.write(`${HELP_TEXT}\n`);
|
|
45
59
|
return;
|
|
46
60
|
}
|
|
47
61
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const host = options.host ?? process.env.HOST ?? DEFAULT_HOST;
|
|
66
|
-
const port = Number(options.port ?? process.env.PORT ?? DEFAULT_PORT);
|
|
67
|
-
|
|
68
|
-
await generateOpenApi({ databasePath, host, outputPath, port, schemaPath });
|
|
69
|
-
process.stdout.write(`OpenAPI-схема сохранена в ${outputPath}\n`);
|
|
62
|
+
const { configPath, files, openapi } = parseArguments(args);
|
|
63
|
+
const config = await readServerConfig(configPath);
|
|
64
|
+
const host = config.host ?? process.env.HOST ?? DEFAULT_HOST;
|
|
65
|
+
const port = config.port ?? Number(process.env.PORT ?? DEFAULT_PORT);
|
|
66
|
+
|
|
67
|
+
validateModeConfig(config, { files, openapi });
|
|
68
|
+
|
|
69
|
+
if (openapi) {
|
|
70
|
+
await services.generateOpenApi({
|
|
71
|
+
databasePath: config.databasePath,
|
|
72
|
+
files,
|
|
73
|
+
host,
|
|
74
|
+
outputPath: config.openapiPath,
|
|
75
|
+
port,
|
|
76
|
+
schemaPath: config.schemaPath,
|
|
77
|
+
});
|
|
78
|
+
process.stdout.write(`OpenAPI-схема сохранена в ${config.openapiPath}\n`);
|
|
70
79
|
return;
|
|
71
80
|
}
|
|
72
81
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
await services.startServer({
|
|
83
|
+
databasePath: config.databasePath,
|
|
84
|
+
filesDirectoryPath: files ? config.filesDirectoryPath : undefined,
|
|
85
|
+
filesMetadataPath: files ? config.filesMetadataPath : undefined,
|
|
86
|
+
host,
|
|
87
|
+
port,
|
|
88
|
+
schemaPath: config.schemaPath,
|
|
89
|
+
});
|
|
78
90
|
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { isObject } from './utils.js';
|
|
4
|
+
|
|
5
|
+
const CONFIG_KEYS = new Set(['database', 'files', 'openapi', 'server']);
|
|
6
|
+
const DATABASE_KEYS = new Set(['path', 'schema']);
|
|
7
|
+
const FILES_KEYS = new Set(['directory', 'metadata']);
|
|
8
|
+
const OPENAPI_KEYS = new Set(['path']);
|
|
9
|
+
const SERVER_KEYS = new Set(['host', 'port']);
|
|
10
|
+
let configImportIndex = 0;
|
|
11
|
+
|
|
12
|
+
const assertKnownKeys = (value, keys, path) => {
|
|
13
|
+
const unknownKey = Object.keys(value).find((key) => !keys.has(key));
|
|
14
|
+
|
|
15
|
+
if (unknownKey != null) {
|
|
16
|
+
throw new Error(`Неизвестный ключ ${path}.${unknownKey}`);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const getObject = (value, path) => {
|
|
21
|
+
if (value == null) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!isObject(value)) {
|
|
26
|
+
throw new Error(`Ключ ${path} должен быть JSON-объектом`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return value;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const getString = (value, path, required = false) => {
|
|
33
|
+
if (value == null && !required) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
38
|
+
throw new Error(`Ключ ${path} должен содержать непустую строку`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const resolveConfigPath = (value, directoryPath) => (value == null ? undefined : resolve(directoryPath, value));
|
|
45
|
+
|
|
46
|
+
export async function readServerConfig(configPath) {
|
|
47
|
+
const resolvedConfigPath = resolve(getString(configPath, 'config', true));
|
|
48
|
+
let config;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const configUrl = pathToFileURL(resolvedConfigPath);
|
|
52
|
+
|
|
53
|
+
configUrl.searchParams.set('deep-json-server-import', String(configImportIndex++));
|
|
54
|
+
config = (await import(configUrl.href)).default;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new Error(`Не удалось загрузить конфигурацию ${resolvedConfigPath}: ${error.message}`, { cause: error });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!isObject(config)) {
|
|
60
|
+
throw new Error('Конфигурация сервера должна экспортировать JSON-объект через export default');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
assertKnownKeys(config, CONFIG_KEYS, 'config');
|
|
64
|
+
|
|
65
|
+
const database = getObject(config.database, 'config.database');
|
|
66
|
+
const files = getObject(config.files, 'config.files');
|
|
67
|
+
const openapi = getObject(config.openapi, 'config.openapi');
|
|
68
|
+
const server = getObject(config.server, 'config.server');
|
|
69
|
+
|
|
70
|
+
assertKnownKeys(database, DATABASE_KEYS, 'config.database');
|
|
71
|
+
assertKnownKeys(files, FILES_KEYS, 'config.files');
|
|
72
|
+
assertKnownKeys(openapi, OPENAPI_KEYS, 'config.openapi');
|
|
73
|
+
assertKnownKeys(server, SERVER_KEYS, 'config.server');
|
|
74
|
+
|
|
75
|
+
const directoryPath = dirname(resolvedConfigPath);
|
|
76
|
+
const databasePath = getString(database.path, 'config.database.path', true);
|
|
77
|
+
const schemaPath = getString(database.schema, 'config.database.schema');
|
|
78
|
+
const openapiPath = getString(openapi.path, 'config.openapi.path');
|
|
79
|
+
const filesDirectory = getString(files.directory, 'config.files.directory');
|
|
80
|
+
const filesMetadata = getString(files.metadata, 'config.files.metadata');
|
|
81
|
+
const host = getString(server.host, 'config.server.host');
|
|
82
|
+
|
|
83
|
+
if (server.port != null && (!Number.isInteger(server.port) || server.port < 0 || server.port > 65_535)) {
|
|
84
|
+
throw new Error('Ключ config.server.port должен быть целым числом от 0 до 65535');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
configPath: resolvedConfigPath,
|
|
89
|
+
databasePath: resolveConfigPath(databasePath, directoryPath),
|
|
90
|
+
filesDirectoryPath: resolveConfigPath(filesDirectory, directoryPath),
|
|
91
|
+
filesMetadataPath: resolveConfigPath(filesMetadata, directoryPath),
|
|
92
|
+
host,
|
|
93
|
+
openapiPath: resolveConfigPath(openapiPath, directoryPath),
|
|
94
|
+
port: server.port,
|
|
95
|
+
schemaPath: resolveConfigPath(schemaPath, directoryPath),
|
|
96
|
+
};
|
|
97
|
+
}
|