@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.ru.md CHANGED
@@ -14,24 +14,67 @@
14
14
  npm install --save-dev @kollors/deep-json-server
15
15
  ```
16
16
 
17
- Добавьте команду в `package.json`:
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 mock/database.json --schema mock/database-schema.json --port 4001",
23
- "openapi": "deep-json-server mock/database.json --generate mock/database-schema.json mock/openapi-schema.yaml"
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
- ```bash
31
- npm run mock
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
- По умолчанию сервер доступен по адресу `http://127.0.0.1:4001`. Адрес и порт можно задать через `--host` и `--port` либо переменные окружения `HOST` и `PORT`.
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, а `PUT` и `PATCH` сохраняют исходный тип ID. Все операции записи — `POST`, `PUT`, `PATCH` и `DELETE` — сохраняют изменения в JSON-файле.
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
- Префикс `-` перед полем включает сортировку по убыванию. Неизвестные и небезопасные поля сортировки возвращают `400`.
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
- "or": [
159
- { "title": { "contains": "отец" } },
160
- { "actors": { "some": { "userId": { "eq": "2" } } } }
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
- Поддерживаются операторы полей: `contains`, `endsWith`, `eq`, `every`, `gt`, `gte`, `in`, `lt`, `lte`, `ne`, `none`, `not`, `some` и `startsWith`.
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`, чтобы заменить ID связанными записями:
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` заменяет его актуальной связанной записью. Для поиска связей лениво создаются ID-индексы только используемых в текущем запросе ресурсов.
278
+ Явное поле `...Id` или `...Ids` считается источником истины. Если в записи также сохранено устаревшее вложенное значение, `_embed` заменяет это свойство ответа актуальной связанной записью. Если цель одиночной связи не найдена, результатом будет `null`; отсутствующие цели связи с коллекцией не попадут в итоговый массив. Для поиска связей лениво создаются ID-индексы только используемых в текущем запросе ресурсов.
209
279
 
210
- ## Генерация OpenAPI
280
+ ## Файлы
211
281
 
212
- Создайте рядом с базой небольшой файл конфигурации, например `mock/database-schema.json`:
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
- Сгенерируйте OpenAPI 3.0.3 и завершите работу:
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 mock/database.json --generate mock/database-schema.json mock/openapi-schema.yaml --host 127.0.0.1 --port 4001
355
+ deep-json-server --openapi --files server.config.js
242
356
  ```
243
357
 
244
- Генератор определяет ресурсы и типы полей по всем записям базы. По умолчанию все найденные поля необязательные, а поле `id` верхнего уровня, присутствующее в итоговой схеме ресурса, всегда обязательное. Остальные обязательные поля перечисляются в `required`; для вложенных полей используются пути через точку, например `actors.userId`. Объект `formats` добавляет форматы OpenAPI, например `date` и `uri`.
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 формируется автоматически из параметров `--host` и `--port`, соответствующих переменных окружения `HOST` и `PORT` или адреса по умолчанию `http://127.0.0.1:4001`.
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 создаётся только с параметром `--generate`; обычный запуск сервера файл не перезаписывает.
394
+ В сгенерированном документе описаны CRUD, пагинация, сортировка, глубокие фильтры, `_embed`, а также прямые и обратные связи в ответах, определённые по полям `...Id` и `...Ids`. При наличии `--files` в него также добавляются маршруты бинарной загрузки, получения и удаления файлов. Числовой ID базы описывается как `integer | string`, поскольку последующий `POST` создаст строковый ID в том же ресурсе. Файл можно передать, например, в RTK Query OpenAPI Codegen. OpenAPI создаётся только с параметром `--openapi`; обычный запуск сервера файл не перезаписывает.
281
395
 
282
- При обычном запуске тела запросов проверяются по автоматически выведенным схемам ресурсов. Передайте `--schema mock/database-schema.json`, чтобы в runtime применялись те же явные ограничения `required`, `formats` и `properties`. Некорректные тела `POST`, `PUT` и `PATCH` возвращают `400`.
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
- const server = await createServer({ databasePath: 'mock/database.json', logger: false, maxPageSize: 1000, schemaPath: 'mock/database-schema.json' });
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
- await startServer({ databasePath: 'mock/database.json', host: '127.0.0.1', port: 4001, schemaPath: 'mock/database-schema.json' });
296
-
297
- await generateOpenApi({ databasePath: 'mock/database.json', host: '127.0.0.1', port: 4001, schemaPath: 'mock/database-schema.json', outputPath: 'mock/openapi-schema.yaml' });
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
- `createServer()` удобен для тестов: он возвращает экземпляр Fastify, не открывая сетевой порт. Пакет содержит сгенерированные TypeScript-декларации для всех экспортируемых функций.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kollors/deep-json-server",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "JSON mock server with deep filters and recursive relationship embedding",
5
5
  "type": "module",
6
6
  "types": "./types/index.d.ts",
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 <database.json> [--schema <database-schema.json>] [--host <host>] [--port <port>]
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
- --generate Сгенерировать OpenAPI и завершить работу
14
- --host, -h Адрес сервера (по умолчанию 127.0.0.1)
15
- --port, -p Порт сервера (по умолчанию 4001)
16
- --schema Проверять запросы записи по указанной схеме
17
- --help Показать справку`;
18
-
19
- const parseOptions = (args, allowedOptions) => {
20
- const options = {};
21
-
22
- for (let index = 0; index < args.length; index += 2) {
23
- const option = args[index];
24
- const value = args[index + 1];
25
-
26
- if (!allowedOptions.includes(option)) {
27
- throw new Error(`Неизвестный параметр: ${option}`);
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
- if (value == null || value.startsWith('-')) {
31
- throw new Error(`Не указано значение параметра ${option}`);
32
- }
35
+ if (configPath == null) {
36
+ throw new Error('Укажите путь к файлу конфигурации');
37
+ }
33
38
 
34
- const optionName = ['--host', '-h'].includes(option) ? 'host' : ['--port', '-p'].includes(option) ? 'port' : 'schemaPath';
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
- options[optionName] = value;
47
+ if (files && config.filesDirectoryPath == null) {
48
+ throw new Error('Для --files укажите ключ config.files.directory');
37
49
  }
38
50
 
39
- return options;
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 [databasePath] = args;
49
-
50
- if (databasePath == null || databasePath.startsWith('-')) {
51
- throw new Error('Укажите путь к JSON-базе данных');
52
- }
53
-
54
- const generateIndex = args.indexOf('--generate');
55
-
56
- if (generateIndex !== -1) {
57
- const schemaPath = args[generateIndex + 1];
58
- const outputPath = args[generateIndex + 2];
59
-
60
- if (generateIndex !== 1 || schemaPath == null || outputPath == null) {
61
- throw new Error('Используйте: deep-json-server <database.json> --generate <database-schema.json> <openapi-schema.yaml> [--host <host>] [--port <port>]');
62
- }
63
-
64
- const options = parseOptions(args.slice(4), ['--host', '-h', '--port', '-p']);
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
- const options = parseOptions(args.slice(1), ['--host', '-h', '--port', '-p', '--schema']);
74
- const host = options.host ?? process.env.HOST ?? DEFAULT_HOST;
75
- const port = Number(options.port ?? process.env.PORT ?? DEFAULT_PORT);
76
-
77
- await startServer({ databasePath, host, port, schemaPath: options.schemaPath });
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
+ }
package/src/constants.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export const DEFAULT_HOST = '127.0.0.1';
2
+ export const DEFAULT_MAX_FILE_SIZE = 100 * 1024 * 1024;
2
3
  export const DEFAULT_PAGE_SIZE = 10;
3
4
  export const DEFAULT_PORT = 4001;
4
5
  export const MAX_PAGE_SIZE = 1000;