@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.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 --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,7 +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-файле. Файл базы должен существовать до запуска и содержать JSON-объект, каждое свойство верхнего уровня которого является массивом записей отдельного ресурса.
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
- "or": [
157
- { "title": { "contains": "отец" } },
158
- { "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 } } }
159
215
  ]
160
216
  }
161
217
  ```
162
218
 
163
- Поддерживаются операторы полей: `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` | Отрицание вложенного условия поля |
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`, чтобы заменить ID связанными записями:
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` заменяет его актуальной связанной записью. Для поиска связей лениво создаются ID-индексы только используемых в текущем запросе ресурсов.
278
+ Явное поле `...Id` или `...Ids` считается источником истины. Если в записи также сохранено устаревшее вложенное значение, `_embed` заменяет это свойство ответа актуальной связанной записью. Если цель одиночной связи не найдена, результатом будет `null`; отсутствующие цели связи с коллекцией не попадут в итоговый массив. Для поиска связей лениво создаются ID-индексы только используемых в текущем запросе ресурсов.
205
279
 
206
- ## Генерация OpenAPI
280
+ ## Файлы
207
281
 
208
- Создайте рядом с базой небольшой файл конфигурации, например `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:
209
317
 
210
318
  ```json
211
319
  {
@@ -231,13 +339,23 @@ GET /countries/1?_embed=users
231
339
  }
232
340
  ```
233
341
 
234
- Сгенерируйте 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 и завершите работу:
235
353
 
236
354
  ```bash
237
- 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
238
356
  ```
239
357
 
240
- Генератор определяет ресурсы и типы полей по всем записям базы. По умолчанию все найденные поля необязательные, а поле `id` верхнего уровня, присутствующее в итоговой схеме ресурса, всегда обязательное. Остальные обязательные поля перечисляются в `required`; для вложенных полей используются пути через точку, например `actors.userId`. Объект `formats` добавляет форматы OpenAPI, например `date` и `uri`.
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 формируется автоматически из параметров `--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`.
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` и связи в ответах, определённые по полям `...Id` и `...Ids`. Файл можно передать, например, в RTK Query OpenAPI Codegen. OpenAPI создаётся только с параметром `--generate`; обычный запуск сервера файл не перезаписывает.
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
- const server = await createServer({ databasePath: 'mock/database.json', logger: false });
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
- await startServer({ databasePath: 'mock/database.json', host: '127.0.0.1', port: 4001 });
290
-
291
- 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
+ );
292
448
  ```
293
449
 
294
- `createServer()` удобен для тестов: он возвращает экземпляр Fastify, не открывая сетевой порт.
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-декларации для всех экспортируемых функций.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ import process from 'node:process';
4
+ import { runCli } from '../src/cli.js';
5
+
6
+ try {
7
+ await runCli();
8
+ } catch (error) {
9
+ process.stderr.write(`${error.message}\n`);
10
+ process.exitCode = 1;
11
+ }
package/index.js CHANGED
@@ -1,12 +1,2 @@
1
- #!/usr/bin/env node
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.2",
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": "index.js"
8
+ "deep-json-server": "bin/deep-json-server.js"
8
9
  },
9
10
  "exports": {
10
- ".": "./index.js"
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
- "test": "node --test"
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 { generateOpenApi } from './openapi.js';
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 HELP = `Deep JSON Server
7
+ const HELP_TEXT = `Deep JSON Server
6
8
 
7
9
  Использование:
8
- deep-json-server <database.json> [--host <host>] [--port <port>]
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
- --generate Сгенерировать OpenAPI и завершить работу
13
- --host, -h Адрес сервера (по умолчанию 127.0.0.1)
14
- --port, -p Порт сервера (по умолчанию 4001)
15
- --help Показать справку`;
16
-
17
- const parseServerOptions = (args) => {
18
- const options = {};
19
-
20
- for (let index = 1; index < args.length; index += 2) {
21
- const option = args[index];
22
- const value = args[index + 1];
23
-
24
- if (!['--host', '-h', '--port', '-p'].includes(option)) {
25
- throw new Error(`Неизвестный параметр: ${option}`);
26
- }
27
-
28
- if (value == null || value.startsWith('-')) {
29
- 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('Можно указать только один файл конфигурации');
30
32
  }
33
+ });
31
34
 
32
- options[['--host', '-h'].includes(option) ? 'host' : 'port'] = value;
35
+ if (configPath == null) {
36
+ throw new Error('Укажите путь к файлу конфигурации');
33
37
  }
34
38
 
35
- return options;
39
+ return { configPath, ...flags };
36
40
  };
37
41
 
38
- export async function runCli(args = process.argv.slice(2)) {
39
- if (args.includes('--help')) {
40
- process.stdout.write(`${HELP}\n`);
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
- const [databasePath] = args;
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
- const generateIndex = args.indexOf('--generate');
51
-
52
- if (generateIndex !== -1) {
53
- const schemaPath = args[generateIndex + 1];
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
- await generateOpenApi({ databasePath, host, outputPath, port, schemaPath });
65
- process.stdout.write(`OpenAPI-схема сохранена в ${outputPath}\n`);
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 options = parseServerOptions(args);
70
- const host = options.host ?? process.env.HOST ?? '127.0.0.1';
71
- const port = Number(options.port ?? process.env.PORT ?? 4001);
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({ databasePath, host, port });
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
  }