@kollors/deep-json-server 0.2.5 → 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
@@ -109,7 +109,7 @@ DELETE /movies/:id
109
109
  ## Pagination and sorting
110
110
 
111
111
  ```http
112
- GET /movies?_page=1&_per_page=10&_sort=-id,title
112
+ GET /movies?_page=1&_perPage=10&_sort=-id,title
113
113
  ```
114
114
 
115
115
  Without `_page`, a GET collection returns an array. With `_page`, it returns:
@@ -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`.
226
233
 
227
- Use `modelName` when a resource needs an explicit schema name instead of the automatically singularized name:
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`.
235
+
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
- "modelName": "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
@@ -109,7 +109,7 @@ DELETE /movies/:id
109
109
  ## Пагинация и сортировка
110
110
 
111
111
  ```http
112
- GET /movies?_page=1&_per_page=10&_sort=-id,title
112
+ GET /movies?_page=1&_perPage=10&_sort=-id,title
113
113
  ```
114
114
 
115
115
  Без `_page` GET-запрос к коллекции возвращает массив. С `_page` возвращается объект:
@@ -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`.
226
233
 
227
- Используйте `modelName`, если ресурсу нужно явно задать имя схемы вместо автоматически полученного имени в единственном числе:
234
+ `$info` становится объектом `info` в OpenAPI, а настройки ресурсов находятся внутри `$schema`. Поле `servers` в OpenAPI формируется автоматически из параметров `--host` и `--port`, соответствующих переменных окружения `HOST` и `PORT` или адреса по умолчанию `http://127.0.0.1:4001`.
235
+
236
+ Используйте `name`, если ресурсу нужно явно задать имя схемы вместо автоматически полученного имени в единственном числе:
228
237
 
229
238
  ```json
230
239
  {
231
- "equipment": {
232
- "modelName": "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.5",
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 }) };
@@ -134,7 +150,7 @@ const createParameters = () => ({
134
150
  Embed: { description: 'Relationship paths to embed', explode: true, in: 'query', name: '_embed', schema: { items: { type: 'string' }, type: 'array' }, style: 'form' },
135
151
  Id: { in: 'path', name: 'id', required: true, schema: { type: 'string' } },
136
152
  Page: { in: 'query', name: '_page', required: true, schema: { minimum: 1, type: 'integer' } },
137
- PerPage: { in: 'query', name: '_per_page', required: true, schema: { minimum: 1, type: 'integer' } },
153
+ PerPage: { in: 'query', name: '_perPage', required: true, schema: { minimum: 1, type: 'integer' } },
138
154
  Sort: { description: 'Comma-separated fields; prefix with - for descending order', in: 'query', name: '_sort', schema: { type: 'string' } },
139
155
  Where: { description: 'JSON-encoded deep filter', in: 'query', name: '_where', schema: { type: 'string' } },
140
156
  });
@@ -194,15 +210,16 @@ 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] : {};
205
- const componentName = typeof resourceConfig.modelName === 'string' && resourceConfig.modelName !== '' ? resourceConfig.modelName : toPascalCase(singularize(resource));
221
+ const resourceConfig = isObject(resourceConfigs[resource]) ? resourceConfigs[resource] : {};
222
+ const componentName = typeof resourceConfig.name === 'string' && resourceConfig.name !== '' ? resourceConfig.name : toPascalCase(singularize(resource));
206
223
 
207
224
  return [resource, componentName];
208
225
  }));
@@ -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 });
package/src/query.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHttpError, isEqual, isObject, isSafeKey, toArray } from './utils.js';
2
2
 
3
3
  const FIELD_OPERATORS = new Set(['contains', 'endsWith', 'eq', 'every', 'gt', 'gte', 'in', 'lt', 'lte', 'ne', 'none', 'not', 'some', 'startsWith']);
4
- const RESERVED_QUERY_KEYS = new Set(['_embed', '_page', '_per_page', '_sort', '_where']);
4
+ const RESERVED_QUERY_KEYS = new Set(['_embed', '_page', '_perPage', '_sort', '_where']);
5
5
 
6
6
  const compareValues = (left, right) => {
7
7
  if (Object.is(left, right)) {
package/src/server.js CHANGED
@@ -56,7 +56,7 @@ export async function createServer({ databasePath, logger = true } = {}) {
56
56
  const sortedItems = sortItems(filteredItems, request.query._sort);
57
57
  const page = request.query._page == null ? undefined : Number(request.query._page);
58
58
 
59
- return page == null ? sortedItems : paginateItems(sortedItems, page, Number(request.query._per_page));
59
+ return page == null ? sortedItems : paginateItems(sortedItems, page, Number(request.query._perPage));
60
60
  });
61
61
 
62
62
  server.get('/:resource/:id', async(request) => {