@kollors/deep-json-server 0.1.1 → 0.2.1

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/index.js CHANGED
@@ -1,589 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import Fastify from 'fastify';
4
- import { JSONFilePreset } from 'lowdb/node';
5
- import { randomBytes } from 'node:crypto';
6
- import { resolve } from 'node:path';
7
3
  import process from 'node:process';
8
- import { fileURLToPath } from 'node:url';
4
+ import { runCli } from './src/cli.js';
5
+ import { isMainModule } from './src/utils.js';
9
6
 
10
- const FIELD_OPERATORS = new Set(['contains', 'endsWith', 'eq', 'every', 'gt', 'gte', 'in', 'lt', 'lte', 'ne', 'none', 'not', 'some', 'startsWith']);
11
- const RESERVED_QUERY_KEYS = new Set(['_embed', '_page', '_per_page', '_sort', '_where']);
12
- const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
13
- const CORS_HEADERS = {
14
- 'Access-Control-Allow-Headers': 'Content-Type',
15
- 'Access-Control-Allow-Methods': 'DELETE, GET, OPTIONS, PATCH, POST, PUT',
16
- 'Access-Control-Allow-Origin': '*',
17
- };
7
+ export { createOpenApiDocument, generateOpenApi } from './src/openapi.js';
8
+ export { createServer, startServer } from './src/server.js';
18
9
 
