@kollors/deep-json-server 0.3.2 → 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 +206 -37
- package/README.ru.md +206 -37
- package/bin/deep-json-server.js +11 -0
- package/index.js +1 -11
- package/package.json +22 -5
- package/src/cli.js +67 -51
- package/src/config.js +97 -0
- package/src/constants.js +5 -0
- package/src/database.js +128 -0
- package/src/files.js +278 -0
- package/src/openapi/config.js +110 -0
- package/src/openapi/document.js +331 -0
- package/src/openapi/index.js +25 -0
- package/src/openapi/inference.js +187 -0
- package/src/{query.js → query/filter.js} +23 -113
- package/src/query/index.js +3 -0
- package/src/query/pagination.js +50 -0
- package/src/query/sort.js +63 -0
- package/src/relation-metadata.js +40 -0
- package/src/relations.js +62 -24
- package/src/server.js +132 -101
- package/src/utils.js +7 -18
- package/types/index.d.ts +2 -0
- package/types/src/constants.d.ts +5 -0
- package/types/src/database.d.ts +8 -0
- package/types/src/files.d.ts +5 -0
- package/types/src/openapi/config.d.ts +3 -0
- package/types/src/openapi/document.d.ts +12 -0
- package/types/src/openapi/index.d.ts +14 -0
- package/types/src/openapi/inference.d.ts +9 -0
- package/types/src/query/filter.d.ts +3 -0
- package/types/src/query/index.d.ts +3 -0
- package/types/src/query/pagination.d.ts +13 -0
- package/types/src/query/sort.d.ts +1 -0
- package/types/src/relation-metadata.d.ts +9 -0
- package/types/src/relations.d.ts +3 -0
- package/types/src/server.d.ts +30 -0
- package/types/src/utils.d.ts +10 -0
- package/src/openapi.js +0 -520
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,7 +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-файле.
|
|
153
|
+
|
|
154
|
+
Файл базы должен существовать до запуска. Имена ресурсов могут содержать латинские буквы, цифры, `_` и `-` и должны начинаться с буквы. Каждый ресурс является массивом JSON-объектов. У каждой записи должен быть непустой строковый или конечный числовой `id`; ID должны быть уникальны внутри ресурса при сравнении как строки, поэтому `1` и `"1"` не могут существовать одновременно. Перед каждым GET-запросом и изменением сервер заново читает файл, поэтому корректные внешние правки становятся видны без перезапуска.
|
|
155
|
+
|
|
156
|
+
Успешная операция записи возвращает созданную, заменённую, обновлённую или удалённую запись. Ошибки используют подходящий HTTP-статус и следующий JSON-формат:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{ "error": "Понятное описание ошибки" }
|
|
160
|
+
```
|
|
110
161
|
|
|
111
162
|
## Пагинация и сортировка
|
|
112
163
|
|
|
@@ -128,9 +179,9 @@ GET-запрос к коллекции всегда возвращает объ
|
|
|
128
179
|
}
|
|
129
180
|
```
|
|
130
181
|
|
|
131
|
-
Оба параметра пагинации должны быть положительными целыми числами. При некорректном значении сервер возвращает `400`, а не исправляет его автоматически.
|
|
182
|
+
Оба параметра пагинации должны быть положительными целыми числами. По умолчанию `_perPage` не может превышать `1000`; лимит можно изменить программным параметром `maxPageSize`. При некорректном значении сервер возвращает `400`, а не исправляет его автоматически. Страница после последней возвращает пустой массив `data`, а `prev` указывает на последнюю доступную страницу.
|
|
132
183
|
|
|
133
|
-
|
|
184
|
+
`_sort` принимает пути полей через запятую. Правила применяются слева направо; префикс `-` включает сортировку по убыванию. Через точку можно обратиться к полю вложенного объекта, включая поля, добавленные через `_embed`, например `GET /users?_embed=country&_sort=country.name,-id`. Неизвестные и небезопасные поля сортировки возвращают `400`.
|
|
134
185
|
|
|
135
186
|
## Фильтры
|
|
136
187
|
|
|
@@ -149,18 +200,33 @@ GET /movies?_where={"title":{"contains":"отец"}}
|
|
|
149
200
|
}
|
|
150
201
|
```
|
|
151
202
|
|
|
152
|
-
|
|
203
|
+
Для явных логических групп используйте `and`, `or` и `not`:
|
|
153
204
|
|
|
154
205
|
```json
|
|
155
206
|
{
|
|
156
|
-
"
|
|
157
|
-
{
|
|
158
|
-
|
|
207
|
+
"and": [
|
|
208
|
+
{
|
|
209
|
+
"or": [
|
|
210
|
+
{ "title": { "contains": "отец" } },
|
|
211
|
+
{ "actors": { "some": { "userId": { "eq": "2" } } } }
|
|
212
|
+
]
|
|
213
|
+
},
|
|
214
|
+
{ "not": { "isArchived": { "eq": true } } }
|
|
159
215
|
]
|
|
160
216
|
}
|
|
161
217
|
```
|
|
162
218
|
|
|
163
|
-
|
|
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` | Отрицание вложенного условия поля |
|
|
164
230
|
|
|
165
231
|
Также можно использовать простые query-параметры:
|
|
166
232
|
|
|
@@ -170,42 +236,84 @@ GET /movies?title:contains=отец
|
|
|
170
236
|
|
|
171
237
|
В простых фильтрах распознаются JSON-примитивы: числа, `true`, `false` и `null`. Значения с ведущими нулями, например `001`, остаются строками. Неизвестные операторы, некорректные логические условия и отсутствующие в непустом ресурсе пути фильтра возвращают `400`.
|
|
172
238
|
|
|
239
|
+
Для простого фильтра `in` перечислите значения через запятую: `GET /movies?id:in=1,2`. Если передан `_where`, он становится полным фильтром, а остальные простые параметры фильтрации игнорируются. В примерах JSON оставлен читаемым; при ручном формировании URL HTTP-клиент должен закодировать значение `_where`.
|
|
240
|
+
|
|
241
|
+
Фильтрация выполняется после `_embed`. Поэтому фильтр может обращаться к полям добавленной связи, если в том же запросе указан соответствующий `_embed`; сохранённые поля `...Id` и `...Ids` всегда можно фильтровать напрямую.
|
|
242
|
+
|
|
173
243
|
## Связи
|
|
174
244
|
|
|
175
|
-
Используйте `_embed`, чтобы
|
|
245
|
+
Используйте `_embed`, чтобы добавить связанные записи в ответ:
|
|
176
246
|
|
|
177
247
|
```http
|
|
178
248
|
GET /movies/1?_embed=actors.user.country&_embed=actors.genres&_embed=publishers
|
|
179
249
|
```
|
|
180
250
|
|
|
181
|
-
Ответ будет содержать актёров, пользователя и жанры каждого актёра, страну пользователя и издателей. Глубина вложения не ограничена:
|
|
251
|
+
Ответ будет содержать актёров, пользователя и жанры каждого актёра, страну пользователя и издателей. Исходные поля с ID остаются в ответе, а файл базы не изменяется. Глубина вложения не ограничена:
|
|
182
252
|
|
|
183
253
|
```http
|
|
184
254
|
GET /movies/1?_embed=actors.user.country
|
|
185
255
|
GET /genres/2?_embed=parents.parents
|
|
186
256
|
```
|
|
187
257
|
|
|
258
|
+
Неизвестные и некорректные пути `_embed` возвращают `400`.
|
|
259
|
+
|
|
260
|
+
Параметр `_embed` можно передать несколько раз, как в примере выше, либо перечислить пути через запятую в одном параметре. Пагинация применяется только к запрошенной корневой коллекции; вложенные связанные записи возвращаются полностью.
|
|
261
|
+
|
|
188
262
|
Поддерживаются и обратные связи:
|
|
189
263
|
|
|
190
264
|
```http
|
|
191
265
|
GET /countries/1?_embed=users
|
|
192
266
|
```
|
|
193
267
|
|
|
194
|
-
Связи определяются по
|
|
268
|
+
Связи определяются по неймингу. Поле `<relation>Id` создаёт одиночную связь, а `<relation>Ids` — связь с коллекцией. Имя связи сопоставляется с ресурсом верхнего уровня напрямую или через его форму в единственном числе. Например:
|
|
195
269
|
|
|
196
270
|
- `countryId` ссылается на `countries`;
|
|
197
271
|
- `userId` ссылается на `users`, если запрошена связь `user`;
|
|
198
272
|
- `genreIds` ссылается на `genres`;
|
|
199
273
|
- `publisherIds` ссылается на `publishers`;
|
|
200
|
-
- `parentIds` ссылается на тот же ресурс, если запрошена связь `_embed=parents
|
|
274
|
+
- `parentIds` ссылается на тот же ресурс, если запрошена связь `_embed=parents`; `_embed=children` загружает обратную связь с дочерними записями.
|
|
201
275
|
|
|
202
|
-
Это мягкие ссылки: сервер загружает их по запросу, но не проверяет ссылочную целостность при записи данных.
|
|
276
|
+
Имя обратной связи совпадает с именем исходного ресурса. Например, `_embed=users` у страны находит пользователей, во вложенных данных которых указан соответствующий `countryId`. Это мягкие ссылки: сервер загружает их по запросу, но не проверяет ссылочную целостность при записи данных.
|
|
203
277
|
|
|
204
|
-
Явное поле `...Id` или `...Ids` считается источником истины. Если в записи также сохранено устаревшее вложенное значение, `_embed` заменяет
|
|
278
|
+
Явное поле `...Id` или `...Ids` считается источником истины. Если в записи также сохранено устаревшее вложенное значение, `_embed` заменяет это свойство ответа актуальной связанной записью. Если цель одиночной связи не найдена, результатом будет `null`; отсутствующие цели связи с коллекцией не попадут в итоговый массив. Для поиска связей лениво создаются ID-индексы только используемых в текущем запросе ресурсов.
|
|
205
279
|
|
|
206
|
-
##
|
|
280
|
+
## Файлы
|
|
207
281
|
|
|
208
|
-
|
|
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:
|
|
209
317
|
|
|
210
318
|
```json
|
|
211
319
|
{
|
|
@@ -231,13 +339,23 @@ GET /countries/1?_embed=users
|
|
|
231
339
|
}
|
|
232
340
|
```
|
|
233
341
|
|
|
234
|
-
|
|
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 и завершите работу:
|
|
235
353
|
|
|
236
354
|
```bash
|
|
237
|
-
deep-json-server
|
|
355
|
+
deep-json-server --openapi --files server.config.js
|
|
238
356
|
```
|
|
239
357
|
|
|
240
|
-
Генератор определяет ресурсы и типы полей по всем записям базы. По умолчанию все найденные поля необязательные, а поле `id` верхнего
|
|
358
|
+
Генератор определяет ресурсы и типы полей по всем записям базы. По умолчанию все найденные поля необязательные, а поле `id` верхнего уровня всегда обязательное в схемах ответа и исключается из схем создания и обновления. Остальные обязательные поля перечисляются в `required`. Вложенный обязательный путь делает обязательным само вложенное свойство, но не все его родительские пути; при необходимости родителя нужно перечислить отдельно.
|
|
241
359
|
|
|
242
360
|
Разные типы значений определяются независимо и объединяются через `oneOf`. Перед генерацией проверяются `$info`, имена ресурсов и схем, а также структура `properties`; пути из `required` и `formats` должны существовать в итоговой схеме.
|
|
243
361
|
|
|
@@ -257,9 +375,9 @@ deep-json-server mock/database.json --generate mock/database-schema.json mock/op
|
|
|
257
375
|
}
|
|
258
376
|
```
|
|
259
377
|
|
|
260
|
-
Пустой ресурс всё равно получает обязательное строковое поле `id`, поскольку сервер создаёт строковые ID. При совпадении имён схем или `operationId` генерация завершается понятной ошибкой; коллизию имён схем можно устранить с помощью явного `name`.
|
|
378
|
+
Пустой ресурс всё равно получает обязательное строковое поле `id`, поскольку сервер создаёт строковые ID. При совпадении имён схем или `operationId` генерация завершается понятной ошибкой; коллизию имён схем можно устранить с помощью явного `name`. Директория результата создаётся автоматически, а настроенный YAML-файл заменяется при каждой генерации.
|
|
261
379
|
|
|
262
|
-
`$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`.
|
|
263
381
|
|
|
264
382
|
Используйте `name`, если ресурсу нужно явно задать имя схемы вместо автоматически полученного имени в единственном числе:
|
|
265
383
|
|
|
@@ -273,22 +391,73 @@ deep-json-server mock/database.json --generate mock/database-schema.json mock/op
|
|
|
273
391
|
}
|
|
274
392
|
```
|
|
275
393
|
|
|
276
|
-
В сгенерированном документе описаны CRUD, пагинация, сортировка, глубокие фильтры, `_embed
|
|
394
|
+
В сгенерированном документе описаны CRUD, пагинация, сортировка, глубокие фильтры, `_embed`, а также прямые и обратные связи в ответах, определённые по полям `...Id` и `...Ids`. При наличии `--files` в него также добавляются маршруты бинарной загрузки, получения и удаления файлов. Числовой ID базы описывается как `integer | string`, поскольку последующий `POST` создаст строковый ID в том же ресурсе. Файл можно передать, например, в RTK Query OpenAPI Codegen. OpenAPI создаётся только с параметром `--openapi`; обычный запуск сервера файл не перезаписывает.
|
|
395
|
+
|
|
396
|
+
При обычном запуске тела запросов проверяются по тем же автоматически выведенным и настроенным схемам. Для `POST` и `PUT` проверяются обязательные поля из `required`; `PATCH` проверяет только фактически переданные поля. Настройки `formats` и `properties` применяются ко всем трём методам. Не перечисленные дополнительные поля объекта остаются разрешёнными. Некорректные тела возвращают `400`.
|
|
397
|
+
|
|
398
|
+
## Назначение и безопасность
|
|
399
|
+
|
|
400
|
+
Deep JSON Server предназначен для локальной разработки и автоматических тестов. В нём нет аутентификации и авторизации, CORS разрешён для любого источника, принятые изменения напрямую сохраняются в настроенные файлы, а ссылочная целостность не проверяется. Оставляйте адрес loopback по умолчанию, если внешняя среда не предоставляет собственный контроль доступа; не открывайте сервер и файловые маршруты для недоверенной сети.
|
|
277
401
|
|
|
278
402
|
## Программный API
|
|
279
403
|
|
|
280
404
|
```js
|
|
281
|
-
import { createServer, generateOpenApi, startServer } from '@kollors/deep-json-server';
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
});
|
|
284
417
|
|
|
285
418
|
const response = await server.inject({ method: 'GET', url: '/movies' });
|
|
286
419
|
|
|
287
420
|
await server.close();
|
|
288
421
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
+
);
|
|
292
448
|
```
|
|
293
449
|
|
|
294
|
-
|
|
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/index.js
CHANGED
|
@@ -1,12 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import process from 'node:process';
|
|
4
|
-
import { runCli } from './src/cli.js';
|
|
5
|
-
import { isMainModule } from './src/utils.js';
|
|
6
|
-
|
|
7
|
-
export { createOpenApiDocument, generateOpenApi } from './src/openapi.js';
|
|
1
|
+
export { createOpenApiDocument, generateOpenApi } from './src/openapi/index.js';
|
|
8
2
|
export { createServer, startServer } from './src/server.js';
|
|
9
|
-
|
|
10
|
-
if (isMainModule(process.argv[1], import.meta.url)) {
|
|
11
|
-
await runCli();
|
|
12
|
-
}
|
package/package.json
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kollors/deep-json-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "JSON mock server with deep filters and recursive relationship embedding",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"types": "./types/index.d.ts",
|
|
6
7
|
"bin": {
|
|
7
|
-
"deep-json-server": "
|
|
8
|
+
"deep-json-server": "bin/deep-json-server.js"
|
|
8
9
|
},
|
|
9
10
|
"exports": {
|
|
10
|
-
".":
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./types/index.d.ts",
|
|
13
|
+
"import": "./index.js"
|
|
14
|
+
}
|
|
11
15
|
},
|
|
12
16
|
"files": [
|
|
17
|
+
"bin",
|
|
13
18
|
"index.js",
|
|
14
19
|
"src",
|
|
20
|
+
"types",
|
|
15
21
|
"LICENSE",
|
|
16
22
|
"README.md",
|
|
17
23
|
"README.ru.md"
|
|
18
24
|
],
|
|
19
25
|
"scripts": {
|
|
20
|
-
"check": "node --check index.js && node --check src/*.js",
|
|
21
|
-
"
|
|
26
|
+
"check": "node --check index.js && node --check bin/deep-json-server.js && node --check src/*.js && node --check src/openapi/*.js && node --check src/query/*.js",
|
|
27
|
+
"lint": "biome check .",
|
|
28
|
+
"lint:fix": "biome check --write .",
|
|
29
|
+
"prepack": "npm run types",
|
|
30
|
+
"prepublishOnly": "npm run verify",
|
|
31
|
+
"test": "node --test",
|
|
32
|
+
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=90 --test-coverage-branches=80 --test-coverage-functions=90",
|
|
33
|
+
"types": "tsc -p tsconfig.types.json",
|
|
34
|
+
"verify": "npm run check && npm run lint && npm run test:coverage && npm run types"
|
|
22
35
|
},
|
|
23
36
|
"engines": {
|
|
24
37
|
"node": ">=20"
|
|
@@ -49,5 +62,9 @@
|
|
|
49
62
|
"license": "MIT",
|
|
50
63
|
"publishConfig": {
|
|
51
64
|
"access": "public"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@biomejs/biome": "^2.5.11",
|
|
68
|
+
"typescript": "^7.0.2"
|
|
52
69
|
}
|
|
53
70
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,74 +1,90 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
|
-
import {
|
|
2
|
+
import { readServerConfig } from './config.js';
|
|
3
|
+
import { DEFAULT_HOST, DEFAULT_PORT } from './constants.js';
|
|
4
|
+
import { generateOpenApi } from './openapi/index.js';
|
|
3
5
|
import { startServer } from './server.js';
|
|
4
6
|
|
|
5
|
-
const
|
|
7
|
+
const HELP_TEXT = `Deep JSON Server
|
|
6
8
|
|
|
7
9
|
Использование:
|
|
8
|
-
deep-json-server
|
|
9
|
-
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>
|
|
10
11
|
|
|
11
12
|
Параметры:
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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('Можно указать только один файл конфигурации');
|
|
30
32
|
}
|
|
33
|
+
});
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
if (configPath == null) {
|
|
36
|
+
throw new Error('Укажите путь к файлу конфигурации');
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
return
|
|
39
|
+
return { configPath, ...flags };
|
|
36
40
|
};
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
return;
|
|
42
|
+
const validateModeConfig = (config, { files, openapi }) => {
|
|
43
|
+
if (openapi && config.openapiPath == null) {
|
|
44
|
+
throw new Error('Для --openapi укажите ключ config.openapi.path');
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (databasePath == null || databasePath.startsWith('-')) {
|
|
47
|
-
throw new Error('Укажите путь к JSON-базе данных');
|
|
47
|
+
if (files && config.filesDirectoryPath == null) {
|
|
48
|
+
throw new Error('Для --files укажите ключ config.files.directory');
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const outputPath = args[generateIndex + 2];
|
|
55
|
-
|
|
56
|
-
if (generateIndex !== 1 || schemaPath == null || outputPath == null) {
|
|
57
|
-
throw new Error('Используйте: deep-json-server <database.json> --generate <database-schema.json> <openapi-schema.yaml> [--host <host>] [--port <port>]');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const options = parseServerOptions([databasePath, ...args.slice(4)]);
|
|
61
|
-
const host = options.host ?? process.env.HOST ?? '127.0.0.1';
|
|
62
|
-
const port = Number(options.port ?? process.env.PORT ?? 4001);
|
|
51
|
+
if (files && config.filesMetadataPath == null) {
|
|
52
|
+
throw new Error('Для --files укажите ключ config.files.metadata');
|
|
53
|
+
}
|
|
54
|
+
};
|
|
63
55
|
|
|
64
|
-
|
|
65
|
-
|
|
56
|
+
export async function runCli(args = process.argv.slice(2), services = { generateOpenApi, startServer }) {
|
|
57
|
+
if (args.includes('--help')) {
|
|
58
|
+
process.stdout.write(`${HELP_TEXT}\n`);
|
|
66
59
|
return;
|
|
67
60
|
}
|
|
68
61
|
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
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`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
72
81
|
|
|
73
|
-
await startServer({
|
|
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
|
+
});
|
|
74
90
|
}
|