@kollors/deep-json-server 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -201,16 +201,23 @@ Create a small configuration file next to the database, for example `mock/databa
201
201
 
202
202
  ```json
203
203
  {
204
- "movies": {
205
- "optional": ["description"],
206
- "formats": {
207
- "coverSrc": "uri"
208
- }
204
+ "$info": {
205
+ "title": "Movie Catalog API",
206
+ "version": "1.0.0"
209
207
  },
210
- "users": {
211
- "formats": {
212
- "avatarSrc": "uri",
213
- "bornAt": "date"
208
+ "$schema": {
209
+ "movies": {
210
+ "required": ["actors", "actors.genreIds", "actors.userId", "publisherIds", "title"],
211
+ "formats": {
212
+ "coverSrc": "uri"
213
+ }
214
+ },
215
+ "users": {
216
+ "required": ["bornAt", "fullName"],
217
+ "formats": {
218
+ "avatarSrc": "uri",
219
+ "bornAt": "date"
220
+ }
214
221
  }
215
222
  }
216
223
  }
@@ -219,17 +226,21 @@ Create a small configuration file next to the database, for example `mock/databa
219
226
  Generate an OpenAPI 3.0.3 file and exit:
220
227
 
221
228
  ```bash
222
- deep-json-server mock/database.json --generate mock/database-schema.json mock/openapi-schema.yaml
229
+ deep-json-server mock/database.json --generate mock/database-schema.json mock/openapi-schema.yaml --host 127.0.0.1 --port 4001
223
230
  ```
224
231
 
225
- The generator infers resources and field types from all database records. Fields present in every record are required unless listed in `optional`; `formats` adds OpenAPI formats such as `date` and `uri`. Nested fields use dot paths, for example `actors.id`.
232
+ The generator infers resources and field types from all database records. Every inferred field is optional by default, while the top-level resource record's primary key `id` is always required. Add other required fields to `required`; nested fields use dot paths such as `actors.userId`. The `formats` object adds OpenAPI formats such as `date` and `uri`.
233
+
234
+ `$info` becomes the OpenAPI `info` object, while resource settings live under `$schema`. The OpenAPI `servers` entry is generated automatically from `--host` and `--port`, their `HOST` and `PORT` environment variable equivalents, or the default `http://127.0.0.1:4001`.
226
235
 
227
236
  Use `name` when a resource needs an explicit schema name instead of the automatically singularized name:
228
237
 
229
238
  ```json
230
239
  {
231
- "equipment": {
232
- "name": "Equipment"
240
+ "$schema": {
241
+ "equipment": {
242
+ "name": "Equipment"
243
+ }
233
244
  }
234
245
  }
235
246
  ```
@@ -249,7 +260,7 @@ await server.close();
249
260
 
250
261
  await startServer({ databasePath: 'mock/database.json', host: '127.0.0.1', port: 4001 });
251
262
 
252
- await generateOpenApi({ databasePath: 'mock/database.json', schemaPath: 'mock/database-schema.json', outputPath: 'mock/openapi-schema.yaml' });
263
+ await generateOpenApi({ databasePath: 'mock/database.json', host: '127.0.0.1', port: 4001, schemaPath: 'mock/database-schema.json', outputPath: 'mock/openapi-schema.yaml' });
253
264
  ```
254
265
 
255
266
  `createServer()` is useful for tests because it returns a Fastify instance without opening a network port.
package/README.ru.md CHANGED
@@ -201,16 +201,23 @@ GET /countries/1?_embed=users
201
201
 
202
202
  ```json
203
203
  {
204
- "movies": {
205
- "optional": ["description"],
206
- "formats": {
207
- "coverSrc": "uri"
208
- }
204
+ "$info": {
205
+ "title": "API каталога фильмов",
206
+ "version": "1.0.0"
209
207
  },
210
- "users": {
211
- "formats": {
212
- "avatarSrc": "uri",
213
- "bornAt": "date"
208
+ "$schema": {
209
+ "movies": {
210
+ "required": ["actors", "actors.genreIds", "actors.userId", "publisherIds", "title"],
211
+ "formats": {
212
+ "coverSrc": "uri"
213
+ }
214
+ },
215
+ "users": {
216
+ "required": ["bornAt", "fullName"],
217
+ "formats": {
218
+ "avatarSrc": "uri",
219
+ "bornAt": "date"
220
+ }
214
221
  }
215
222
  }
216
223
  }
@@ -219,17 +226,21 @@ GET /countries/1?_embed=users
219
226
  Сгенерируйте OpenAPI 3.0.3 и завершите работу:
220
227
 
221
228
  ```bash
222
- deep-json-server mock/database.json --generate mock/database-schema.json mock/openapi-schema.yaml
229
+ deep-json-server mock/database.json --generate mock/database-schema.json mock/openapi-schema.yaml --host 127.0.0.1 --port 4001
223
230
  ```
224
231
 
225
- Генератор определяет ресурсы и типы полей по всем записям базы. Поля, присутствующие в каждой записи, считаются обязательными, если они не перечислены в `optional`; `formats` добавляет форматы OpenAPI, например `date` и `uri`. Для вложенных полей используются пути через точку, например `actors.id`.
232
+ Генератор определяет ресурсы и типы полей по всем записям базы. По умолчанию все найденные поля необязательные, а первичный ключ `id` корневой записи ресурса всегда обязательный. Остальные обязательные поля перечисляются в `required`; для вложенных полей используются пути через точку, например `actors.userId`. Объект `formats` добавляет форматы OpenAPI, например `date` и `uri`.
233
+
234
+ `$info` становится объектом `info` в OpenAPI, а настройки ресурсов находятся внутри `$schema`. Поле `servers` в OpenAPI формируется автоматически из параметров `--host` и `--port`, соответствующих переменных окружения `HOST` и `PORT` или адреса по умолчанию `http://127.0.0.1:4001`.
226
235
 
227
236
  Используйте `name`, если ресурсу нужно явно задать имя схемы вместо автоматически полученного имени в единственном числе:
228
237
 
229
238
  ```json
230
239
  {
231
- "equipment": {
232
- "name": "Equipment"
240
+ "$schema": {
241
+ "equipment": {
242
+ "name": "Equipment"
243
+ }
233
244
  }
234
245
  }
235
246
  ```
@@ -249,7 +260,7 @@ await server.close();
249
260
 
250
261
  await startServer({ databasePath: 'mock/database.json', host: '127.0.0.1', port: 4001 });
251
262
 
252
- await generateOpenApi({ databasePath: 'mock/database.json', schemaPath: 'mock/database-schema.json', outputPath: 'mock/openapi-schema.yaml' });
263
+ await generateOpenApi({ databasePath: 'mock/database.json', host: '127.0.0.1', port: 4001, schemaPath: 'mock/database-schema.json', outputPath: 'mock/openapi-schema.yaml' });
253
264
  ```
254
265
 
255
266
  `createServer()` удобен для тестов: он возвращает экземпляр Fastify, не открывая сетевой порт.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kollors/deep-json-server",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "JSON mock server with deep filters and recursive relationship embedding",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -6,7 +6,7 @@ const HELP = `Deep JSON Server
6
6
 
7
7
  Использование:
8
8
  deep-json-server <database.json> [--host <host>] [--port <port>]
9
- deep-json-server <database.json> --generate <database-schema.json> <openapi-schema.yaml>
9
+ deep-json-server <database.json> --generate <database-schema.json> <openapi-schema.yaml> [--host <host>] [--port <port>]
10
10
 
11
11
  Параметры:
12
12
  --generate Сгенерировать OpenAPI и завершить работу
@@ -53,11 +53,15 @@ export async function runCli(args = process.argv.slice(2)) {
53
53
  const schemaPath = args[generateIndex + 1];
54
54
  const outputPath = args[generateIndex + 2];
55
55
 
56
- if (generateIndex !== 1 || schemaPath == null || outputPath == null || args.length !== 4) {
57
- throw new Error('Используйте: deep-json-server <database.json> --generate <database-schema.json> <openapi-schema.yaml>');
56
+ if (generateIndex !== 1 || schemaPath == null || outputPath == null) {
57
+ throw new Error('Используйте: deep-json-server <database.json> --generate <database-schema.json> <openapi-schema.yaml> [--host <host>] [--port <port>]');
58
58
  }
59
59
 
60
- await generateOpenApi({ databasePath, outputPath, schemaPath });
60
+ const options = parseServerOptions([databasePath, ...args.slice(4)]);
61
+ const host = options.host ?? process.env.HOST ?? '127.0.0.1';
62
+ const port = Number(options.port ?? process.env.PORT ?? 4001);
63
+
64
+ await generateOpenApi({ databasePath, host, outputPath, port, schemaPath });
61
65
  process.stdout.write(`OpenAPI-схема сохранена в ${outputPath}\n`);
62
66
  return;
63
67
  }
package/src/openapi.js CHANGED
@@ -13,6 +13,22 @@ const readJson = async(path, label) => {
13
13
  return value;
14
14
  };
15
15
 
16
+ const getServerUrl = (host, port) => {
17
+ const serverPort = Number(port);
18
+
19
+ if (typeof host !== 'string' || host === '') {
20
+ throw new Error('Адрес сервера не должен быть пустым');
21
+ }
22
+
23
+ if (!Number.isInteger(serverPort) || serverPort < 1 || serverPort > 65_535) {
24
+ throw new Error('Порт должен быть целым числом от 1 до 65535');
25
+ }
26
+
27
+ const serverHost = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
28
+
29
+ return `http://${serverHost}:${serverPort}`;
30
+ };
31
+
16
32
  const mergeSchemas = (schemas) => {
17
33
  const uniqueSchemas = [...new Map(schemas.map((schema) => [JSON.stringify(schema), schema])).values()];
18
34
  const nullable = uniqueSchemas.some((schema) => schema.type === 'null');
@@ -69,7 +85,7 @@ function inferObjectSchema(values, path, options) {
69
85
  const required = keys.filter((key) => {
70
86
  const fieldPath = path === '' ? key : `${path}.${key}`;
71
87
 
72
- return !options.optional.has(fieldPath) && values.every((value) => Object.hasOwn(value, key));
88
+ return path === '' && key === 'id' || options.required.has(fieldPath);
73
89
  });
74
90
 
75
91
  return { properties, type: 'object', ...(required.length > 0 && { required }) };
@@ -194,14 +210,15 @@ const createResourcePaths = (resource, componentName) => {
194
210
  };
195
211
  };
196
212
 
197
- export function createOpenApiDocument(database, schemaConfig = {}) {
213
+ export function createOpenApiDocument(database, schemaConfig = {}, { host = '127.0.0.1', port = 4001 } = {}) {
198
214
  if (!isObject(database) || !isObject(schemaConfig)) {
199
215
  throw new Error('База данных и её схема должны содержать JSON-объекты');
200
216
  }
201
217
 
202
218
  const resources = getResourceNames(database);
219
+ const resourceConfigs = isObject(schemaConfig.$schema) ? schemaConfig.$schema : {};
203
220
  const componentNames = Object.fromEntries(resources.map((resource) => {
204
- const resourceConfig = isObject(schemaConfig[resource]) ? schemaConfig[resource] : {};
221
+ const resourceConfig = isObject(resourceConfigs[resource]) ? resourceConfigs[resource] : {};
205
222
  const componentName = typeof resourceConfig.name === 'string' && resourceConfig.name !== '' ? resourceConfig.name : toPascalCase(singularize(resource));
206
223
 
207
224
  return [resource, componentName];
@@ -212,10 +229,10 @@ export function createOpenApiDocument(database, schemaConfig = {}) {
212
229
 
213
230
  resources.forEach((resource) => {
214
231
  const componentName = componentNames[resource];
215
- const resourceConfig = isObject(schemaConfig[resource]) ? schemaConfig[resource] : {};
232
+ const resourceConfig = isObject(resourceConfigs[resource]) ? resourceConfigs[resource] : {};
216
233
  const options = {
217
234
  formats: isObject(resourceConfig.formats) ? resourceConfig.formats : {},
218
- optional: new Set(Array.isArray(resourceConfig.optional) ? resourceConfig.optional : []),
235
+ required: new Set(Array.isArray(resourceConfig.required) ? resourceConfig.required : []),
219
236
  };
220
237
  const values = database[resource].filter(isObject);
221
238
  const rawSchema = values.length === 0 ? { additionalProperties: true, type: 'object' } : inferObjectSchema(values, '', options);
@@ -244,15 +261,15 @@ export function createOpenApiDocument(database, schemaConfig = {}) {
244
261
  info: isObject(schemaConfig.$info) ? schemaConfig.$info : { title: 'Deep JSON Server API', version: '1.0.0' },
245
262
  openapi: '3.0.3',
246
263
  paths: Object.assign({}, ...resources.map((resource) => createResourcePaths(resource, componentNames[resource]))),
247
- servers: Array.isArray(schemaConfig.$servers) ? schemaConfig.$servers : [{ url: 'http://127.0.0.1:4001' }],
264
+ servers: [{ url: getServerUrl(host, port) }],
248
265
  tags: resources.map((resource) => ({ name: resource })),
249
266
  };
250
267
  }
251
268
 
252
- export async function generateOpenApi({ databasePath, outputPath, schemaPath }) {
269
+ export async function generateOpenApi({ databasePath, host, outputPath, port, schemaPath }) {
253
270
  const database = await readJson(databasePath, 'База данных');
254
271
  const schemaConfig = await readJson(schemaPath, 'Схема базы данных');
255
- const document = createOpenApiDocument(database, schemaConfig);
272
+ const document = createOpenApiDocument(database, schemaConfig, { host, port });
256
273
  const resolvedOutputPath = resolve(outputPath);
257
274
 
258
275
  await mkdir(dirname(resolvedOutputPath), { recursive: true });