@kollors/deep-json-server 1.0.0-rc.8 → 1.0.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
@@ -4,21 +4,19 @@
4
4
 
5
5
  [История выпусков](CHANGELOG.md) · [Переход с 0.9.0](MIGRATION.ru.md)
6
6
 
7
- JSON-сервер для создания тестового API с REST, GraphQL, связями между записями, загрузкой файлов и экспортом схем. Поддерживает вход пользователей, права владельца и администратора, даты записей и мягкое удаление. Требуется Node.js 22 или новее.
8
-
9
- **Изменения в 1.0.0, нарушающие совместимость.** При переходе с 0.9.0 воспользуйтесь [инструкцией по миграции](MIGRATION.ru.md). `*` в REST `scope` выбирает только скалярные поля, которые не хранят ключи связей. Массивы, объекты, связи и явно объявленные ключи связей нужно указывать в выборе полей. При включённой аутентификации права на запись доступны через виртуальное поле `actions`.
7
+ JSON-сервер для тестового API с REST, GraphQL, связями между записями, загрузкой файлов и экспортом схем. Поддерживает аутентификацию, права владельца и администратора, даты создания и изменения записей, а также мягкое удаление. Требуется Node.js 22 или новее.
10
8
 
11
9
  ## Установка
12
10
 
13
11
  ```sh
14
- npm install @kollors/deep-json-server@rc
12
+ npm install @kollors/deep-json-server
15
13
  ```
16
14
 
17
- Для установки этого релиз-кандидата укажите `@1.0.0-rc.8`.
15
+ Чтобы установить именно версию 1.0.0, выполните `npm install @kollors/deep-json-server@1.0.0`.
18
16
 
19
17
  ## Быстрый старт
20
18
 
21
- Создайте два файла в одном каталоге.
19
+ Создайте два файла в одном каталоге. У конфигурации расширение `.mjs`, поскольку это ES-модуль. Если используете `.js`, добавьте `"type": "module"` в `package.json` проекта.
22
20
 
23
21
  `database.json`:
24
22
 
@@ -30,14 +28,14 @@ npm install @kollors/deep-json-server@rc
30
28
  }
31
29
  ```
32
30
 
33
- `server.config.js`:
31
+ `server.config.mjs`:
34
32
 
35
33
  ```js
36
34
  export default { storage: 'file', database: { source: './database.json' } };
37
35
  ```
38
36
 
39
37
  ```sh
40
- npx deep-json-server server.config.js
38
+ npx deep-json-server server.config.mjs
41
39
  ```
42
40
 
43
41
  Список пользователей доступен по адресу `http://127.0.0.1:4001/users`.
@@ -46,7 +44,9 @@ npx deep-json-server server.config.js
46
44
 
47
45
  ## Конфигурация
48
46
 
49
- При `storage: 'file'` задайте пути для всех источников и схемы, при `'memory'` — передайте данные напрямую. Используйте один режим во всей конфигурации. Секции `auth` и `files` включают соответствующие REST-маршруты. Секция `graphql` настраивает маршрут GraphQL, а `openapi` публикует описание REST. `graphql: {}` и `openapi: {}` используют стандартные пути; для экспорта схемы добавьте `target`. GraphQL должен быть также включён корневым `api` схемы моделей.
47
+ При `storage: 'file'` укажите пути ко всем источникам данных и схеме, при `'memory'` — передайте сами данные. Режим хранения общий для всей конфигурации.
48
+
49
+ Секции `auth` и `files` включают соответствующие REST-маршруты. Секция `graphql` включает GraphQL, а `openapi` — HTTP-маршрут со спецификацией REST. Пустые секции `graphql: {}` и `openapi: {}` используют пути по умолчанию; для экспорта в файл добавьте `target`. При наличии секции `graphql` явно заданный корневой `api` должен содержать `graphql`. Если `api` не задан, сервер использует `['rest', 'graphql']`.
50
50
 
51
51
  ```js
52
52
  export default {
@@ -64,25 +64,25 @@ export default {
64
64
  | Настройка | Назначение |
65
65
  |---|---|
66
66
  | `storage` | Обязательный режим: `file` или `memory`; общий для всех источников и схемы |
67
- | `database.source` | Путь к JSON базы или объект коллекций |
68
- | `database.schema` | Путь к JSON схемы или объект схемы; необязателен для REST |
69
- | `auth.source` | Путь к JSON пользователей или массив пользователей |
70
- | `auth.expiresIn` | Срок сессии в секундах; по умолчанию 3600 |
67
+ | `database.source` | Путь к JSON-файлу базы или объект с коллекциями |
68
+ | `database.schema` | Путь к JSON-файлу схемы или объект схемы; для REST можно не задавать |
69
+ | `auth.source` | Путь к JSON-файлу пользователей или массив пользователей |
70
+ | `auth.expiresIn` | Срок действия сессии в секундах; по умолчанию 3600 |
71
71
  | `files.source` | Каталог файлов или массив начальных файлов |
72
- | `files.metadata` | Только для `file`: путь JSON метаданных; по умолчанию `.files.json` внутри `files.source` |
72
+ | `files.metadata` | Только для `file`: путь к JSON-файлу метаданных; по умолчанию `.files.json` внутри `files.source` |
73
73
  | `graphql.endpoint` | HTTP-маршрут; по умолчанию `/graphql` |
74
74
  | `graphql.target` | Файл для экспорта GraphQL SDL |
75
75
  | `openapi.endpoint` | HTTP-маршрут; по умолчанию `/openapi.json` |
76
76
  | `openapi.target` | Файл для экспорта OpenAPI |
77
77
  | `package.source` | `package.json` проекта; обязателен при наличии `openapi` |
78
78
  | `server.host`, `server.port` | По умолчанию `127.0.0.1`, `4001`; CLI также читает `HOST`/`PORT` |
79
- | `server.pageSize`, `server.maxPageSize` | По умолчанию 10 и 100; размер по умолчанию ограничен максимумом |
79
+ | `server.pageSize`, `server.maxPageSize` | По умолчанию 10 и 100; если `pageSize` больше максимума, используется максимум |
80
80
  | `server.cors`, `server.logger` | По умолчанию `true`; logger принимает также настройки Fastify |
81
81
  | `server.maxFileSize` | По умолчанию 100 МиБ |
82
82
 
83
83
  Формат `package.source` зависит от режима хранения. При `storage: 'file'` укажите путь к `package.json`. При `storage: 'memory'` передайте метаданные напрямую: `{ name: 'example-api', version: '1.0.0', description: 'Example API' }`; `name` и `version` обязательны, `description` необязателен.
84
84
 
85
- Относительные пути разрешаются от каталога файла конфигурации; при вызове `createServer(config)` — от рабочего каталога. Данные в памяти, включая схему и метаданные пакета, копируются. Порт `0` позволяет системе выбрать свободный порт.
85
+ Относительные пути отсчитываются от каталога файла конфигурации; при вызове `createServer(config)` — от рабочего каталога. Сервер копирует переданные в память данные, включая схему и метаданные пакета. Порт `0` позволяет системе выбрать свободный порт.
86
86
 
87
87
  Чтобы открыть базу только через GraphQL, а аутентификацию и файлы оставить в REST с описанием OpenAPI, укажите:
88
88
 
@@ -113,7 +113,7 @@ export default {
113
113
  }
114
114
  ```
115
115
 
116
- Для REST с описанием OpenAPI используйте `"api": ["rest"]` и `openapi: {}`; для обоих API базы — `["rest", "graphql"]` с обеими секциями. Секция `graphql` обязательна тогда и только тогда, когда корневой `api` содержит `graphql`. OpenAPI описывает маршруты базы, только если включён REST; маршруты аутентификации и файлов включаются при наличии соответствующих секций.
116
+ Для REST с описанием OpenAPI используйте `"api": ["rest"]` и `openapi: {}`; для обоих API базы — `["rest", "graphql"]` с обеими секциями. Если корневой `api` задан явно, при наличии секции `graphql` в конфигурации добавьте `graphql` и в список API; без этой секции включать его в список нельзя. Если корневой `api` не задан, сервер определяет его по конфигурации. OpenAPI описывает маршруты базы, только если включён REST; маршруты аутентификации и файлов включаются при наличии соответствующих секций.
117
117
 