19
- const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
20
- const isSafeKey = (key) => !UNSAFE_KEYS.has(key);
21
- const toArray = (value) => (Array.isArray(value) ? value : [value]);
22
-
23
- const createHttpError = (statusCode, message) => {
24
- const error = new Error(message);
25
-
26
- error.statusCode = statusCode;
27
-
28
- return error;
29
- };
30
-
31
- const isEqual = (left, right) => {
32
- if (Object.is(left, right)) {
33
- return true;
34
- }
35
-
36
- if (Array.isArray(left) && Array.isArray(right)) {
37
- return left.length === right.length && left.every((value, index) => isEqual(value, right[index]));
38
- }
39
-
40
- if (isObject(left) && isObject(right)) {
41
- const leftKeys = Object.keys(left);
42
- const rightKeys = Object.keys(right);
43
-
44
- return leftKeys.length === rightKeys.length && leftKeys.every((key) => isSafeKey(key) && Object.hasOwn(right, key) && isEqual(left[key], right[key]));
45
- }
46
-
47
- return false;
48
- };
49
-
50
- const compareValues = (left, right) => {
51
- if (Object.is(left, right)) {
52
- return 0;
53
- }
54
-
55
- if (left == null) {
56
- return 1;
57
- }
58
-
59
- if (right == null) {
60
- return -1;
61
- }
62
-
63
- if (typeof left === 'number' && typeof right === 'number') {
64
- return left - right;
65
- }
66
-
67
- return String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: 'base' });
68
- };
69
-
70
- const getValueByPath = (value, path) => path.split('.').reduce((currentValue, key) => {
71
- return isSafeKey(key) && currentValue != null ? currentValue[key] : undefined;
72
- }, value);
73
-
74
- const matchesOperator = (field, operator, expectedValue) => {
75
- switch (operator) {
76
- case 'contains':
77
- return typeof field === 'string'
78
- ? field.toLowerCase().includes(String(expectedValue).toLowerCase())
79
- : Array.isArray(field) && field.some((value) => isEqual(value, expectedValue));
80
- case 'endsWith':
81
- return typeof field === 'string' && field.toLowerCase().endsWith(String(expectedValue).toLowerCase());
82
- case 'eq':
83
- return isEqual(field, expectedValue);
84
- case 'every':
85
- return Array.isArray(field) && field.every((value) => matchesValue(value, expectedValue));
86
- case 'gt':
87
- return field != null && field > expectedValue;
88
- case 'gte':
89
- return field != null && field >= expectedValue;
90
- case 'in': {
91
- const expectedValues = toArray(expectedValue);
92
-
93
- return Array.isArray(field)
94
- ? field.some((value) => expectedValues.some((expectedItem) => isEqual(value, expectedItem)))
95
- : expectedValues.some((expectedItem) => isEqual(field, expectedItem));
96
- }
97
- case 'lt':
98
- return field != null && field < expectedValue;
99
- case 'lte':
100
- return field != null && field <= expectedValue;
101
- case 'ne':
102
- return !isEqual(field, expectedValue);
103
- case 'none':
104
- return Array.isArray(field) && !field.some((value) => matchesValue(value, expectedValue));
105
- case 'not':
106
- return !matchesValue(field, expectedValue);
107
- case 'some':
108
- return Array.isArray(field) && field.some((value) => matchesValue(value, expectedValue));
109
- case 'startsWith':
110
- return typeof field === 'string' && field.toLowerCase().startsWith(String(expectedValue).toLowerCase());
111
- default:
112
- return false;
113
- }
114
- };
115
-
116
- function matchesValue(field, condition) {
117
- if (!isObject(condition)) {
118
- return isEqual(field, condition);
119
- }
120
-
121
- const conditionEntries = Object.entries(condition);
122
- const operatorEntries = conditionEntries.filter(([operator]) => FIELD_OPERATORS.has(operator));
123
- const nestedEntries = conditionEntries.filter(([key]) => !FIELD_OPERATORS.has(key));
124
-
125
- if (!operatorEntries.every(([operator, expectedValue]) => matchesOperator(field, operator, expectedValue))) {
126
- return false;
127
- }
128
-
129
- if (nestedEntries.length === 0) {
130
- return true;
131
- }
132
-
133
- return isObject(field) && matchesWhere(field, Object.fromEntries(nestedEntries));
134
- }
135
-
136
- function matchesWhere(value, where) {
137
- if (!isObject(value) || !isObject(where)) {
138
- return false;
139
- }
140
-
141
- return Object.entries(where).every(([key, condition]) => {
142
- if (key === 'and') {
143
- return Array.isArray(condition) && condition.every((nestedWhere) => matchesWhere(value, nestedWhere));
144
- }
145
-
146
- if (key === 'or') {
147
- return Array.isArray(condition) && condition.length > 0 && condition.some((nestedWhere) => matchesWhere(value, nestedWhere));
148
- }
149
-
150
- if (key === 'not') {
151
- return isObject(condition) && !matchesWhere(value, condition);
152
- }
153
-
154
- return isSafeKey(key) && matchesValue(value[key], condition);
155
- });
156
- }
157
-
158
- const parsePrimitive = (value) => {
159
- if (value === 'true') {
160
- return true;
161
- }
162
-
163
- if (value === 'false') {
164
- return false;
165
- }
166
-
167
- if (value === 'null') {
168
- return null;
169
- }
170
-
171
- return value;
172
- };
173
-
174
- const parseFilterKey = (key) => {
175
- const colonIndex = key.lastIndexOf(':');
176
-
177
- if (colonIndex !== -1) {
178
- const path = key.slice(0, colonIndex);
179
- const operator = key.slice(colonIndex + 1);
180
-
181
- return FIELD_OPERATORS.has(operator) ? { operator, path } : undefined;
182
- }
183
-
184
- const legacyOperator = key.match(/^(.*)_([a-zA-Z]+)$/);
185
-
186
- if (legacyOperator?.[1] != null && legacyOperator[2] != null && FIELD_OPERATORS.has(legacyOperator[2])) {
187
- return { operator: legacyOperator[2], path: legacyOperator[1] };
188
- }
189
-
190
- return { operator: 'eq', path: key };
191
- };
192
-
193
- const setWhereOperator = (where, path, operator, value) => {
194
- const keys = path.split('.').filter(Boolean);
195
-
196
- if (keys.length === 0 || keys.some((key) => !isSafeKey(key))) {
197
- return;
198
- }
199
-
200
- const fieldKey = keys.pop();
201
- let currentValue = where;
202
-
203
- keys.forEach((key) => {
204
- currentValue[key] = isObject(currentValue[key]) ? currentValue[key] : {};
205
- currentValue = currentValue[key];
206
- });
207
-
208
- currentValue[fieldKey] = isObject(currentValue[fieldKey]) ? currentValue[fieldKey] : {};
209
- currentValue[fieldKey][operator] = operator === 'in' && typeof value === 'string' ? value.split(',').map((item) => parsePrimitive(item.trim())) : parsePrimitive(value);
210
- };
211
-
212
- const parseWhere = (query) => {
213
- const rawWhere = toArray(query._where).at(-1);
214
-
215
- if (rawWhere != null) {
216
- try {
217
- const where = JSON.parse(rawWhere);
218
-
219
- if (!isObject(where)) {
220
- throw new Error();
221
- }
222
-
223
- return where;
224
- } catch {
225
- throw createHttpError(400, 'Параметр _where должен содержать JSON-объект');
226
- }
227
- }
228
-
229
- const where = {};
230
-
231
- Object.entries(query).forEach(([key, rawValue]) => {
232
- if (RESERVED_QUERY_KEYS.has(key)) {
233
- return;
234
- }
235
-
236
- const filterKey = parseFilterKey(key);
237
-
238
- if (filterKey == null) {
239
- return;
240
- }
241
-
242
- toArray(rawValue).forEach((value) => setWhereOperator(where, filterKey.path, filterKey.operator, value));
243
- });
244
-
245
- return where;
246
- };
247
-
248
- const singularize = (value) => {
249
- if (value === 'clothes') {
250
- return value;
251
- }
252
-
253
- if (value.endsWith('ies')) {
254
- return `${value.slice(0, -3)}y`;
255
- }
256
-
257
- return value.endsWith('s') ? value.slice(0, -1) : value;
258
- };
259
-
260
- const getResourceNames = (database) => Object.entries(database.data).filter(([, value]) => Array.isArray(value)).map(([resource]) => resource);
261
-
262
- const resolveResource = (database, relation, sourceResource) => {
263
- const resourceNames = getResourceNames(database);
264
- const resource = resourceNames.find((resourceName) => resourceName === relation) ?? resourceNames.find((resourceName) => singularize(resourceName) === relation);
265
-
266
- if (resource != null) {
267
- return resource;
268
- }
269
-
270
- return ['child', 'children', 'parent', 'parents'].includes(relation) && resourceNames.includes(sourceResource) ? sourceResource : undefined;
271
- };
272
-
273
- const getRelationKeys = (...names) => [...new Set(names.flatMap((name) => [`${name}Id`, `${name}Ids`]))];
274
-
275
- const findLocalRelation = (item, relation, targetResource) => {
276
- const relationKey = getRelationKeys(relation, singularize(relation), targetResource, singularize(targetResource)).find((key) => Object.hasOwn(item, key));
277
-
278
- if (relationKey == null) {
279
- return undefined;
280
- }
281
-
282
- return { ids: toArray(item[relationKey]).filter((id) => id != null), isMany: relationKey.endsWith('Ids') || Array.isArray(item[relationKey]) };
283
- };
284
-
285
- const hasReference = (value, relationKeys, id) => {
286
- if (Array.isArray(value)) {
287
- return value.some((item) => hasReference(item, relationKeys, id));
288
- }
289
-
290
- if (!isObject(value)) {
291
- return false;
292
- }
293
-
294
- return Object.entries(value).some(([key, nestedValue]) => {
295
- if (!isSafeKey(key)) {
296
- return false;
297
- }
298
-
299
- if (relationKeys.includes(key)) {
300
- return toArray(nestedValue).some((nestedId) => isEqual(nestedId, id));
301
- }
302
-
303
- return hasReference(nestedValue, relationKeys, id);
304
- });
305
- };
306
-
307
- const findRelatedValue = (database, item, sourceResource, relation, targetResource) => {
308
- const targetItems = database.data[targetResource];
309
- const localRelation = findLocalRelation(item, relation, targetResource);
310
-
311
- if (!Array.isArray(targetItems)) {
312
- return undefined;
313
- }
314
-
315
- if (localRelation != null) {
316
- const relatedItems = localRelation.ids.map((id) => targetItems.find((targetItem) => isObject(targetItem) && isEqual(targetItem.id, id))).filter((targetItem) => targetItem != null);
317
-
318
- return localRelation.isMany ? relatedItems : relatedItems[0] ?? null;
319
- }
320
-
321
- if (item.id == null) {
322
- return undefined;
323
- }
324
-
325
- const reverseRelationKeys = getRelationKeys(singularize(sourceResource));
326
-
327
- if (relation === 'child' || relation === 'children') {
328
- reverseRelationKeys.push('parentId', 'parentIds');
329
- }
330
-
331
- return targetItems.filter((targetItem) => hasReference(targetItem, reverseRelationKeys, item.id));
332
- };
333
-
334
- const embedPath = (database, item, sourceResource, [relation, ...nestedRelations]) => {
335
- if (relation == null || !isSafeKey(relation)) {
336
- return item;
337
- }
338
-
339
- const currentValue = item[relation];
340
- const nestedSourceResource = resolveResource(database, relation, sourceResource) ?? relation;
341
-
342
- if (Array.isArray(currentValue)) {
343
- return nestedRelations.length === 0 ? item : { ...item, [relation]: currentValue.map((value) => (isObject(value) ? embedPath(database, value, nestedSourceResource, nestedRelations) : value)) };
344
- }
345
-
346
- if (isObject(currentValue)) {
347
- return nestedRelations.length === 0 ? item : { ...item, [relation]: embedPath(database, currentValue, nestedSourceResource, nestedRelations) };
348
- }
349
-
350
- const targetResource = resolveResource(database, relation, sourceResource);
351
-
352
- if (targetResource == null) {
353
- return item;
354
- }
355
-
356
- const relatedValue = findRelatedValue(database, item, sourceResource, relation, targetResource);
357
-
358
- if (relatedValue == null || nestedRelations.length === 0) {
359
- return relatedValue === undefined ? item : { ...item, [relation]: relatedValue };
360
- }
361
-
362
- return {
363
- ...item,
364
- [relation]: Array.isArray(relatedValue)
365
- ? relatedValue.map((value) => embedPath(database, value, targetResource, nestedRelations))
366
- : embedPath(database, relatedValue, targetResource, nestedRelations),
367
- };
368
- };
369
-
370
- const parseEmbedPaths = (embed) => toArray(embed)
371
- .flatMap((value) => (typeof value === 'string' ? value.split(',') : []))
372
- .map((path) => path.split('.').filter(Boolean))
373
- .filter((path) => path.length > 0 && path.every(isSafeKey));
374
-
375
- const embedItem = (database, item, resource, embedPaths) => embedPaths.reduce((embeddedItem, path) => embedPath(database, embeddedItem, resource, path), item);
376
-
377
- const sortItems = (items, sort) => {
378
- const sortRules = typeof sort === 'string' ? sort.split(',').filter(Boolean) : [];
379
-
380
- if (sortRules.length === 0) {
381
- return [...items];
382
- }
383
-
384
- return [...items].sort((left, right) => {
385
- for (const sortRule of sortRules) {
386
- const isDescending = sortRule.startsWith('-');
387
- const path = isDescending ? sortRule.slice(1) : sortRule;
388
- const comparison = compareValues(getValueByPath(left, path), getValueByPath(right, path));
389
-
390
- if (comparison !== 0) {
391
- return isDescending ? -comparison : comparison;
392
- }
393
- }
394
-
395
- return 0;
396
- });
397
- };
398
-
399
- const paginateItems = (items, page, pageSize) => {
400
- const safePageSize = Number.isFinite(pageSize) && pageSize > 0 ? Math.floor(pageSize) : 10;
401
- const pages = Math.max(1, Math.ceil(items.length / safePageSize));
402
- const safePage = Math.max(1, Math.min(Number.isFinite(page) ? Math.floor(page) : 1, pages));
403
- const offset = (safePage - 1) * safePageSize;
404
-
405
- return {
406
- data: items.slice(offset, offset + safePageSize),
407
- first: 1,
408
- items: items.length,
409
- last: pages,
410
- next: safePage < pages ? safePage + 1 : null,
411
- pages,
412
- prev: safePage > 1 ? safePage - 1 : null,
413
- };
414
- };
415
-
416
- const getCollection = (database, resource) => {
417
- const collection = isSafeKey(resource) ? database.data[resource] : undefined;
418
-
419
- if (!Array.isArray(collection)) {
420
- throw createHttpError(404, 'Ресурс не найден');
421
- }
422
-
423
- return collection;
424
- };
425
-
426
- const getRequestBody = (body) => {
427
- if (!isObject(body)) {
428
- throw createHttpError(400, 'Тело запроса должно быть JSON-объектом');
429
- }
430
-
431
- return body;
432
- };
433
-
434
- const findItem = (collection, id) => collection.find((item) => isObject(item) && String(item.id) === id);
435
- const resolveDatabasePath = (databasePath) => {
436
- if (typeof databasePath !== 'string' || databasePath === '') {
437
- throw new Error('Укажите путь к JSON-базе данных');
438
- }
439
-
440
- return resolve(databasePath);
441
- };
442
-
443
- export async function createServer({ databasePath, logger = true } = {}) {
444
- const database = await JSONFilePreset(resolveDatabasePath(databasePath), {});
445
- const server = Fastify({ logger });
446
-
447
- server.addHook('onRequest', async(_request, reply) => {
448
- Object.entries(CORS_HEADERS).forEach(([header, value]) => reply.header(header, value));
449
- });
450
-
451
- server.addHook('preHandler', async() => {
452
- await database.read();
453
- });
454
-
455
- server.options('/', async(_request, reply) => reply.code(204).send());
456
- server.options('/*', async(_request, reply) => reply.code(204).send());
457
-
458
- server.get('/', async() => ({ resources: getResourceNames(database) }));
459
-
460
- server.get('/:resource', async(request) => {
461
- const collection = getCollection(database, request.params.resource);
462
- const where = parseWhere(request.query);
463
- const embedPaths = parseEmbedPaths(request.query._embed);
464
- const embeddedItems = collection.map((item) => embedItem(database, item, request.params.resource, embedPaths));
465
- const filteredItems = embeddedItems.filter((item) => matchesWhere(item, where));
466
- const sortedItems = sortItems(filteredItems, request.query._sort);
467
- const page = request.query._page == null ? undefined : Number(request.query._page);
468
-
469
- return page == null ? sortedItems : paginateItems(sortedItems, page, Number(request.query._per_page));
470
- });
471
-
472
- server.get('/:resource/:id', async(request) => {
473
- const item = findItem(getCollection(database, request.params.resource), request.params.id);
474
-
475
- if (item == null) {
476
- throw createHttpError(404, 'Запись не найдена');
477
- }
478
-
479
- return embedItem(database, item, request.params.resource, parseEmbedPaths(request.query._embed));
480
- });
481
-
482
- server.post('/:resource', async(request, reply) => {
483
- const collection = getCollection(database, request.params.resource);
484
- const item = { ...getRequestBody(request.body), id: randomBytes(8).toString('base64url') };
485
-
486
- collection.push(item);
487
- await database.write();
488
-
489
- return reply.code(201).send(item);
490
- });
491
-
492
- server.put('/:resource/:id', async(request) => {
493
- const collection = getCollection(database, request.params.resource);
494
- const currentItem = findItem(collection, request.params.id);
495
-
496
- if (currentItem == null) {
497
- throw createHttpError(404, 'Запись не найдена');
498
- }
499
-
500
- const item = { ...getRequestBody(request.body), id: request.params.id };
501
- const itemIndex = collection.indexOf(currentItem);
502
-
503
- collection.splice(itemIndex, 1, item);
504
- await database.write();
505
-
506
- return item;
507
- });
508
-
509
- server.patch('/:resource/:id', async(request) => {
510
- const collection = getCollection(database, request.params.resource);
511
- const currentItem = findItem(collection, request.params.id);
512
-
513
- if (currentItem == null) {
514
- throw createHttpError(404, 'Запись не найдена');
515
- }
516
-
517
- const item = { ...currentItem, ...getRequestBody(request.body), id: request.params.id };
518
- const itemIndex = collection.indexOf(currentItem);
519
-
520
- collection.splice(itemIndex, 1, item);
521
- await database.write();
522
-
523
- return item;
524
- });
525
-
526
- server.delete('/:resource/:id', async(request) => {
527
- const collection = getCollection(database, request.params.resource);
528
- const currentItem = findItem(collection, request.params.id);
529
-
530
- if (currentItem == null) {
531
- throw createHttpError(404, 'Запись не найдена');
532
- }
533
-
534
- collection.splice(collection.indexOf(currentItem), 1);
535
- await database.write();
536
-
537
- return currentItem;
538
- });
539
-
540
- server.setErrorHandler((error, request, reply) => {
541
- const statusCode = Number.isInteger(error.statusCode) ? error.statusCode : 500;
542
-
543
- if (statusCode === 500) {
544
- request.log.error(error);
545
- }
546
-
547
- return reply.code(statusCode).send({ error: error.message });
548
- });
549
-
550
- return server;
551
- }
552
-
553
- export async function startServer({ databasePath, host = '127.0.0.1', logger = true, port = 4001 } = {}) {
554
- if (!Number.isInteger(port) || port < 1 || port > 65_535) {
555
- throw new Error('Порт должен быть целым числом от 1 до 65535');
556
- }
557
-
558
- const resolvedDatabasePath = resolveDatabasePath(databasePath);
559
- const server = await createServer({ databasePath: resolvedDatabasePath, logger });
560
-
561
- await server.listen({ host, port });
562
- server.log.info({ database: resolvedDatabasePath }, 'Deep JSON Server запущен');
563
-
564
- return server;
565
- }
566
-
567
- const getCliOption = (argumentsList, ...names) => {
568
- const optionIndex = argumentsList.findIndex((argument) => names.includes(argument));
569
-
570
- return optionIndex === -1 ? undefined : argumentsList[optionIndex + 1];
571
- };
572
-
573
- const runCli = async() => {
574
- const [databaseArgument, ...cliArguments] = process.argv.slice(2);
575
-
576
- if (databaseArgument == null || databaseArgument.startsWith('-')) {
577
- throw new Error('Укажите путь к базе данных: deep-json-server database.json');
578
- }
579
-
580
- await startServer({
581
- databasePath: databaseArgument,
582
- host: getCliOption(cliArguments, '--host', '-h') ?? process.env.HOST ?? '127.0.0.1',
583
- port: Number(getCliOption(cliArguments, '--port', '-p') ?? process.env.PORT ?? 4001),
584
- });
585
- };
586
-
587
- if (process.argv[1] != null && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
10
+ if (isMainModule(process.argv[1], import.meta.url)) {
588
11
  await runCli();
589
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kollors/deep-json-server",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "JSON mock server with deep filters and recursive relationship embedding",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,12 +11,13 @@
11
11
  },
12
12
  "files": [
13
13
  "index.js",
14
+ "src",
14
15
  "LICENSE",
15
16
  "README.md",
16
17
  "README.ru.md"
17
18
  ],
18
19
  "scripts": {
19
- "check": "node --check index.js",
20
+ "check": "node --check index.js && node --check src/*.js",
20
21
  "test": "node --test"
21
22
  },
22
23
  "engines": {
@@ -24,13 +25,14 @@
24
25
  },
25
26
  "dependencies": {
26
27
  "fastify": "^5.12.1",
27
- "lowdb": "^7.0.1"
28
+ "lowdb": "^7.0.1",
29
+ "yaml": "^2.8.1"
28
30
  },
29
31
  "repository": {
30
32
  "type": "git",
31
33
  "url": "git+https://github.com/kollors/deep-json-server.git"
32
34
  },
33
- "homepage": "https://github.com/kollors/deep-json-server#readme",
35
+ "homepage": "https://github.com/kollors/deep-json-server",
34
36
  "bugs": {
35
37
  "url": "https://github.com/kollors/deep-json-server/issues"
36
38
  },
package/src/cli.js ADDED
@@ -0,0 +1,70 @@
1
+ import process from 'node:process';
2
+ import { generateOpenApi } from './openapi.js';
3
+ import { startServer } from './server.js';
4
+
5
+ const HELP = `Deep JSON Server
6
+
7
+ Использование:
8
+ deep-json-server <database.json> [--host <host>] [--port <port>]
9
+ deep-json-server <database.json> --generate <database-schema.json> <openapi-schema.yaml>
10
+
11
+ Параметры:
12
+ --generate Сгенерировать OpenAPI и завершить работу
13
+ --host, -h Адрес сервера (по умолчанию 127.0.0.1)
14
+ --port, -p Порт сервера (по умолчанию 4001)
15
+ --help Показать справку`;
16
+
17
+ const parseServerOptions = (args) => {
18
+ const options = {};
19
+
20
+ for (let index = 1; index < args.length; index += 2) {
21
+ const option = args[index];
22
+ const value = args[index + 1];
23
+
24
+ if (!['--host', '-h', '--port', '-p'].includes(option)) {
25
+ throw new Error(`Неизвестный параметр: ${option}`);
26
+ }
27
+
28
+ if (value == null || value.startsWith('-')) {
29
+ throw new Error(`Не указано значение параметра ${option}`);
30
+ }
31
+
32
+ options[['--host', '-h'].includes(option) ? 'host' : 'port'] = value;
33
+ }
34
+
35
+ return options;
36
+ };
37
+
38
+ export async function runCli(args = process.argv.slice(2)) {
39
+ if (args.includes('--help')) {
40
+ process.stdout.write(`${HELP}\n`);
41
+ return;
42
+ }
43
+
44
+ const [databasePath] = args;
45
+
46
+ if (databasePath == null || databasePath.startsWith('-')) {
47
+ throw new Error('Укажите путь к JSON-базе данных');
48
+ }
49
+
50
+ const generateIndex = args.indexOf('--generate');
51
+
52
+ if (generateIndex !== -1) {
53
+ const schemaPath = args[generateIndex + 1];
54
+ const outputPath = args[generateIndex + 2];
55
+
56
+ if (generateIndex !== 1 || schemaPath == null || outputPath == null || args.length !== 4) {
57
+ throw new Error('Используйте: deep-json-server <database.json> --generate <database-schema.json> <openapi-schema.yaml>');
58
+ }
59
+
60
+ await generateOpenApi({ databasePath, outputPath, schemaPath });
61
+ process.stdout.write(`OpenAPI-схема сохранена в ${outputPath}\n`);
62
+ return;
63
+ }
64
+
65
+ const options = parseServerOptions(args);
66
+ const host = options.host ?? process.env.HOST ?? '127.0.0.1';
67
+ const port = Number(options.port ?? process.env.PORT ?? 4001);
68
+
69
+ await startServer({ databasePath, host, port });
70
+ }