@kollors/deep-json-server 1.0.0-alpha.1 → 1.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -49
- package/README.ru.md +81 -45
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/src/cli.js +88 -46
- package/dist/src/cli.js.map +1 -1
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +17 -4
- package/dist/src/config.js.map +1 -1
- package/dist/src/constants.d.ts +1 -0
- package/dist/src/constants.js +1 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/engine.d.ts +4 -4
- package/dist/src/engine.js +54 -121
- package/dist/src/engine.js.map +1 -1
- package/dist/src/errors.d.ts +6 -0
- package/dist/src/errors.js +9 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/features.d.ts +8 -0
- package/dist/src/features.js +18 -0
- package/dist/src/features.js.map +1 -0
- package/dist/src/files/disk-store.js +4 -4
- package/dist/src/files/disk-store.js.map +1 -1
- package/dist/src/graphql/preflight.d.ts +3 -0
- package/dist/src/graphql/preflight.js +28 -0
- package/dist/src/graphql/preflight.js.map +1 -0
- package/dist/src/graphql/routes.d.ts +4 -0
- package/dist/src/graphql/routes.js +28 -0
- package/dist/src/graphql/routes.js.map +1 -0
- package/dist/src/graphql.d.ts +1 -1
- package/dist/src/graphql.js +32 -28
- package/dist/src/graphql.js.map +1 -1
- package/dist/src/model.d.ts +1 -3
- package/dist/src/model.js +35 -9
- package/dist/src/model.js.map +1 -1
- package/dist/src/openapi/document.js +25 -22
- package/dist/src/openapi/document.js.map +1 -1
- package/dist/src/query/contract.d.ts +3 -0
- package/dist/src/query/contract.js +19 -0
- package/dist/src/query/contract.js.map +1 -0
- package/dist/src/query/filter.d.ts +4 -1
- package/dist/src/query/filter.js +76 -76
- package/dist/src/query/filter.js.map +1 -1
- package/dist/src/query/options.d.ts +1 -14
- package/dist/src/query/options.js +2 -107
- package/dist/src/query/options.js.map +1 -1
- package/dist/src/rest/options.d.ts +14 -0
- package/dist/src/rest/options.js +108 -0
- package/dist/src/rest/options.js.map +1 -0
- package/dist/src/rest/projection.d.ts +6 -0
- package/dist/src/rest/projection.js +37 -0
- package/dist/src/rest/projection.js.map +1 -0
- package/dist/src/rest/routes.d.ts +3 -0
- package/dist/src/rest/routes.js +48 -0
- package/dist/src/rest/routes.js.map +1 -0
- package/dist/src/schema.d.ts +17 -0
- package/dist/src/schema.js +23 -0
- package/dist/src/schema.js.map +1 -0
- package/dist/src/server.d.ts +2 -5
- package/dist/src/server.js +68 -132
- package/dist/src/server.js.map +1 -1
- package/package.json +1 -1
package/README.ru.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
JSON-сервер для имитации API: REST, GraphQL, вложенные запросы, бинарные файлы и экспорт схем. Требуется Node.js 22 или новее.
|
|
6
6
|
|
|
7
|
-
**1.0.0-alpha.
|
|
7
|
+
**1.0.0-alpha.2 — предварительная версия.** При переходе с 0.x обновите схему моделей и параметры запросов по примерам ниже.
|
|
8
8
|
|
|
9
9
|
## Установка
|
|
10
10
|
|
|
@@ -12,29 +12,35 @@ JSON-сервер для имитации API: REST, GraphQL, вложенные
|
|
|
12
12
|
npm install @kollors/deep-json-server@alpha
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Для установки конкретной версии укажите `@1.0.0-alpha.2`.
|
|
16
16
|
|
|
17
17
|
## Быстрый старт
|
|
18
18
|
|
|
19
|
+
Создайте два файла в одном каталоге.
|
|
20
|
+
|
|
21
|
+
`database.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"users": [
|
|
26
|
+
{ "id": "1", "fullName": "Мира Волкова" }
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
19
31
|
`server.config.js`:
|
|
20
32
|
|
|
21
33
|
```js
|
|
22
34
|
export default {
|
|
23
|
-
database: { path: './database.json'
|
|
24
|
-
graphql: { enabled: true, path: './generated/schema.graphql' },
|
|
25
|
-
openapi: { path: './generated/openapi.yaml' },
|
|
26
|
-
server: { host: '127.0.0.1', port: 4001, pageSize: 10, maxPageSize: 100 },
|
|
35
|
+
database: { path: './database.json' },
|
|
27
36
|
};
|
|
28
37
|
```
|
|
29
38
|
|
|
30
39
|
```sh
|
|
31
40
|
npx deep-json-server server.config.js
|
|
32
|
-
npx deep-json-server --openapi-only --graphql-only server.config.js
|
|
33
41
|
```
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
Полные примеры каталога: [база](examples/database.json), [схема моделей](examples/schema.json), [конфигурация](examples/server.config.js).
|
|
43
|
+
Список пользователей доступен по адресу `http://127.0.0.1:4001/users`.
|
|
38
44
|
|
|
39
45
|
## Конфигурация
|
|
40
46
|
|
|
@@ -42,6 +48,8 @@ npx deep-json-server --openapi-only --graphql-only server.config.js
|
|
|
42
48
|
|---|---|
|
|
43
49
|
| `database.path` / `database.data` | Ровно один: JSON-файл или объект коллекций в памяти |
|
|
44
50
|
| `database.schema` | Объект моделей или путь к JSON-файлу; необязателен для REST |
|
|
51
|
+
| `openapi.enabled` | Включить HTTP-маршрут спецификации; по умолчанию `false` |
|
|
52
|
+
| `openapi.endpoint` | Путь спецификации; по умолчанию `/openapi.json` |
|
|
45
53
|
| `openapi.path` | Путь экспорта YAML |
|
|
46
54
|
| `openapi.info` | Необязательные метаданные: `title`, `version`, `description` |
|
|
47
55
|
| `graphql.enabled` | Включить GraphQL HTTP API; по умолчанию `false` |
|
|
@@ -54,22 +62,34 @@ npx deep-json-server --openapi-only --graphql-only server.config.js
|
|
|
54
62
|
| `files.data` | Бинарные файлы в памяти |
|
|
55
63
|
| `files.directory`, `files.metadata` | Каталог файлов и JSON метаданных; необходимы оба |
|
|
56
64
|
|
|
57
|
-
|
|
65
|
+
Относительные пути отсчитываются от каталога конфигурационного файла. При вызове `createServer()` с объектом конфигурации — от рабочего каталога. Сервер работает с копией переданных данных в памяти.
|
|
58
66
|
|
|
59
67
|
| Флаг CLI | Действие |
|
|
60
68
|
|---|---|
|
|
61
69
|
| `--files` | Включить файловые маршруты |
|
|
62
70
|
| `--graphql` | Включить GraphQL API |
|
|
63
|
-
| `--openapi` |
|
|
64
|
-
| `--
|
|
65
|
-
| `--
|
|
66
|
-
| `--
|
|
67
|
-
| `--
|
|
71
|
+
| `--openapi` | Включить HTTP-маршрут OpenAPI |
|
|
72
|
+
| `--host <host>` | Адрес сервера |
|
|
73
|
+
| `--port <port>` | Порт сервера |
|
|
74
|
+
| `--help`, `-h` | Справка |
|
|
75
|
+
| `--version`, `-v` | Версия пакета |
|
|
68
76
|
|
|
69
|
-
|
|
77
|
+
Приоритет настроек: CLI → конфигурация → `HOST`/`PORT` → значения по умолчанию. Файловые маршруты включаются при наличии секции `files`.
|
|
78
|
+
|
|
79
|
+
Для генерации укажите формат и файл конфигурации:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
npx deep-json-server generate openapi server.config.js
|
|
83
|
+
npx deep-json-server generate graphql server.config.js
|
|
84
|
+
npx deep-json-server generate openapi,graphql server.config.js
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Команда читает `database.schema` и сохраняет схемы по путям `openapi.path` и `graphql.path`. Данные базы и файловое хранилище для генерации не требуются.
|
|
70
88
|
|
|
71
89
|
## Схема моделей
|
|
72
90
|
|
|
91
|
+
Примеры: [база данных](examples/database.json), [схема моделей](examples/schema.json), [конфигурация](examples/server.config.js).
|
|
92
|
+
|
|
73
93
|
```json
|
|
74
94
|
{
|
|
75
95
|
"Country": {
|
|
@@ -102,11 +122,11 @@ npx deep-json-server --openapi-only --graphql-only server.config.js
|
|
|
102
122
|
| Экспорт OpenAPI 3.0.3 | Доступен | Ошибка при запросе экспорта |
|
|
103
123
|
| GraphQL SDL / API | Доступен | Ошибка при запросе |
|
|
104
124
|
|
|
105
|
-
При явной схеме неизвестные поля и коллекции запрещены. Исключение — хранимые ключи, выведенные из связей. Исходная база проверяется при загрузке.
|
|
125
|
+
При явной схеме неизвестные поля и коллекции запрещены. Исключение — хранимые ключи, выведенные из связей. Исходная база проверяется при загрузке. Генерация использует описание моделей. REST без схемы создаёт ключ `id` и сохраняет произвольные JSON-поля. Для фильтров и выбора отдельных полей используются имена в формате идентификаторов; остальные поля возвращаются через `scope=*`.
|
|
106
126
|
|
|
107
127
|
### Поля
|
|
108
128
|
|
|
109
|
-
|
|
129
|
+
Поле `type` принимает `string`, `number`, `boolean`, `object` или имя модели. Для массива добавьте суффикс `[]`: `string[]`, `object[]`, `Genre[]`. Вложенные поля описываются через точку, например `actors.fullName`.
|
|
110
130
|
|
|
111
131
|
| Свойства | Назначение |
|
|
112
132
|
|---|---|
|
|
@@ -123,11 +143,11 @@ npx deep-json-server --openapi-only --graphql-only server.config.js
|
|
|
123
143
|
| `minimum`, `maximum` | Включительные числовые границы |
|
|
124
144
|
| `source`, `target`, `onDelete` | Описание связи |
|
|
125
145
|
|
|
126
|
-
Ограничения строк и чисел применяются к каждому элементу `string[]`/`number[]`. `required
|
|
146
|
+
Ограничения строк и чисел применяются к каждому элементу `string[]`/`number[]`. `required` и `nullable` относятся ко всему массиву; `default` и `example` содержат массив целиком. Элементы массива должны соответствовать его типу и быть отличны от `null`. `required` требует наличия поля; обычный массив при этом может быть пустым.
|
|
127
147
|
|
|
128
|
-
У модели ровно один
|
|
148
|
+
У модели ровно один первичный ключ типа `string` или `number`, объявленный на верхнем уровне. Имя произвольное: `id`, `username`, `code`. Если `generated` не задан, значение передаёт клиент при создании. Генерируемые поля объявляются на верхнем уровне, исключаются из входных типов и несовместимы с `default`. При замене записи сохраняются генерируемые значения и поля `readOnly`, включая вложенные объекты. Для защиты полей внутри массива задайте `readOnly` всему массиву или содержащему его объекту. Объекты, состоящие из серверных полей, доступны только в ответах.
|
|
129
149
|
|
|
130
|
-
Например, `LocalUser` с первичным `username` и полем `password: {"type":"string","required":true,"writeOnly":true}` получает запрос `localUser(username: ...)` и маршрут `/localUsers/{username}`.
|
|
150
|
+
Например, `LocalUser` с первичным ключом `username` и полем `password: {"type":"string","required":true,"writeOnly":true}` получает запрос `localUser(username: ...)` и маршрут `/localUsers/{username}`. Поле `writeOnly` доступно для записи и исключено из ответов, `scope`, фильтров и сортировки.
|
|
131
151
|
|
|
132
152
|
### Связи
|
|
133
153
|
|
|
@@ -139,20 +159,20 @@ npx deep-json-server --openapi-only --graphql-only server.config.js
|
|
|
139
159
|
}
|
|
140
160
|
```
|
|
141
161
|
|
|
142
|
-
`Genre` возвращает объект, `Genre[]` — список.
|
|
162
|
+
`Genre` возвращает объект, `Genre[]` — список. По умолчанию `source` указывает на первичный ключ текущей модели, `target` — целевой. Это правило действует и для вложенных связей. Пути задаются от корня соответствующей записи: в примере `actors.genreIds` содержит ключи жанров текущего актёра.
|
|
143
163
|
|
|
144
|
-
|
|
164
|
+
Ключи связей хранятся в базе и входят в собственные поля записи. Их типы выводятся из сопоставляемых ключей. Если поле `source` ссылается на первичный ключ целевой модели, его можно не объявлять отдельно: для множественной связи создаётся описание массива ключей, для одиночной — одного значения. При неоднозначном сопоставлении опишите хранимое поле явно.
|
|
145
165
|
|
|
146
166
|
Обратная связь: `User.movies = {"type":"Movie[]","target":"actors.userId"}`. Фильм возвращается один раз, даже если совпало несколько актёров. Несколько совпадений для одиночной связи — ошибка.
|
|
147
167
|
|
|
148
|
-
|
|
168
|
+
Каждый переданный ключ прямой связи должен указывать на существующую запись. `required: true` у связи требует хотя бы одну связанную запись до фильтрации и пагинации ответа. Обратная связь с первичным ключом в `source` может быть пустой, если не объявлена обязательной. Отсутствующая одиночная связь возвращает `null`.
|
|
149
169
|
|
|
150
170
|
`onDelete` срабатывает **при удалении целевой записи**:
|
|
151
171
|
|
|
152
172
|
- `restrict` — по умолчанию: запретить удаление, пока на цель ссылается сохраняемая запись.
|
|
153
|
-
- `cascade` — удалить ссылающуюся запись. Для `User.country` удаление страны удаляет пользователей. Для `Movie.actors.user` удаление пользователя удаляет соответствующие элементы actors
|
|
173
|
+
- `cascade` — удалить ссылающуюся запись. Для `User.country` удаление страны удаляет пользователей. Для `Movie.actors.user` удаление пользователя удаляет соответствующие элементы `actors`, сохраняя фильм.
|
|
154
174
|
|
|
155
|
-
|
|
175
|
+
Каскадное удаление выполняется целиком, включая циклические связи. Ошибка проверки отменяет всю операцию. Правила `onDelete` действуют и на явно объявленные обратные связи; при описании обоих направлений учитывайте оба правила.
|
|
156
176
|
|
|
157
177
|
## Запросы и ответы
|
|
158
178
|
|
|
@@ -162,9 +182,11 @@ npx deep-json-server --openapi-only --graphql-only server.config.js
|
|
|
162
182
|
{ "data": [], "total": 0 }
|
|
163
183
|
```
|
|
164
184
|
|
|
165
|
-
Массивы примитивов
|
|
185
|
+
Массивы примитивов возвращаются обычными массивами. Каждый список объектов принимает необязательные `where`, `order` и `pager`. Порядок обработки: фильтрация → сортировка → пагинация. `total` — число записей после фильтрации, до пагинации.
|
|
166
186
|
|
|
167
|
-
|
|
187
|
+
В `pager` задаются `page` и `pageSize`. По умолчанию возвращается первая страница с размером из `server.pageSize`. Оба значения должны быть положительными целыми числами; `pageSize` ограничен `server.maxPageSize`. За пределами списка возвращается пустой `data` с общим числом найденных записей в `total`.
|
|
188
|
+
|
|
189
|
+
Фильтры: `eq`, `ne`, `in`; для строк `contains`, `startsWith`, `endsWith`; сравнения `gt`, `gte`, `lt`, `lte`. Условия объединяются через `and`, `or`, `not`. У массивов есть `some`, `every`, `none`; у примитивных массивов также `contains`, `in`. Поиск строк не учитывает регистр. Условие поля задаётся объектом с оператором, например `{ "id": { "eq": "1" } }`.
|
|
168
190
|
|
|
169
191
|
```json
|
|
170
192
|
{
|
|
@@ -180,9 +202,9 @@ npx deep-json-server --openapi-only --graphql-only server.config.js
|
|
|
180
202
|
}
|
|
181
203
|
```
|
|
182
204
|
|
|
183
|
-
Корневой where выбирает
|
|
205
|
+
Корневой `where` выбирает записи основной коллекции. `where` внутри связи фильтрует её элементы, сохраняя родительскую запись. Каждый вложенный список обрабатывается отдельно. Фильтрация по связи работает независимо от её включения в ответ.
|
|
184
206
|
|
|
185
|
-
`order` — массив правил `{ "field": "fullName", "direction": "ASC" }`. Первое правило приоритетнее;
|
|
207
|
+
`order` — массив правил `{ "field": "fullName", "direction": "ASC" }`. Первое правило приоритетнее; при равных значениях сохраняется порядок хранения. `null` и отсутствующее значение сравниваются одинаково. REST использует путь `profile.name`, GraphQL — enum `profile_name`. Неоднозначные имена enum вызывают ошибку генерации. Сортировка доступна по скалярным полям текущего объекта, включая вложенные поля. Для связанных списков задаётся собственный `order`.
|
|
186
208
|
|
|
187
209
|
### REST
|
|
188
210
|
|
|
@@ -195,7 +217,7 @@ npx deep-json-server --openapi-only --graphql-only server.config.js
|
|
|
195
217
|
| PATCH | `/users/{id}` | `userUpdate` |
|
|
196
218
|
| DELETE | `/users/{id}` | `userDelete` |
|
|
197
219
|
|
|
198
|
-
Имя параметра пути соответствует первичному ключу. POST
|
|
220
|
+
Имя параметра пути соответствует первичному ключу. POST, PUT и PATCH принимают JSON-объект записи. PUT заменяет запись с сохранением ключа и серверных полей. PATCH объединяет поля на верхнем уровне; переданные вложенные объекты заменяются с сохранением их полей `readOnly`. Создание и замена требуют всех обязательных полей. При обновлении проверяются переданные значения и итоговая запись. Отсутствующая запись — `404`, конфликт — `409`. DELETE возвращает удалённую запись.
|
|
199
221
|
|
|
200
222
|
Параметры `where`, `order`, `pager`, `nested` содержат JSON; `scope` — строку выбора. Пример до URL-кодирования:
|
|
201
223
|
|
|
@@ -213,7 +235,7 @@ const params = new URLSearchParams({
|
|
|
213
235
|
const response = await fetch(`/users?${params}`);
|
|
214
236
|
```
|
|
215
237
|
|
|
216
|
-
`scope=*,actors(user(id,fullName),genres(*))` выбирает собственные поля и
|
|
238
|
+
`scope=*,actors(user(id,fullName),genres(*))` выбирает собственные поля и указанные связи. `*` включает собственные поля текущего объекта и хранимые ключи, кроме полей `writeOnly`. Связи перечисляются явно. По умолчанию выбираются собственные поля. Списки сохраняют структуру ответа `{ data, total }`.
|
|
217
239
|
|
|
218
240
|
`nested` сопоставляет полные пути ответа и настройки списков:
|
|
219
241
|
|
|
@@ -227,7 +249,7 @@ const response = await fetch(`/users?${params}`);
|
|
|
227
249
|
}
|
|
228
250
|
```
|
|
229
251
|
|
|
230
|
-
Путь nested должен быть выбран через scope и вести к списку объектов.
|
|
252
|
+
Путь в `nested` должен быть выбран через `scope` и вести к списку объектов. Маршруты отдельных записей и мутации принимают `scope` и `nested`; корневые `where`, `order` и `pager` применяются к GET коллекции. Неизвестные имена и небезопасные пути возвращают `400`.
|
|
231
253
|
|
|
232
254
|
### GraphQL
|
|
233
255
|
|
|
@@ -250,10 +272,9 @@ query {
|
|
|
250
272
|
}
|
|
251
273
|
```
|
|
252
274
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
Строковый первичный ключ представлен GraphQL ID, обычные строки — String, числа — Float, пагинация — Int. Enum сохраняет допустимые строковые имена; остальные значения получают имена VALUE_0, VALUE_1 и т. д. Длины строк и форматы проверяет общий валидатор во время выполнения: SDL не выражает все ограничения. Доступны интроспекция, обычные query и mutation; подписки и массовые мутации не добавлены.
|
|
275
|
+
Запрос `user(id: ...)` возвращает одну запись или `null`, если она отсутствует. Мутации: `userCreate(data: ...)`, `userReplace(id: ..., data: ...)`, `userUpdate(id: ..., data: ...)`, `userDelete(id: ...)`. Для модели, состоящей из генерируемых полей, мутация создания вызывается без аргумента `data`. Правила записи и валидации совпадают с REST. Связи, выбранные в результате мутации, определяют содержимое ответа.
|
|
256
276
|
|
|
277
|
+
Строковый первичный ключ представлен типом GraphQL `ID`, обычные строки — `String`, числа — `Float`, параметры пагинации — `Int`. Enum сохраняет допустимые строковые имена; остальные значения получают имена `VALUE_0`, `VALUE_1` и т. д. Длины строк, форматы и другие ограничения модели проверяются сервером при выполнении запроса. Для просмотра схемы доступна интроспекция. Параметры выбранных списков проверяются до выполнения мутаций. Ошибки содержат `extensions.code`: `INVALID_INPUT`, `INVALID_QUERY`, `NOT_FOUND`, `CONFLICT` или `INTERNAL_ERROR`.
|
|
257
278
|
|
|
258
279
|
## Пример базы данных
|
|
259
280
|
|
|
@@ -361,10 +382,10 @@ query {
|
|
|
361
382
|
|
|
362
383
|
## Файлы
|
|
363
384
|
|
|
364
|
-
Добавьте `files.directory` и `files.metadata` в
|
|
385
|
+
Добавьте `files.directory` и `files.metadata` в конфигурацию и запустите сервер:
|
|
365
386
|
|
|
366
387
|
```bash
|
|
367
|
-
deep-json-server
|
|
388
|
+
deep-json-server server.config.js
|
|
368
389
|
```
|
|
369
390
|
|
|
370
391
|
Для временных тестов вместо этого используйте `files.data`. Каждая начальная запись содержит `name`, `mimeType`, бинарное `content` в виде `Uint8Array` и необязательный `directory`. Загруженные файлы в таком режиме остаются в памяти до завершения процесса.
|
|
@@ -420,9 +441,9 @@ Content-Type: application/json
|
|
|
420
441
|
|
|
421
442
|
`PATCH` возвращает обновлённые метаданные со статусом `200`; если по новому пути уже существует файл, сервер возвращает `409`. `DELETE` отвечает статусом `204` без тела. Если файл не найден, любая операция по пути возвращает `404`. Пути в URL задаются относительно `files.directory`, а все возвращаемые URL — относительно адреса сервера.
|
|
422
443
|
|
|
423
|
-
При хранении на диске бинарный файл находится по пути `<files.directory>/<directory>/<name>`. Файл метаданных содержит только `directory`, `mimeType` и `name`; `size` считывается у фактического файла, а URL вычисляются. Используйте дисковую базу и файловое хранилище только из одного процесса сервера одновременно и не редактируйте сохранённые файлы или метаданные до его остановки. Сервер автоматически создаёт директории, пути внутри `files.directory` не могут содержать символические ссылки, а имена файлов ограничены переносимыми между поддерживаемыми операционными системами значениями. Изначально файл метаданных может отсутствовать: он создаётся при первой загрузке.
|
|
444
|
+
При хранении на диске бинарный файл находится по пути `<files.directory>/<directory>/<name>`. Файл метаданных содержит только `directory`, `mimeType` и `name`; `size` считывается у фактического файла, а URL вычисляются. Используйте дисковую базу и файловое хранилище только из одного процесса сервера одновременно и не редактируйте сохранённые файлы или метаданные до его остановки. Сервер автоматически создаёт директории, пути внутри `files.directory` не могут содержать символические ссылки, а имена файлов ограничены переносимыми между поддерживаемыми операционными системами значениями. Изначально файл метаданных может отсутствовать: он создаётся при первой загрузке.
|
|
424
445
|
|
|
425
|
-
|
|
446
|
+
Отправляйте файл как бинарное тело запроса. В браузере для этого можно использовать `xhr.send(file)`, а прогресс отслеживать через `XMLHttpRequest.upload.onprogress`. Максимальный размер по умолчанию равен 100 МиБ и настраивается через `server.maxFileSize`. Отсутствующие или небезопасные заголовки и пути возвращают `400`, превышение лимита — `413`, а отсутствующий, некорректный или не поддерживаемый Fastify `Content-Type` — `400` либо `415` в зависимости от этапа проверки.
|
|
426
447
|
|
|
427
448
|
## Программный API
|
|
428
449
|
|
|
@@ -438,19 +459,34 @@ await server.listen();
|
|
|
438
459
|
// await server.close();
|
|
439
460
|
```
|
|
440
461
|
|
|
441
|
-
|
|
462
|
+
Методы `openapi()` и `graphql()` возвращают схемы. `fastify()` возвращает экземпляр сервера для настройки и запуска. База и включённые сервисы инициализируются при `ready()`, `listen()` или первом `inject()`; ошибка инициализации останавливает запуск. Возможности сервера можно переопределить через `createServer(config, { files: false, graphql: true, openapi: true })`.
|
|
463
|
+
|
|
464
|
+
Генераторы можно использовать отдельно:
|
|
465
|
+
|
|
466
|
+
```js
|
|
467
|
+
import { generateOpenapi, generateGraphql, writeOpenapi, writeGraphql } from '@kollors/deep-json-server';
|
|
468
|
+
|
|
469
|
+
const document = await generateOpenapi('./schema.json', { files: true });
|
|
470
|
+
const sdl = await generateGraphql('./schema.json');
|
|
471
|
+
await writeOpenapi(document, './generated/openapi.yaml');
|
|
472
|
+
await writeGraphql(sdl, './generated/schema.graphql');
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
`generateOpenapi()` также принимает `host`, `port`, `pageSize`, `maxPageSize` и `info`. Вместо пути можно передать объект схемы. Сервер и генераторы работают с собственной копией модели.
|
|
442
476
|
|
|
443
477
|
## Хранение и разработка
|
|
444
478
|
|
|
445
|
-
Изменения выполняются последовательно внутри экземпляра сервера и проверяются на копии данных до сохранения. Для одного файла базы используйте один
|
|
479
|
+
Изменения выполняются последовательно внутри экземпляра сервера и проверяются на копии данных до сохранения. Для одного файла базы используйте один процесс сервера. Счётчики `increment` хранятся рядом с базой в `<путь базы>.counters.json`; сохраняйте этот файл вместе с базой. Номера резервируются до записи данных: сбой может оставить пропуск, но не приводит к повторной выдаче номера.
|
|
446
480
|
|
|
447
|
-
|
|
481
|
+
Сервер предназначен для имитации API. Авторизацию и хеширование паролей при необходимости реализуйте в приложении.
|
|
448
482
|
|
|
449
483
|
```sh
|
|
450
484
|
npm ci
|
|
451
485
|
npm run verify
|
|
452
486
|
```
|
|
453
487
|
|
|
454
|
-
|
|
488
|
+
Команда проверяет типы, стиль кода, покрытие тестами и установку пакета из архива.
|
|
489
|
+
|
|
490
|
+
Для публикации новой альфы обновите версию в `package.json` и отправьте изменения в `main`. GitHub Actions создаст тег версии и опубликует пакет в канал npm `alpha` через trusted publishing. Уже опубликованная версия пропускается. Если тег создан, а публикация не завершилась, повторный запуск использует этот тег и проверяет соответствие ему файлов пакета. Отправка тега версии также запускает публикацию; стабильные версии публикуются в `latest`.
|
|
455
491
|
|
|
456
492
|
Лицензия: MIT.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export type { DatabaseConfig, DeepJsonServerConfig, FilesConfig, GraphqlConfig, MemoryFile, OpenapiConfig, ServerConfig } from './src/config.js';
|
|
2
|
+
export type { ServerFeatures } from './src/features.js';
|
|
2
3
|
export type { FileMetadata, FileUpdate } from './src/files/contract.js';
|
|
3
4
|
export type { EntityDefinition, Field, ModelSchema } from './src/model.js';
|
|
5
|
+
export type { OpenapiOptions } from './src/schema.js';
|
|
6
|
+
export { generateGraphql, generateOpenapi, writeGraphql, writeOpenapi } from './src/schema.js';
|
|
4
7
|
export type { ServerFacade } from './src/server.js';
|
|
5
8
|
export { createServer } from './src/server.js';
|
|
6
9
|
export type { OpenapiDocument } from './src/types.js';
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/F,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/src/cli.js
CHANGED
|
@@ -1,66 +1,108 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
|
-
import { readServerConfig } from './config.js';
|
|
3
|
-
import { DEFAULT_HOST, DEFAULT_PORT } from './constants.js';
|
|
2
|
+
import { normalizeServerConfig, readServerConfig } from './config.js';
|
|
3
|
+
import { DEFAULT_HOST, DEFAULT_PORT, VERSION } from './constants.js';
|
|
4
|
+
import { resolveFeatures } from './features.js';
|
|
5
|
+
import { generateGraphql, generateOpenapi, writeGraphql, writeOpenapi } from './schema.js';
|
|
4
6
|
import { createServer } from './server.js';
|
|
5
7
|
const HELP_TEXT = `Deep JSON Server
|
|
6
8
|
|
|
7
9
|
Usage:
|
|
8
|
-
deep-json-server [
|
|
10
|
+
deep-json-server [options] <server.config.js>
|
|
11
|
+
deep-json-server generate <openapi|graphql|openapi,graphql> <server.config.js>
|
|
9
12
|
|
|
10
|
-
--files
|
|
11
|
-
--graphql
|
|
12
|
-
--openapi
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
13
|
+
--files Enable file routes
|
|
14
|
+
--graphql Enable the GraphQL endpoint
|
|
15
|
+
--openapi Enable the OpenAPI endpoint
|
|
16
|
+
--host <host> Server address
|
|
17
|
+
--port <port> Server port
|
|
18
|
+
--help, -h Show help
|
|
19
|
+
--version, -v Show version
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
Files are enabled when configured. Generate writes schemas to the configured paths.`;
|
|
19
22
|
export async function runCli(args = process.argv.slice(2), services = { createServer }) {
|
|
20
|
-
if (args.includes('--help')) {
|
|
23
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
21
24
|
process.stdout.write(`${HELP_TEXT}\n`);
|
|
22
25
|
return;
|
|
23
26
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
28
|
+
process.stdout.write(`${VERSION}\n`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const positional = [];
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
const features = {};
|
|
34
|
+
let host;
|
|
35
|
+
let port;
|
|
36
|
+
for (let index = 0; index < args.length; index++) {
|
|
37
|
+
const arg = args[index];
|
|
38
|
+
if (!arg.startsWith('-')) {
|
|
39
|
+
positional.push(arg);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (!['--files', '--graphql', '--openapi', '--host', '--port'].includes(arg) || seen.has(arg))
|
|
43
|
+
throw new Error(`Неизвестный параметр или повтор: ${arg}`);
|
|
44
|
+
seen.add(arg);
|
|
45
|
+
if (arg === '--host' || arg === '--port') {
|
|
46
|
+
const value = args[++index];
|
|
47
|
+
if (!value || value.startsWith('-'))
|
|
48
|
+
throw new Error(`Укажите значение ${arg}`);
|
|
49
|
+
if (arg === '--host')
|
|
50
|
+
host = value;
|
|
51
|
+
else {
|
|
52
|
+
if (!/^\d+$/.test(value))
|
|
53
|
+
throw new Error('Invalid --port');
|
|
54
|
+
port = Number(value);
|
|
55
|
+
}
|
|
31
56
|
}
|
|
32
|
-
else if (configPath === undefined)
|
|
33
|
-
configPath = arg;
|
|
34
57
|
else
|
|
35
|
-
|
|
58
|
+
features[arg.slice(2)] = true;
|
|
36
59
|
}
|
|
60
|
+
const generate = positional[0] === 'generate';
|
|
61
|
+
const configPath = positional[generate ? 2 : 0];
|
|
37
62
|
if (!configPath)
|
|
38
63
|
throw new Error('Укажите путь к файлу конфигурации');
|
|
39
|
-
if (
|
|
40
|
-
throw new Error('
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
throw new Error('
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
if (positional.length !== (generate ? 3 : 1))
|
|
65
|
+
throw new Error('Можно указать только один файл конфигурации');
|
|
66
|
+
const formats = generate ? positional[1].split(',') : [];
|
|
67
|
+
if (generate && (formats.some((format) => !['openapi', 'graphql'].includes(format)) || new Set(formats).size !== formats.length))
|
|
68
|
+
throw new Error('Invalid generation format');
|
|
69
|
+
if (generate && (features.graphql || features.openapi))
|
|
70
|
+
throw new Error('Endpoint flags are only available when starting the server');
|
|
71
|
+
const source = await readServerConfig(configPath);
|
|
72
|
+
const config = normalizeServerConfig({
|
|
73
|
+
...source,
|
|
74
|
+
server: { ...source.server, host: host ?? source.server.host ?? process.env.HOST ?? DEFAULT_HOST, port: port ?? source.server.port ?? Number(process.env.PORT ?? DEFAULT_PORT) },
|
|
75
|
+
});
|
|
76
|
+
const enabled = resolveFeatures(config, features);
|
|
77
|
+
if (generate) {
|
|
78
|
+
if (!config.database.schema)
|
|
79
|
+
throw new Error('Generation requires an explicit model schema');
|
|
80
|
+
// Validate all destinations before writing either document.
|
|
81
|
+
for (const format of formats)
|
|
82
|
+
if (!config[format].path)
|
|
83
|
+
throw new Error(`Укажите config.${format}.path`);
|
|
84
|
+
const openapi = formats.includes('openapi')
|
|
85
|
+
? await generateOpenapi(config.database.schema, {
|
|
86
|
+
files: enabled.files,
|
|
87
|
+
host: config.server.host,
|
|
88
|
+
port: config.server.port,
|
|
89
|
+
pageSize: config.server.pageSize ?? Math.min(10, config.server.maxPageSize ?? 100),
|
|
90
|
+
maxPageSize: config.server.maxPageSize,
|
|
91
|
+
info: config.openapi.info,
|
|
92
|
+
})
|
|
93
|
+
: undefined;
|
|
94
|
+
const graphql = formats.includes('graphql') ? await generateGraphql(config.database.schema) : undefined;
|
|
95
|
+
if (openapi) {
|
|
96
|
+
await writeOpenapi(openapi, config.openapi.path);
|
|
97
|
+
process.stdout.write(`OpenAPI: ${config.openapi.path}\n`);
|
|
98
|
+
}
|
|
99
|
+
if (graphql) {
|
|
100
|
+
await writeGraphql(graphql, config.graphql.path);
|
|
101
|
+
process.stdout.write(`GraphQL: ${config.graphql.path}\n`);
|
|
102
|
+
}
|
|
62
103
|
return;
|
|
63
|
-
|
|
104
|
+
}
|
|
105
|
+
const server = (await services.createServer(config, enabled)).fastify();
|
|
64
106
|
await server.listen();
|
|
65
107
|
server.log.info('Deep JSON Server started');
|
|
66
108
|
}
|
package/dist/src/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"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"}
|