118
118
  ### CLI
119
119
 
@@ -188,9 +188,13 @@ export default {
188
188
 
189
189
  Явный `['rest']` включает REST-маршруты базы; `['graphql']` — операции GraphQL; `['rest', 'graphql']` — оба API. OpenAPI описывает REST-маршруты при наличии секции `openapi`. Корневой `api: []` недопустим; `[]` разрешён только на уровне модели.
190
190
 
191
- Модель без собственного `api` наследует значение из корня. Указанный у модели массив может содержать `['rest']`, `['graphql']`, оба варианта или `[]`. Модель не может включить API, отсутствующий в корневом `api`. OpenAPI включает модели, доступные через REST. Для генерации GraphQL нужна хотя бы одна GraphQL-модель; для генерации OpenAPI с маршрутами базы — хотя бы одна REST-модель. Если база доступна только через GraphQL, OpenAPI всё равно может описывать настроенные маршруты auth и файлов. Связанные модели должны разрешать тот же API. Имена моделей должны быть допустимыми идентификаторами; конфликты типов и операций вызывают ошибку. Имена `and`, `or`, `not` зарезервированы фильтрами.
191
+ Модель без собственного `api` наследует значение из корня. Указанный у модели массив может содержать `['rest']`, `['graphql']`, оба варианта или `[]`. Модель не может включить API, отсутствующий в корневом `api`.
192
+
193
+ OpenAPI включает модели, доступные через REST. Для генерации GraphQL нужна хотя бы одна GraphQL-модель; для генерации OpenAPI с маршрутами базы — хотя бы одна REST-модель. Если база доступна только через GraphQL, OpenAPI всё равно может описывать настроенные маршруты аутентификации и файлов.
192
194
 
193
- Таким образом, явный корневой `"api": ["rest", "graphql"]` включает оба API у моделей, наследующих настройку из корня. Порядок элементов массива не важен.
195
+ Связанные модели должны разрешать тот же API. Имена моделей должны быть допустимыми идентификаторами; конфликты типов и операций вызывают ошибку. Имена `and`, `or`, `not` зарезервированы фильтрами.
196
+
197
+ Порядок элементов в `api` не важен.
194
198
 
195
199
  Например, при корневом `api: ['rest', 'graphql']` модель с `api: ['rest']` получает REST-маршруты и пути в OpenAPI без операций GraphQL, а модель с `api: ['graphql']` — операции GraphQL без REST-маршрутов и путей в OpenAPI.
196
200
 
@@ -218,21 +222,25 @@ REST без схемы создаёт ключ `id` и сохраняет про
218
222
  | `type` | Обязательный тип |
219
223
  | `description`, `example` | Документация и пример |
220
224
  | `required`, `nullable` | По умолчанию `false`; наличие поля и разрешение `null` независимы |
221
- | `default` | Значение при пропуске в create/replace; PATCH не вставляет значения по умолчанию |
225
+ | `default` | Значение для поля, не переданного при создании или замене; PATCH не подставляет его |
222
226
  | `enum` | Допустимые строки, числа или логические значения; у массива — значения каждого элемента |
223
227
  | `primary` | Корневой первичный ключ: обязательный, уникальный, неизменяемый, без `null` |
224
228
  | `generated` | `uuid` для строк, `increment` для чисел; значение создаёт сервер |
225
229
  | `readOnly`, `writeOnly` | Только ответ или только входные данные; взаимно исключаются |
226
230
  | `minLength`, `maxLength`, `pattern` | Ограничения строк |
227
231
  | `format` | `date`, `date-time`, `email`, `uri`, `uuid` |
228
- | `minimum`, `maximum` | Включительные числовые границы |
232
+ | `minimum`, `maximum` | Нижняя и верхняя границы числа, включая сами границы |
229
233
  | `keyOn`, `source`, `target`, `onDelete` | Описание связи |
230
234
 
231
- Ограничения строк и чисел применяются к каждому элементу `string[]`/`number[]`. `required` и `nullable` относятся ко всему массиву; `default` и `example` содержат массив целиком. Элементы массива должны соответствовать его типу и быть отличны от `null`. `required` требует наличия хранимого поля массива, но не требует элементов в нём.
235
+ Ограничения строк и чисел применяются к каждому элементу `string[]`/`number[]`. `required` и `nullable` относятся ко всему массиву; `default` и `example` содержат массив целиком. Элементы должны соответствовать типу массива и не могут быть `null`. При `required: true` поле массива должно присутствовать в записи, но сам массив может быть пустым.
232
236
 
233
237
  Каждое значение `enum` должно соответствовать типу поля и его ограничениям. Чтобы разрешить `null` для поля, укажите `nullable: true`; добавлять `null` в `enum` не нужно. Некорректные значения отклоняются при загрузке схемы.
234
238
 
235
- У модели ровно один первичный ключ типа `string` или `number`, объявленный на верхнем уровне. Имя произвольное: `id`, `username`, `code`. Если `generated` не задан, значение передаёт клиент при создании. Генерируемые поля объявляются на верхнем уровне, исключаются из входных типов и несовместимы с `default`. При замене записи сохраняются генерируемые значения и поля `readOnly`, включая вложенные объекты. Обнуление nullable-родителя отклоняется, если оно удалит сохранённые поля `readOnly` внутри него. Для защиты полей внутри массива задайте `readOnly` всему массиву или содержащему его объекту. Объекты, состоящие из серверных полей, доступны только в ответах.
239
+ У модели ровно один первичный ключ типа `string` или `number`, объявленный на верхнем уровне. Имя произвольное: `id`, `username`, `code`. Если `generated` не задан, значение передаёт клиент при создании.
240
+
241
+ Генерируемые поля объявляются на верхнем уровне, исключаются из входных типов и несовместимы с `default`.
242
+
243
+ При замене записи сохраняются генерируемые значения и поля `readOnly`, в том числе во вложенных объектах. Даже при `nullable: true` объект нельзя заменить на `null`, если это удалит сохранённые поля `readOnly` внутри него. Для защиты полей внутри массива задайте `readOnly` всему массиву или содержащему его объекту. Объекты, состоящие только из полей, которыми управляет сервер, доступны лишь в ответах.
236
244
 
237
245
  Например, `LocalUser` с первичным ключом `username` и полем `password: {"type":"string","required":true,"writeOnly":true}` получает запрос `localUser(username: ...)` и маршрут `/localUsers/{username}`. Поле `writeOnly` доступно для записи и исключено из ответов, `scope`, фильтров и сортировки.
238
246
 
@@ -257,9 +265,13 @@ REST без схемы создаёт ключ `id` и сохраняет про
257
265
 
258
266
  `source` всегда указывает на поле текущей модели, а `target` — на поле модели из `type`. Для обратной связи пути меняются местами: у `Country.users` это `source: "id"` и `target: "countryId"`. Если ключ хранится под нестандартным именем, задайте путь явно. Когда между двумя моделями несколько связей, уточните обратные поля: например, `Country.birthUsers = {"type":"User[]","keyOn":"related","target":"birthCountryId"}` и `Country.residenceUsers = {"type":"User[]","keyOn":"related","target":"residenceCountryId"}`. Отсутствующая, противоречивая или неоднозначная пара вызывает ошибку при загрузке схемы.
259
267
 
260
- Ключи связей остаются в базе, даже если их нет в `fields`. На стороне `current` поле `source` можно не объявлять отдельно, если известен тип целевого ключа: сервер выведет массив для множественной связи или одно значение для одиночной. Это действует и для первичного, и для непервичного целевого поля. Такой ключ остаётся внутренним: REST и GraphQL не позволяют читать, выбирать, фильтровать, сортировать или записывать его напрямую, а OpenAPI его не описывает. Сама связь при этом доступна. Чтобы открыть ключ в API, объявите его в `fields`, например `"countryId": { "type": "string" }`. Неоднозначное сопоставление тоже требует явного объявления.
268
+ Ключи связей остаются в базе, даже если их нет в `fields`. На стороне `current` хранимый ключ, на который указывает `source`, можно не объявлять в `fields`, если известен тип целевого ключа. Сервер определит его тип: массив для множественной связи или одно значение для одиночной. Это действует и для первичного, и для непервичного целевого поля.
261
269
 
262
- Например, `publishers: { "type": "Publisher[]", "keyOn": "current", "target": "code" }` выводит `publisherCodes: { "type": "string[]" }`, если `Publisher.code` — строка, даже при первичном ключе `Publisher.id`. Типы явно объявленных хранимых ключей сохраняются.
270
+ Такой ключ остаётся внутренним: REST и GraphQL не позволяют читать, выбирать, фильтровать, сортировать или записывать его напрямую, а OpenAPI его не описывает. Сама связь при этом доступна.
271
+
272
+ Чтобы открыть ключ в API, объявите его в `fields`, например `"countryId": { "type": "string" }`. При неоднозначном сопоставлении ключ тоже нужно объявить явно.
273
+
274
+ Например, для `publishers: { "type": "Publisher[]", "keyOn": "current", "target": "code" }` сервер определяет ключ `publisherCodes: { "type": "string[]" }`, если `Publisher.code` — строка, даже при первичном ключе `Publisher.id`. Типы явно объявленных хранимых ключей сохраняются.
263
275
 
264
276
  Обратная связь: `User.movies = {"type":"Movie[]","keyOn":"related","target":"actors.userId"}` соответствует `Movie.actors.user = {"type":"User","keyOn":"current","source":"actors.userId"}`. Фильм возвращается один раз, даже если совпало несколько актёров. Несколько совпадений для одиночной связи — ошибка.
265
277
 
@@ -269,13 +281,15 @@ REST без схемы создаёт ключ `id` и сохраняет про
269
281
 
270
282
  `onDelete` срабатывает **при удалении целевой записи**:
271
283
 
272
- - `restrict` — по умолчанию: запретить удаление, пока на цель ссылается сохраняемая запись.
284
+ - `restrict` — по умолчанию: запретить удаление, пока на цель ссылается запись, которая остаётся в базе.
273
285
  - `cascade` — удалить ссылающуюся запись. Для `User.country` удаление страны удаляет пользователей. Для `Movie.actors.user` удаление пользователя удаляет соответствующие элементы `actors`, сохраняя фильм.
274
286
 
275
287
  Каскадное удаление выполняется целиком, включая циклические связи. Ошибка проверки отменяет всю операцию. Обратная связь `related` без `onDelete` сама по себе не запрещает удаление записи из-за ключа на другой стороне; задайте `onDelete`, если для неё нужно дополнительное правило.
276
288
 
277
289
  ## Пример базы данных
278
290
 
291
+ Эта база использует полную [схему примера](examples/schema.json). Кроме пользователей и стран из сокращённой схемы выше, она содержит фильмы, жанры и издателей.
292
+
279
293
  ```json
280
294
  {
281
295
  "countries": [
@@ -368,7 +382,7 @@ REST без схемы создаёт ключ `id` и сохраняет про
368
382
 
369
383
  ## Запросы и ответы
370
384
 
371
- Примеры с фильмами, актёрами и жанрами используют полную [схему из examples](examples/schema.json). Для их запуска используйте [конфигурацию примера](examples/server.config.js):
385
+ Запустите примеры ниже с [конфигурацией примера](examples/server.config.js):
372
386
 
373
387
  ```sh
374
388
  npx deep-json-server examples/server.config.js
@@ -384,7 +398,9 @@ npx deep-json-server examples/server.config.js
384
398
 
385
399
  В `pager` задаются `page` и `pageSize`. По умолчанию возвращается первая страница с размером из `server.pageSize`. Оба значения должны быть положительными целыми числами; `pageSize` ограничен `server.maxPageSize`. За пределами списка возвращается пустой `data` с общим числом найденных записей в `total`.
386
400
 
387
- Фильтры: `eq`, `ne`, `in`; для строк `contains`, `startsWith`, `endsWith`; сравнения `gt`, `gte`, `lt`, `lte`. Несколько условий в одном объекте должны выполняться одновременно. `and` и `or` принимают массив условий, `not` — одно условие; `not` также можно использовать внутри фильтра поля. У массивов есть `some`, `every`, `none`; у примитивных массивов также `contains`, `in`. `contains`, `startsWith` и `endsWith` для строк не учитывают регистр; `eq`, `ne` и `in` сравнивают точные значения. Для строк `gt`, `gte`, `lt` и `lte` используют тот же порядок, что сортировка: без учёта регистра и с числовым сравнением фрагментов строки. Условие поля задаётся объектом с оператором, например `{ "id": { "eq": "1" } }`.
401
+ Фильтры: `eq`, `ne`, `in`; для строк `contains`, `startsWith`, `endsWith`; сравнения `gt`, `gte`, `lt`, `lte`. Несколько условий в одном объекте должны выполняться одновременно. `and` и `or` принимают массив условий, `not` — одно условие; `not` также можно использовать внутри фильтра поля. У массивов есть `some`, `every`, `none`; у примитивных массивов также `contains`, `in`.
402
+
403
+ `contains`, `startsWith` и `endsWith` для строк не учитывают регистр; `eq`, `ne` и `in` сравнивают точные значения. Для строк `gt`, `gte`, `lt` и `lte` используют тот же порядок, что сортировка: без учёта регистра и с числовым сравнением фрагментов строки. Условие поля задаётся объектом с оператором, например `{ "id": { "eq": "1" } }`.
388
404
 
389
405
  ```json
390
406
  {
@@ -417,7 +433,7 @@ npx deep-json-server examples/server.config.js
417
433
  | PATCH | `/users/{id}` | `userUpdate` |
418
434
  | DELETE | `/users/{id}` | `userDelete` |
419
435
 
420
- Имя параметра пути соответствует первичному ключу. POST, PUT и PATCH принимают JSON-объект записи. PUT заменяет запись с сохранением ключа и серверных полей. PATCH объединяет поля на верхнем уровне; переданные вложенные объекты заменяются с сохранением их полей `readOnly`. Создание и замена требуют всех обязательных полей. При обновлении проверяются переданные значения и итоговая запись. Отсутствующая запись — `404`, конфликт — `409`.
436
+ Имя параметра пути соответствует первичному ключу. POST, PUT и PATCH принимают JSON-объект записи. PUT заменяет запись с сохранением ключа и серверных полей. PATCH объединяет поля на верхнем уровне; переданные вложенные объекты заменяются с сохранением их полей `readOnly`. При создании и замене передавайте обязательные поля, доступные для записи и не имеющие значений по умолчанию. При обновлении проверяются переданные значения и итоговая запись. Отсутствующая запись — `404`, конфликт — `409`.
421
437
 
422
438
  POST возвращает созданную запись со статусом `201`; PUT, PATCH и DELETE — результат со статусом `200`. Ошибки REST имеют вид `{ "error": "Error description" }`.
423
439
 
@@ -448,7 +464,11 @@ const params = new URLSearchParams({ scope: JSON.stringify(scope) });
448
464
  const response = await fetch(`/users?${params}`);
449
465
  ```
450
466
 
451
- Скаляры и массивы примитивов выбираются через `true`, объекты и связи — через свой массив `scope`. Если аргументы не нужны, в массиве остаётся только объект полей. `"*": true` включает только скалярные поля текущей модели, которые не хранят ключи связей. Массивы, объекты, связи, их ключи и поля `writeOnly` в него не входят. Со схемой ключ связи можно выбрать через `{ "*": true, "countryId": true }`, только если он объявлен в `fields`. Без схемы ключ, присутствующий в записях базы, можно выбрать явно, даже если его нет в `*`; из `*` он исключается, когда сервер выводит связь по имени ключа и существующей коллекции.
467
+ Скаляры и массивы примитивов выбираются через `true`, объекты и связи — через свой массив `scope`. Если аргументы не нужны, в массиве остаётся только объект полей.
468
+
469
+ `"*": true` включает только скалярные поля текущей модели, которые не хранят ключи связей. Массивы, объекты, связи, их ключи и поля `writeOnly` в него не входят.
470
+
471
+ Со схемой ключ связи можно выбрать через `{ "*": true, "countryId": true }`, только если он объявлен в `fields`. Без схемы ключ, присутствующий в записях базы, можно выбрать явно, даже если его нет в `*`; из `*` он исключается, когда сервер выводит связь по имени ключа и существующей коллекции.
452
472
 
453
473
  Например, собственные поля фильма, пользователи актёров и отсортированные жанры:
454
474
 
@@ -472,9 +492,22 @@ const response = await fetch(`/users?${params}`);
472
492
 
473
493
  Без `scope` возвращаются скалярные поля, как при `[{"*":true}]`. Пустой выбор `[{}]` возвращает объект без полей. Списки сохраняют структуру `{ data, total }`.
474
494
 
475
- Аргументы доступны только у списков. Вместо них список может использовать `{ "union": [scope, ...] }`: каждая часть — обычный scope списка, части выполняются в порядке массива, а для каждого первичного ключа остаётся первая запись. Во вложенных массивах объектов без первичных ключей каждый исходный элемент включается один раз, даже если его выбрали несколько частей. Разные элементы с одинаковым содержимым сохраняются отдельно. В запросе отдельной записи и в ответе мутации аргументы можно задать для вложенных списков. Параметры проверяются даже на пустых данных; ошибка в выборе ответа отменяет изменения записи. Некорректный `scope` возвращает `400`. Максимальная длина JSON — 10 000 символов, глубина выбора — 32 уровня.
495
+ Аргументы доступны только у списков. Вместо них список может использовать `{ "union": [scope, ...] }`: каждая часть — обычный scope списка, части выполняются в порядке массива, а для каждого первичного ключа остаётся первая запись.
476
496
 
477
- REST и GraphQL также ограничивают общий объём работы каждого запроса: до 1 000 узлов выбора, 10 000 развёрнутых записей, 100 000 учитываемых шагов выполнения и консервативной оценки размера JSON в 8 МиБ. Вложенные списки, части `union` и GraphQL aliases расходуют общий бюджет. При превышении сервер возвращает REST `400` или GraphQL `INVALID_QUERY`; уменьшите выборку полей, вложенность или размер страниц. Мутация REST откатывается, если построение её ответа превышает бюджет.
497
+ Во вложенных массивах объектов без первичных ключей каждый исходный элемент включается один раз, даже если его выбрали несколько частей. Разные элементы с одинаковым содержимым сохраняются отдельно.
498
+
499
+ В запросе отдельной записи и в ответе мутации аргументы можно задать для вложенных списков.
500
+
501
+ Параметры проверяются даже на пустых данных; ошибка в выборе ответа отменяет изменения записи. Некорректный `scope` возвращает `400`. Максимальная длина JSON — 10 000 символов, глубина выбора — 32 уровня.
502
+
503
+ REST и GraphQL ограничивают общий объём работы одного запроса:
504
+
505
+ - 1 000 элементов в дереве выбора полей.
506
+ - 10 000 записей, развёрнутых в ответе.
507
+ - 100 000 учитываемых шагов обработки, включая фильтрацию и сортировку.
508
+ - 8 МиБ для оценочного размера JSON-ответа.
509
+
510
+ Эти лимиты общие для вложенных списков, частей `union` и псевдонимов GraphQL. Размер оценивается с запасом на кодирование JSON, поэтому лимит может сработать, даже если фактический ответ меньше 8 МиБ. При превышении сервер возвращает REST `400` или GraphQL `INVALID_QUERY`; уменьшите число выбранных полей, вложенность или размер страниц. Если построение ответа REST превышает лимит, изменения записи откатываются.
478
511
 
479
512
  ### Вложенная запись
480
513
 
@@ -504,11 +537,15 @@ Content-Type: application/json
504
537
  | Объект с первичным ключом и другими полями | PATCH обновляет переданные поля, PUT заменяет связанную запись |
505
538
  | Объект без первичного ключа | Создать запись со значениями по умолчанию и сгенерированным ключом |
506
539
 
507
- Имя и тип ключа берутся из целевой модели. Объект только с этим ключом считается ссылкой в POST, PUT и PATCH; остальные обязательные поля целевой модели ему не нужны. Если добавить другие поля, это уже обновление: PUT требует все обязательные поля, удаляет пропущенные необязательные или задаёт им значения по умолчанию; POST/PATCH обновляют переданные поля. При замене сохраняются первичный ключ, генерируемые значения и поля `readOnly`. Если запись по ключу не найдена, операция завершится ошибкой. Для создания вложенной записи без ключа нужна его автоматическая генерация. Объявленные хранимые ключи, например `countryId`, тоже позволяют менять ссылки напрямую.
540
+ Имя и тип ключа берутся из целевой модели. Объект только с этим ключом считается ссылкой в POST, PUT и PATCH; остальные обязательные поля целевой модели ему не нужны.
541
+
542
+ Если добавить другие поля, это уже обновление: PUT требует обязательные поля, доступные для записи и не имеющие значений по умолчанию, удаляет пропущенные необязательные или задаёт им значения по умолчанию; POST/PATCH обновляют переданные поля. При замене сохраняются первичный ключ, генерируемые значения и поля `readOnly`.
543
+
544
+ Если запись по ключу не найдена, операция завершится ошибкой. Для создания вложенной записи без ключа нужна его автоматическая генерация. Объявленные хранимые ключи, например `countryId`, тоже позволяют менять ссылки напрямую.
508
545
 
509
546
  Переданный список заменяет состав связи. PATCH сохраняет пропущенные связи. PUT заменяет доступные для записи поля текущей записи, включая её собственные ключи связей, но сохраняет пропущенные связи `keyOn: "related"`, чьи ключи хранятся в других записях. `[]` очищает список, `null` — одиночную связь с разрешённым `nullable`. Разрыв связи не удаляет связанную запись. Обязательная одиночная связь должна сохраняться; обязательная связь-список может быть пустой.
510
547
 
511
- Если хранимый ключ объявлен в `fields`, в одном объекте указывайте либо связь, либо её ключ: например, `genres` или `genreIds`. Одновременная передача обоих полей возвращает `400 INVALID_INPUT`, даже если наборы ключей совпадают:
548
+ Если хранимый ключ объявлен в `fields`, в одном объекте указывайте либо связь, либо её ключ: например, `genres` или `genreIds`. Одновременная передача обоих полей возвращает REST `400` или GraphQL `INVALID_INPUT`, даже если наборы ключей совпадают:
512
549
 
513
550
  ```json
514
551
  {
@@ -517,9 +554,9 @@ Content-Type: application/json
517
554
  }
518
555
  ```
519
556
 
520
- Сервер не выбирает приоритет и не объединяет значения; вся операция отменяется. Поле связи принимает только объекты; для изменения одних ссылок без создания и обновления связанных записей используйте объявленный ключ, например `genreIds`. Для обратной связи сервер меняет целевой ключ. Если путь проходит через массив и нельзя однозначно выбрать элемент для связи, передайте массив с нужными ключами явно. Защищённые ключи изменять нельзя.
557
+ Сервер отклоняет запрос с обоими полями и отменяет всю операцию. Поле связи принимает только объекты; чтобы изменить ссылки без создания и обновления связанных записей, используйте объявленный ключ, например `genreIds`. Для обратной связи сервер меняет целевой ключ. Если путь проходит через массив и нельзя однозначно выбрать элемент для связи, передайте массив с нужными ключами явно. Защищённые ключи изменять нельзя.
521
558
 
522
- Все вложенные изменения входят в транзакцию основной записи. Ошибка проверки, отсутствующая запись или неверный выбор полей ответа отменяет всю операцию. Изменения общей записи видны всем, кто с ней связан.
559
+ Все вложенные изменения входят в транзакцию основной записи. Ошибка проверки или отсутствующая целевая запись отменяет все изменения. В REST неверный выбор полей ответа тоже приводит к откату. GraphQL проверяет выбор полей до записи, но ошибка при построении результата уже сохранённой мутации не отменяет её; подробнее в разделе [GraphQL](#graphql). Изменения общей записи видны всем, кто с ней связан.
523
560
 
524
561
  GraphQL принимает в полях связей типизированные объекты. Чтобы изменить только ссылки, в том числе при замене, передайте объекты только с первичными ключами либо объявленные поля ключей, например `genreIds`. Обязательные поля вложенного создания или замены с дополнительными полями проверяются при выполнении. Пример:
525
562
 
@@ -541,7 +578,7 @@ mutation {
541
578
  Укажите `database.schema`, включите `graphql` в корневой `api` схемы и добавьте в конфигурацию `graphql: {}`:
542
579
 
543
580
  ```sh
544
- npx deep-json-server server.config.js
581
+ npx deep-json-server server.config.mjs
545
582
  ```
546
583
 
547
584
  Отправляйте запросы на `/graphql` методом POST с `Content-Type: application/json` и телом `{ "query": "…", "variables": {} }`. Путь можно изменить через `graphql.endpoint`.
@@ -577,13 +614,13 @@ query {
577
614
 
578
615
  Строковый первичный ключ представлен типом GraphQL `ID`, обычные строки — `String`, числа — `Float`, параметры пагинации — `Int`. Enum сохраняет допустимые строковые имена; остальные значения получают имена `VALUE_0`, `VALUE_1` и т. д. Длины строк, форматы и другие ограничения модели проверяются сервером при выполнении запроса. Для просмотра схемы доступна интроспекция. Параметры выбранных списков проверяются до выполнения мутаций.
579
616
 
580
- Ошибки содержат `extensions.code`: `INVALID_INPUT`, `INVALID_QUERY`, `NOT_FOUND`, `CONFLICT`, `UNAUTHENTICATED`, `FORBIDDEN` или `INTERNAL_ERROR`. Ошибки синтаксиса и типов GraphQL возвращаются в стандартном массиве `errors`. Максимальная глубина запроса — 32 уровня. Ограничения общего объёма работы, описанные для REST, действуют и в GraphQL. Лимит узлов выбора и аргументы списков проверяются до выполнения мутаций. Ошибки при построении результата мутации, включая превышение бюджета во время выполнения, не отменяют уже сохранённую мутацию; перед повтором запроса проверьте состояние записи.
617
+ Ошибки содержат `extensions.code`: `INVALID_INPUT`, `INVALID_QUERY`, `NOT_FOUND`, `CONFLICT`, `UNAUTHENTICATED`, `FORBIDDEN` или `INTERNAL_ERROR`. Ошибки синтаксиса и типов GraphQL возвращаются в стандартном массиве `errors`. Максимальная глубина запроса — 32 уровня. Лимиты запросов, описанные для REST, действуют и в GraphQL. Число элементов выбора полей и аргументы списков проверяются до выполнения мутаций. Ошибки при построении результата, в том числе превышение лимита запроса, не отменяют уже сохранённую мутацию; перед повтором проверьте состояние записи.
581
618
 
582
- Операция GraphQL с несколькими полями мутаций не является одной транзакцией: поля выполняются по порядку, а ранее сохранённые изменения остаются при ошибке последующего результата. Превышение бюджета во время выполнения возвращает `data: null` и останавливает следующие поля мутаций, но `data: null` не означает откат записей. Прочитайте сохранённое состояние менее объёмным запросом. Автоматический повтор создания с генерируемым ключом может создать вторую запись.
619
+ Операция GraphQL с несколькими полями мутаций не является одной транзакцией: поля выполняются по порядку, а ранее сохранённые изменения остаются при ошибке последующего результата. Превышение лимита во время выполнения возвращает `data: null` и останавливает следующие поля мутаций. Перед повтором проверьте сохранённое состояние запросом с меньшей выборкой: `data: null` не означает откат, а повторное создание записи с генерируемым ключом может привести к дубликату.
583
620
 
584
621
  ## OpenAPI и экспорт схем
585
622
 
586
- Экспорт использует OpenAPI 3.0.3. Добавьте `openapi: {}`, `database.schema` и `package.source`, чтобы получать спецификацию по HTTP на `/openapi.json`. Путь меняется через `openapi.endpoint`. Маршруты базы включаются только для REST-моделей; при базе только с GraphQL документ всё равно может описывать настроенные маршруты auth и файлов. Поля OpenAPI `info.title`, `info.version` и необязательное `info.description` берутся из настроенного `package.json`. Спецификацию можно открыть в Swagger UI или импортировать в API-клиент.
623
+ Спецификация соответствует OpenAPI 3.0.3. Добавьте `openapi: {}`, `database.schema` и `package.source`, чтобы получать её по адресу `/openapi.json`. Путь меняется через `openapi.endpoint`. Маршруты базы включаются только для REST-моделей; если база доступна только через GraphQL, документ всё равно может описывать настроенные маршруты аутентификации и файлов. Поля OpenAPI `info.title`, `info.version` и необязательное `info.description` берутся из настроенного `package.json`. Спецификацию можно открыть в Swagger UI или импортировать в API-клиент.
587
624
 
588
625
  Для сохранения схем задайте пути экспорта:
589
626
 
@@ -598,13 +635,13 @@ export default {
598
635
  ```
599
636
 
600
637
  ```bash
601
- npx deep-json-server server.config.js --generate-only
602
- npx deep-json-server server.config.js --generate
638
+ npx deep-json-server server.config.mjs --generate-only
639
+ npx deep-json-server server.config.mjs --generate
603
640
  ```
604
641
 
605
642
  `--generate-only` экспортирует и завершает работу, `--generate` после экспорта запускает сервер. Форматы определяются наличием секций. Для каждого выбранного формата обязателен свой `target`. HTTP-маршрут OpenAPI возвращает JSON; `writeOpenapi()` и CLI записывают YAML. Если секций нет, отсутствует `target` или генерация завершилась ошибкой, команда возвращает ошибку и сервер не запускается.
606
643
 
607
- Экспорт не открывает базу, учётные записи и файлы. Перед записью проверяются все выбранные `target`; они не могут совпадать с конфигурацией, базой, схемой, пользователями, счётчиками или метаданными файлов.
644
+ Генерация не читает записи базы, учётные записи и загруженные файлы. Перед записью проверяются все выбранные `target`: они не могут перезаписать конфигурацию, базу, схему, пользователей, блокировки, счётчики или метаданные файлов.
608
645
 
609
646
  ## Аутентификация
610
647
 
@@ -639,7 +676,7 @@ export default {
639
676
  };
640
677
  ```
641
678
 
642
- Запустите `npx deep-json-server server.config.js`. Каждой исходной учётной записи нужны уникальный строковый `id`, уникальный `username` и `passwordHash`, созданный функцией выше. `isAdmin` по умолчанию равен `false`, а `sessions` — `[]`, поэтому существующие файлы пользователей остаются допустимыми. Пароли хешируются через scrypt со случайной солью.
679
+ Запустите `npx deep-json-server server.config.mjs`. Каждой исходной учётной записи нужны уникальный строковый `id`, уникальный `username` и `passwordHash`, созданный функцией выше. `isAdmin` по умолчанию равен `false`, а `sessions` — `[]`, поэтому существующие файлы пользователей остаются допустимыми. Пароли хешируются через scrypt со случайной солью.
643
680
 
644
681
  При `storage: 'memory'` передайте массив в `auth.source`:
645
682
 
@@ -660,13 +697,13 @@ export default {
660
697
  };
661
698
  ```
662
699
 
663
- Учётные записи auth и их сессии хранятся вместе в `auth.source`, отдельно от базы. Каждый элемент массива `sessions` пользователя содержит `{ tokenHash, expiresAt }`: `tokenHash` — SHA-256 хеш токена в виде 64 шестнадцатеричных символов нижнего регистра, а `expiresAt` — абсолютное время истечения сессии в миллисекундах Unix. Исходные токены не сохраняются, а ответы auth не раскрывают ни хеши паролей, ни записи сессий. Хеши токенов должны быть уникальны среди всех пользователей.
700
+ Учётные записи и сессии хранятся вместе в `auth.source`, отдельно от базы. Каждый элемент массива `sessions` содержит `{ tokenHash, expiresAt }`. `tokenHash` — хеш токена SHA-256: строка из 64 шестнадцатеричных символов в нижнем регистре. `expiresAt` — время окончания сессии, выраженное числом миллисекунд с 1 января 1970 года, 00:00 UTC. Сами токены не сохраняются, а ответы методов аутентификации не раскрывают хеши паролей и данные сессий. Хеши токенов должны быть уникальны среди всех пользователей.
664
701
 
665
- При `storage: 'file'` вход, выход и изменения пользователей сохраняются в один JSON-файл до успешного ответа. Действующие сессии переживают перезапуск сервера: клиент может продолжать использовать прежний токен. Новый пароль и удаление всех сессий пользователя сохраняются вместе. При ошибке записи прежние пользователи и сессии сохраняются без изменений. После ручного редактирования файла перезапустите сервер; если меняете хеш пароля вручную, также очистите `sessions` этого пользователя.
702
+ При `storage: 'file'` изменения учётных записей и сессий сохраняются в JSON-файл до отправки успешного ответа. Действующие сессии сохраняются после перезапуска сервера: клиент может продолжать использовать прежний токен. Новый пароль и удаление всех сессий пользователя записываются вместе. При ошибке записи пользователи и сессии остаются без изменений. Для ручного редактирования файла остановите сервер; если меняете хеш пароля, также очистите `sessions` этого пользователя, затем запустите сервер снова.
666
703
 
667
- При `storage: 'memory'` переданный массив пользователей не изменяется. Каждый запуск заново загружает исходных пользователей и сессии. Сессии, созданные во время работы, после перезапуска исчезают; заданные в исходных данных неистёкшие сессии восстанавливаются, включая завершённые через logout во время предыдущего запуска. Клиенту по-прежнему нужен соответствующий исходный токен: восстановить его из `tokenHash` нельзя.
704
+ При `storage: 'memory'` переданный массив пользователей не изменяется. Каждый запуск заново загружает исходных пользователей и сессии. Сессии, созданные во время работы, после перезапуска исчезают. Действующие сессии из исходных данных восстанавливаются, даже если при предыдущем запуске пользователь вышел через `/auth/logout`. Клиенту по-прежнему нужен сам токен: восстановить его из `tokenHash` нельзя.
668
705
 
669
- `auth.expiresIn` задаёт срок новых сессий и по умолчанию равен 3600 секундам. У существующих сессий сохраняется записанное время истечения, в том числе после перезапуска. Истёкшие сессии игнорируются при загрузке, отклоняются при каждом запросе и удаляются у всех пользователей при следующем успешном изменении auth, например входе или выходе. Срок сессии автоматически не продлевается.
706
+ `auth.expiresIn` задаёт срок новых сессий и по умолчанию равен 3600 секундам. У существующих сессий сохраняется записанное время истечения, в том числе после перезапуска. Истёкшие сессии игнорируются при загрузке, отклоняются при каждом запросе и удаляются у всех пользователей при следующем успешном изменении данных пользователей или сессий, например при входе или выходе. Срок сессии автоматически не продлевается.
670
707
 
671
708
  | REST-запрос | JSON тела | Ответ |
672
709
  |---|---|---|
@@ -677,7 +714,7 @@ export default {
677
714
  | `PATCH /auth/users/:id/password` | `{ "currentPassword": "…", "newPassword": "…" }` | `{ success: true }` |
678
715
  | `PATCH /auth/users/:id/admin` | `{ "isAdmin": true }` | `{ id, username, isAdmin }` |
679
716
 
680
- Регистрация и вход доступны без токена. Для остальных методов передавайте `Authorization: Bearer <accessToken>`. Регистрация создаёт обычного пользователя с новым `id`; поля `id`, `passwordHash`, `isAdmin` и `sessions` в запросе запрещены. `username` уникален с учётом регистра, повтор возвращает `409`. Имя должно содержать хотя бы один непробельный символ и не более 256 символов, пароль — от 1 до 1024 символов. Значения не обрезаются. Регистрация не открывает сессию: после неё выполните вход.
717
+ Регистрация и вход доступны без токена. Для остальных методов передавайте `Authorization: Bearer <accessToken>`. Регистрация создаёт обычного пользователя с новым `id`; поля `id`, `passwordHash`, `isAdmin` и `sessions` в запросе запрещены. Имена пользователей уникальны с учётом регистра; повторная регистрация того же имени возвращает `409`. Имя должно содержать хотя бы один непробельный символ и не более 256 символов, пароль — от 1 до 1024 символов. Пробелы по краям не удаляются. Регистрация не создаёт сессию: после неё выполните вход.
681
718
 
682
719
  Пользователь меняет только свой пароль, передавая `currentPassword` и `newPassword`. Для администратора при смене своего пароля действует то же правило. Администратор может сменить пароль обычному пользователю, передав только `newPassword`. Изменение пароля другого администратора возвращает `403`. После успешной смены завершаются все сессии этого пользователя, включая текущую при смене своего пароля; требуется новый вход. Неверный текущий пароль возвращает `401` без изменения сессий.
683
720
 
@@ -685,7 +722,7 @@ export default {
685
722
 
686
723
  Недействительный или истёкший токен возвращает `401`, недостаток прав — `403`, отсутствующий пользователь при разрешённой операции — `404`. Ошибки тела запроса возвращают `400`. Вход, регистрация и смена пароля могут вернуть `429` при превышении числа одновременных вычислений паролей; вход также возвращает `429`, если уже есть 10 000 активных сессий среди всех пользователей, включая загруженные из источника. Выход завершает только сессию переданного токена.
687
724
 
688
- При включённом auth у каждой записи модели доступен виртуальный объект `actions`. Он вычисляется для текущего пользователя и не сохраняется в базе. В REST его нужно явно запросить через `scope`:
725
+ При включённой аутентификации у каждой записи модели доступен виртуальный объект `actions`. Он вычисляется для текущего пользователя и не сохраняется в базе. В REST его нужно явно запросить через `scope`:
689
726
 
690
727
  ```json
691
728
  [
@@ -715,9 +752,9 @@ query {
715
752
  }
716
753
  ```
717
754
 
718
- `actions` доступен у корневых и связанных записей моделей, включая результаты мутаций. Его нельзя записывать, фильтровать или сортировать. Обычные вложенные объекты и ответы методов auth это поле не получают.
755
+ `actions` доступен у корневых и связанных записей моделей, включая результаты мутаций. Его нельзя записывать, фильтровать или сортировать. Обычные вложенные объекты и ответы методов аутентификации это поле не получают.
719
756
 
720
- Когда настроен `openapi`, его документ описывает маршруты auth, необязательную Bearer-аутентификацию при чтении записей, обязательную при изменениях и поле ответа `actions`. В Swagger UI токен из ответа на вход можно вставить в **Authorize**. Для экспорта схем включите auth в конфигурации и выполните `npx deep-json-server server.config.js --generate-only`; файл учётных записей при генерации не читается. Методы auth доступны через REST. В GraphQL тот же токен проверяется при чтении прав и изменении записей. Для GraphQL и OpenAPI нужна `database.schema`.
757
+ Когда настроен `openapi`, его документ описывает маршруты аутентификации, необязательную Bearer-аутентификацию при чтении записей, обязательную при изменениях и поле ответа `actions`. В Swagger UI токен из ответа на вход можно вставить в **Authorize**. Для экспорта схем включите аутентификацию в конфигурации и выполните `npx deep-json-server server.config.mjs --generate-only`; файл учётных записей при генерации не читается. Методы аутентификации доступны через REST. В GraphQL тот же токен проверяется при чтении прав и изменении записей. Для GraphQL и OpenAPI нужна `database.schema`.
721
758
 
722
759
  ## Даты записей, удаление и владельцы
723
760
 
@@ -756,9 +793,9 @@ query {
756
793
  | `createdById`, `updatedById` | `auth` | Создатель/владелец и последний редактор |
757
794
  | `deletedById` | `auth` + `softDelete` | Пользователь, удаливший запись, или `null` |
758
795
 
759
- Даты — строки ISO 8601 в UTC. При создании даты создания и изменения совпадают, а при включённом auth создателем и редактором становится текущий пользователь. PUT/PATCH сохраняют создателя и дату создания. DELETE обновляет поля удаления и включённые поля последнего изменения. Для старых записей с неизвестными датами или авторами возвращается `null`. Эти поля доступны только для чтения в REST и GraphQL; обычные вложенные объекты собственных полей авторства и дат не получают. При отключении функции уже сохранённые значения её полей сохраняются.
796
+ Даты записываются строками ISO 8601 в UTC. У новой записи даты создания и изменения совпадают; при включённой аутентификации создателем и последним редактором становится текущий пользователь. PUT/PATCH сохраняют создателя и дату создания. DELETE обновляет поля удаления, а также дату и автора последнего изменения, если соответствующие функции включены. Для старых записей с неизвестными датами или авторами возвращается `null`. Эти поля доступны только для чтения в REST и GraphQL; обычные вложенные объекты собственных полей дат и авторства не получают. Отключение функции не удаляет её ранее сохранённые значения.
760
797
 
761
- При мягком удалении DELETE оставляет запись в базе. Повторный DELETE уже удалённой записи сохраняет прежние сведения об удалении. Получение по первичному ключу возвращает и удалённые записи. Успешный PUT/PATCH восстанавливает запись, обнуляя `deletedAt` и `deletedById`. Пустой PATCH восстанавливает без замены остальных полей; для PUT нужно передать все обязательные поля.
798
+ При мягком удалении DELETE оставляет запись в базе. Повторный DELETE уже удалённой записи сохраняет прежние сведения об удалении. Получение по первичному ключу возвращает и удалённые записи. Успешный PUT/PATCH восстанавливает запись, обнуляя `deletedAt` и `deletedById`. Пустой PATCH восстанавливает без замены остальных полей; PUT следует обычным правилам замены.
762
799
 
763
800
  Списки по умолчанию возвращают активные записи. Для получения удалённых задайте условие по `deletedAt`:
764
801
 
@@ -773,9 +810,13 @@ query {
773
810
 
774
811
  Каскадное удаление учитывает `onDelete` и настройку `softDelete` каждой затронутой модели. Восстановление записи, начавшей каскад, возвращает записи, удалённые той же операцией, не затрагивая удалённые ранее. Физически удалённые записи восстановить нельзя. Если каскад удалил вложенный объект, восстановление возможно, пока соответствующее поле объекта не менялось после удаления. Конфликт или отсутствие обязательной связи отменяет всю операцию.
775
812
 
776
- Служебные сведения восстановления хранятся вместе с записями и переживают перезапуск; сохраняйте их при резервном копировании базы. Внутреннее поле `djsDeletion` зарезервировано и не выдаётся через API.
813
+ Служебные данные для восстановления хранятся вместе с записями и сохраняются после перезапуска; включайте их в резервную копию базы. Внутреннее поле `djsDeletion` зарезервировано и не выдаётся через API.
814
+
815
+ При включённой аутентификации создавать записи может любой вошедший пользователь. Изменять, удалять и восстанавливать — владелец по `createdById` или пользователь с `isAdmin: true`. Записи без владельца изменяет только администратор. Его правки не меняют владельца. Правила действуют на вложенные записи, изменение ключей связей, каскады и восстановление.
816
+
817
+ Ссылка через объект только с первичным ключом или объявленный хранимый ключ не требует владения неизменяемой целевой записью. Вложенный объект с дополнительными полями требует права изменять её. При изменении обратных связей нужны права на каждую запись, чей хранимый ключ меняется.
777
818
 
778
- При включённом auth создавать записи может любой вошедший пользователь. Изменять, удалять и восстанавливать — владелец по `createdById` или пользователь с `isAdmin: true`. Записи без владельца изменяет только администратор. Его правки не меняют владельца. Правила действуют на вложенные записи, изменение ключей связей, каскады и восстановление. Ссылка через объект только с первичным ключом или объявленный хранимый ключ не требует владения неизменяемой целевой записью. Вложенный объект с дополнительными полями требует права изменять её. При изменении обратных связей нужны права на каждую запись, чей хранимый ключ меняется. Каждая мутация атомарна: отказ сохраняет прежнее состояние всех затронутых записей.
819
+ Изменение записи вместе с вложенными изменениями атомарно: отказ в правах сохраняет прежнее состояние всех записей, затронутых этим изменением. В GraphQL это относится к отдельному полю мутации; выполнение операций с несколькими полями описано в разделе [GraphQL](#graphql).
779
820
 
780
821
  REST возвращает 401 при отсутствии действительного токена и 403 при недостатке прав. GraphQL проверяет мутации по тем же правилам и возвращает коды `UNAUTHENTICATED` или `FORBIDDEN`; токен получают через REST-вход и передают в `Authorization: Bearer <token>`. GraphQL-запросы чтения, OPTIONS и все операции с файлами остаются открытыми.
781
822
 
@@ -794,7 +835,7 @@ export default {
794
835
  Запустите сервер:
795
836
 
796
837
  ```bash
797
- npx deep-json-server server.config.js
838
+ npx deep-json-server server.config.mjs
798
839
  ```
799
840
 
800
841
  Для временных тестов выберите `storage: 'memory'` и передайте массив в `files.source`. Каждая начальная запись содержит `name`, `mimeType`, бинарное `content` в виде `Uint8Array` и необязательный `directory`. Загруженные файлы в таком режиме остаются в памяти до завершения процесса.
@@ -829,13 +870,13 @@ Content-Type: image/jpeg
829
870
  Сочетание `directory` и `name` идентифицирует файл. Повторная загрузка по существующему пути возвращает `409`. Чтобы заменить файл, передайте `Content-Override: true`; успешная перезапись возвращает `200`. Сервер поддерживает следующие файловые маршруты:
830
871
 
831
872
  ```text
832
- POST /_files/storage Upload or replace a file
833
- GET /_files/storage/* Return file contents inline
834
- PATCH /_files/storage/* Rename or move a file
835
- DELETE /_files/storage/* Delete a file
873
+ POST /_files/storage Загрузить или заменить файл
874
+ GET /_files/storage/* Получить содержимое файла
875
+ PATCH /_files/storage/* Переименовать или переместить файл
876
+ DELETE /_files/storage/* Удалить файл
836
877
 
837
- GET /_files/metadata/* Return file metadata as JSON
838
- GET /_files/download/* Download a file as an attachment
878
+ GET /_files/metadata/* Получить метаданные в JSON
879
+ GET /_files/download/* Скачать файл как вложение
839
880
  ```
840
881
 
841
882
  Для переименования, перемещения либо обеих операций отправьте JSON-объект. Нужно указать хотя бы одно поле:
@@ -854,7 +895,7 @@ Content-Type: application/json
854
895
 
855
896
  При хранении на диске бинарный файл находится по пути `<files.source>/<directory>/<name>`. Метаданные по умолчанию хранятся в `<files.source>/.files.json`; другой путь можно задать через `files.metadata`. Директории и файл метаданных создаются по мере необходимости.
856
897
 
857
- Используйте один процесс сервера для каждого записываемого дискового источника. Второй сервер с общей базой, файлом пользователей auth, каталогом файлового хранилища или метаданными не запустится. Блокировки — отдельные JSON-файлы рядом с источниками: для `database.json` создаётся `database-lock.json`; последнее расширение имени из конфигурации убирается. Файл содержит тип блокировки, PID процесса и токен владельца. При штатном закрытии файлы блокировок удаляются; после аварии ОС освобождает блокировки, а следующий сервер повторно использует оставшиеся файлы. Для дисковых блокировок используется нативная зависимость `fs-native-extensions`; хранение в памяти и генерация схем её не загружают. Для источника с символической ссылкой блокируются исходная запись и конечная цель. Перед ручным изменением файлов или метаданных остановите сервер. Пути внутри каталога файлового хранилища не могут содержать символические ссылки. Загрузка и переименование не могут перезаписать базу, её блокировку, счётчики, схему, учётные записи auth, загруженную конфигурацию или файл метаданных.
898
+ Пути внутри каталога файлового хранилища не могут содержать символические ссылки. Загрузка и переименование не могут перезаписать базу, её блокировку, счётчики, схему, учётные записи, загруженную конфигурацию или файл метаданных. Для ручного изменения файлов или метаданных остановите сервер. Общие дисковые источники защищены блокировками, описанными в разделе [хранение данных](#хранение-данных).
858
899
 
859
900
  Отправляйте файл как бинарное тело запроса. В браузере для этого можно использовать `xhr.send(file)`, а прогресс отслеживать через `XMLHttpRequest.upload.onprogress`. Максимальный размер по умолчанию равен 100 МиБ и настраивается через `server.maxFileSize`. Отсутствующие или небезопасные заголовки и пути возвращают `400`, превышение лимита — `413`, а отсутствующий, некорректный или не поддерживаемый Fastify `Content-Type` — `400` либо `415` в зависимости от этапа проверки.
860
901
 
@@ -862,7 +903,7 @@ Content-Type: application/json
862
903
 
863
904
  ```js
864
905
  import { createServer } from '@kollors/deep-json-server/server';
865
- import config from './server.config.js';
906
+ import config from './server.config.mjs';
866
907
 
867
908
  const facade = await createServer(config);
868
909
  const server = facade.fastify();
@@ -870,9 +911,9 @@ await server.listen();
870
911
  // await server.close();
871
912
  ```
872
913
 
873
- Методы `openapi()` и `graphql()` возвращают схемы и требуют `database.schema`. `fastify()` возвращает экземпляр сервера для настройки и запуска. База и включённые сервисы инициализируются при `ready()`, `listen()` или первом `inject()`; ошибка инициализации останавливает запуск.
914
+ `fastify()` возвращает экземпляр сервера для настройки и запуска. База и включённые сервисы инициализируются при `ready()`, `listen()` или первом `inject()`; ошибка инициализации останавливает запуск.
874
915
 
875
- `createServer(config)` принимает тот же объект конфигурации, что и CLI. При наличии `openapi` обязателен `package.source`. Для методов `openapi()` и `graphql()` нужна соответствующая секция. Методы возвращают схему и не записывают файлы.
916
+ `createServer(config)` принимает тот же объект конфигурации, что и CLI. Для методов `openapi()` и `graphql()` нужны `database.schema` и соответствующая секция конфигурации. OpenAPI также требует `package.source`. Оба метода возвращают документы схем и не записывают файлы.
876
917
 
877
918
  Полученный экземпляр Fastify принимает обычные параметры `listen()`, включая путь Unix-сокета. Вызов `listen()` без параметров использует `server.host` и `server.port`.
878
919
 
@@ -888,13 +929,17 @@ await writeOpenapi(document, './generated/openapi.yaml');
888
929
  await writeGraphql(sdl, './generated/schema.graphql');
889
930
  ```
890
931
 
891
- Отдельные генераторы принимают путь или объект схемы и не требуют конфигурации сервера. Если корневой `api` не задан, `generateOpenapi()` использует REST по умолчанию, а `generateGraphql()` — GraphQL. Явные значения `api` в корне и у моделей сохраняют силу; для выбранного формата нужна хотя бы одна доступная модель. `timestamps` и `softDelete` берутся из схемы. Опция `{ auth: true }` добавляет поля владельца и `actions`; OpenAPI также описывает маршруты auth и требования токена. `hashPassword()` доступна и через общий импорт пакета.
932
+ Отдельные генераторы принимают путь или объект схемы и не требуют конфигурации сервера. Если корневой `api` не задан, `generateOpenapi()` использует REST по умолчанию, а `generateGraphql()` — GraphQL. Явные значения `api` в корне и у моделей сохраняют силу; для выбранного формата нужна хотя бы одна доступная модель. `timestamps` и `softDelete` берутся из схемы. Опция `{ auth: true }` добавляет поля владельца и `actions`; OpenAPI также описывает маршруты аутентификации и требования токена. `hashPassword()` доступна и через общий импорт пакета.
892
933
 
893
- Для `generateOpenapi()` обязателен `packagePath`; дополнительно доступны `host`, `port`, `pageSize` и `maxPageSize`. Метаданные OpenAPI читаются из указанного пакета. Вместо пути можно передать объект схемы. Сервер и генераторы работают с собственной копией модели. Размеры страниц должны быть положительными целыми числами; `pageSize` не может превышать `maxPageSize`.
934
+ Для `generateOpenapi()` обязателен `packagePath`; дополнительно доступны `host`, `port`, `pageSize` и `maxPageSize`. Метаданные OpenAPI читаются из указанного пакета. Сервер и генераторы работают с собственной копией модели. Размеры страниц должны быть положительными целыми числами; `pageSize` не может превышать `maxPageSize`.
894
935
 
895
936
  ## Хранение данных
896
937
 
897
- Изменения выполняются последовательно внутри экземпляра сервера и проверяются на копии данных до сохранения. Для одного файла базы используйте один процесс сервера. Счётчики `increment` хранятся рядом с базой в `<database path>.counters.json`; сохраняйте этот файл вместе с базой. Номера резервируются до записи данных: сбой может оставить пропуск, но не приводит к повторной выдаче номера.
938
+ Изменения выполняются последовательно внутри экземпляра сервера и проверяются на копии данных до сохранения. Счётчики `increment` хранятся рядом с базой в `<database path>.counters.json`; сохраняйте этот файл вместе с базой. Номера резервируются до записи данных: сбой может оставить пропуск, но не приводит к повторной выдаче номера.
939
+
940
+ Для каждого записываемого дискового источника используйте один процесс сервера. Второй сервер с общей базой, файлом пользователей, каталогом файлового хранилища или метаданными не запустится. Для блокировки рядом с источником создаётся JSON-файл: например, для `database.json` — `database-lock.json`. Последнее расширение имени источника заменяется на `-lock.json`. Файл содержит тип блокировки, PID процесса и токен владельца.
941
+
942
+ При закрытии сервера файлы блокировок удаляются. После аварии ОС освобождает блокировки, а следующий сервер использует оставшиеся файлы. Дисковые блокировки работают через нативную зависимость `fs-native-extensions`; хранение в памяти и генерация схем её не загружают. Если источник задан символической ссылкой, сервер блокирует и путь ссылки, и целевой путь. Перед ручным изменением любого хранилища остановите сервер.
898
943
 
899
944
  ## Разработка
900
945
 
@@ -905,8 +950,10 @@ npm ci
905
950
  npm run verify
906
951
  ```
907
952
 
908
- `npm run verify` проверяет типы, стиль кода, покрытие тестами и установку пакета из архива.
953
+ `npm run verify` сначала проверяет рабочие зависимости через `npm audit`, затем запускает проверку типов, линтер, тесты с порогами покрытия и проверку установки пакета из архива. Для установленного пакета проверяются публичные типы, запуск CLI с HTTP-запросами и восстановление после аварийного завершения процесса.
954
+
955
+ Для предварительной версии обновите номер в `package.json` и `package-lock.json`, затем отправьте коммит в `main`. Для стабильного релиза дополнительно отправьте тег `v<version>`, например `v1.0.0`. Сервер и CLI читают номер версии из метаданных пакета.
909
956
 
910
- Для публикации предварительной версии обновите номер в `package.json` и `package-lock.json` (сервер и CLI читают его из метаданных пакета), затем отправьте коммит в `main`. GitHub Actions создаст тег `v<version>` и опубликует пакет через trusted publishing в канал `alpha`, `beta` или `rc`. Стабильные версии публикуются в `latest` из явно отправленного тега версии.
957
+ GitHub Actions выполняет полную проверку на Linux, Windows и macOS с Node.js 22, 24 и 26. Публикация начинается только после успеха всех девяти запусков. Для предварительных версий тег создаётся автоматически, а пакет публикуется в канал `alpha`, `beta` или `rc`; стабильные версии попадают в `latest`. npm проверяет подлинность запуска через trusted publishing, поэтому хранить npm-токен в секретах не требуется.
911
958
 
912
959
  Лицензия: MIT.