oak-domain 5.1.21 → 5.1.23

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.
@@ -285,7 +285,7 @@ function outputContext(depGraph, sourceFile, printer, filename) {
285
285
  const { statements } = sourceFile;
286
286
  const isBackend = filename.includes('BackendRuntimeContext');
287
287
  const statements2 = [
288
- factory.createImportDeclaration(undefined, factory.createImportClause(false, factory.createIdentifier(isBackend ? "BaseBackendRuntimeContext" : "BaseFrontendRuntimeContext"), undefined), factory.createStringLiteral(`${root}/${isBackend ? 'lib' : 'es'}/context/${isBackend ? 'BackendRuntimeContext' : 'FrontendRuntimeContext'}`), undefined),
288
+ factory.createImportDeclaration(undefined, factory.createImportClause(false, factory.createIdentifier(isBackend ? "BaseBackendRuntimeContext" : "BaseFrontendRuntimeContext"), undefined), factory.createStringLiteral(`@${root}/context/${isBackend ? 'BackendRuntimeContext' : 'FrontendRuntimeContext'}`), undefined),
289
289
  ...statements
290
290
  ];
291
291
  const result = printer.printList(ts.ListFormat.SourceFileStatements, factory.createNodeArray(statements2), sourceFile);
@@ -745,27 +745,34 @@ function injectDataIndexFile(dataIndexFile, briefNames, printer) {
745
745
  console.log(`注入${dataIndexFile}文件成功,共注入了${briefNames.length}个初始化数据引用`);
746
746
  }
747
747
  /**
748
- * 尝试将pages目录下的页面移到项目目录中。
749
- * 目前简化处理,假设目录结构都是pages/namespace/entity结构,以entity目录作为单元,如果有就放弃,没有就移植
750
- * @param cwdPageDir
751
- * @param modulePageDir
748
+ * 将依赖项目的目录去覆盖原来的目录
749
+ * @param fromDir 依赖项目的目录
750
+ * @param toDir 当前项目的目录
752
751
  */
753
- function tryCopyPages(cwdPageDir, modulePageDir) {
754
- // 各个namespace处理
755
- const nss = (0, fs_1.readdirSync)(modulePageDir);
756
- nss.forEach((namespace) => {
757
- const pages = (0, fs_1.readdirSync)(join(modulePageDir, namespace));
758
- pages.forEach((page) => {
759
- const destDir = join(cwdPageDir, namespace, page);
760
- if (!(0, fs_1.existsSync)(destDir)) {
761
- (0, fs_extra_1.mkdirSync)(destDir);
762
- const srcDir = join(modulePageDir, namespace, page);
763
- console.log(`拷贝${srcDir}到${destDir}下`);
764
- (0, fs_extra_1.copySync)(srcDir, destDir, {
765
- recursive: true,
766
- });
752
+ function tryCopyFilesRecursively(fromDir, toDir) {
753
+ const files = (0, fs_1.readdirSync)(fromDir);
754
+ files.forEach((file) => {
755
+ const fromFile = join(fromDir, file);
756
+ const toFile = join(toDir, file);
757
+ const stat = (0, fs_1.statSync)(fromFile);
758
+ if (stat.isFile()) {
759
+ if ((0, fs_1.existsSync)(join(toDir, file))) {
760
+ console.log(`覆盖文件${toFile}`);
767
761
  }
768
- });
762
+ else {
763
+ console.log(`拷贝文件${toFile}`);
764
+ }
765
+ (0, fs_extra_1.copySync)(fromFile, toFile, {
766
+ overwrite: true,
767
+ });
768
+ }
769
+ else {
770
+ if (!(0, fs_1.existsSync)(toFile)) {
771
+ console.log(`创建文件夹${toFile}`);
772
+ (0, fs_extra_1.mkdirSync)(toFile);
773
+ }
774
+ tryCopyFilesRecursively(fromFile, toFile);
775
+ }
769
776
  });
770
777
  }
771
778
  /**
@@ -792,10 +799,10 @@ function tryCopyModuleTemplateFiles(cwd, dependencies, briefNames, printer) {
792
799
  injectDataIndexFileBriefNames.push(briefNames[idx]);
793
800
  }
794
801
  }
795
- // pages中设计的页面,拷贝到pages对应的目录下,考虑namespace
796
- const pageDir = join(moduleTemplateDir, 'pages');
797
- if ((0, fs_1.existsSync)(pageDir)) {
798
- tryCopyPages(join(cwd, 'src', 'pages'), pageDir);
802
+ // src下面的文件是可以拷贝到项目中的
803
+ const srcDir = join(moduleTemplateDir, 'src');
804
+ if ((0, fs_1.existsSync)(srcDir)) {
805
+ tryCopyFilesRecursively(srcDir, join(cwd, 'src'));
799
806
  }
800
807
  }
801
808
  });
@@ -1,3 +1,22 @@
1
+ import * as ts from 'typescript';
2
+ declare const Schema: Record<string, {
3
+ schemaAttrs: Array<ts.PropertySignature>;
4
+ fulltextIndex?: true;
5
+ indexes?: ts.ArrayLiteralExpression;
6
+ sourceFile: ts.SourceFile;
7
+ enumAttributes: Record<string, string[]>;
8
+ locale: ts.ObjectLiteralExpression;
9
+ toModi: boolean;
10
+ toLog: boolean;
11
+ actionType: string;
12
+ static: boolean;
13
+ inModi: boolean;
14
+ relations: false | string[];
15
+ extendsFrom: string[];
16
+ importAttrFrom: Record<string, [string, string | undefined]>;
17
+ }>;
18
+ export declare function constructAttributes(entity: string): ts.PropertyAssignment[];
19
+ export declare function translateLocaleObject(locale: ts.ObjectLiteralExpression): Record<string, any>;
1
20
  /**
2
21
  * 此函数不再使用
3
22
  * @param map
@@ -23,6 +42,9 @@ export declare function registerFixedDestinationPathMap(map: Record<string, stri
23
42
  * @param map
24
43
  */
25
44
  export declare function registerDeducedRelationMap(map: Record<string, string>): void;
45
+ export declare const getAnalizedSchema: () => typeof Schema;
26
46
  export declare function analyzeEntities(inputDir: string, relativePath?: string): void;
27
47
  export declare function buildSchemaBackup(outputDir: string): void;
48
+ export declare function getProjectionKeys(entity: string): string[];
28
49
  export declare function buildSchema(outputDir: string): void;
50
+ export {};
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildSchema = exports.buildSchemaBackup = exports.analyzeEntities = exports.registerDeducedRelationMap = exports.registerFixedDestinationPathMap = exports.registerIgnoredRelationPathMap = exports.registerFreeEntities = exports.registerIgnoredForeignKeyMap = void 0;
3
+ exports.buildSchema = exports.getProjectionKeys = exports.buildSchemaBackup = exports.analyzeEntities = exports.getAnalizedSchema = exports.registerDeducedRelationMap = exports.registerFixedDestinationPathMap = exports.registerIgnoredRelationPathMap = exports.registerFreeEntities = exports.registerIgnoredForeignKeyMap = exports.translateLocaleObject = exports.constructAttributes = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const path_1 = tslib_1.__importDefault(require("path"));
6
6
  const assert_1 = tslib_1.__importDefault(require("assert"));
@@ -2256,6 +2256,64 @@ function _constructOpProjection(statements, entity) {
2256
2256
  exprNode,
2257
2257
  ])));
2258
2258
  }
2259
+ function getOpProjectionKeys(entity) {
2260
+ const { schemaAttrs, enumAttributes } = Schema[entity];
2261
+ const { [entity]: manyToOneSet } = ManyToOne;
2262
+ const result = [
2263
+ 'id',
2264
+ '$$createAt$$',
2265
+ '$$updateAt$$',
2266
+ '$$seq$$',
2267
+ ];
2268
+ for (const attr of schemaAttrs) {
2269
+ const { type, name } = attr;
2270
+ const attrName = name.text;
2271
+ if (ts.isTypeReferenceNode(type)) {
2272
+ const { typeName } = type;
2273
+ if (ts.isIdentifier(typeName)) {
2274
+ const typeStr = typeName.text;
2275
+ switch (typeStr) {
2276
+ case 'String':
2277
+ case 'Text':
2278
+ case 'Int':
2279
+ case 'Uint':
2280
+ case 'Float':
2281
+ case 'Double':
2282
+ case 'Boolean':
2283
+ case 'Datetime':
2284
+ case 'Image':
2285
+ case 'File':
2286
+ case 'SingleGeo':
2287
+ case 'Geo':
2288
+ case 'Price':
2289
+ case 'Decimal':
2290
+ result.push(attrName);
2291
+ break;
2292
+ case 'Object':
2293
+ result.push(attrName);
2294
+ break;
2295
+ default: {
2296
+ const refEntity = typeStr === 'Schema' ? entity : typeStr;
2297
+ const isManyToOne = manyToOneSet?.some(([e]) => e === refEntity);
2298
+ if (isManyToOne) {
2299
+ result.push(`${attrName}Id`);
2300
+ }
2301
+ else if (!enumAttributes?.[attrName]) {
2302
+ result.push(attrName);
2303
+ }
2304
+ else {
2305
+ result.push(attrName);
2306
+ }
2307
+ }
2308
+ }
2309
+ }
2310
+ }
2311
+ else {
2312
+ result.push(attrName);
2313
+ }
2314
+ }
2315
+ return result;
2316
+ }
2259
2317
  /**
2260
2318
  * 构造Query
2261
2319
  * @param statements
@@ -3951,6 +4009,26 @@ function constructAttributes(entity) {
3951
4009
  });
3952
4010
  return result;
3953
4011
  }
4012
+ exports.constructAttributes = constructAttributes;
4013
+ function translateLocaleObject(locale) {
4014
+ const result = {};
4015
+ locale.properties.forEach((ele) => {
4016
+ (0, assert_1.default)(ts.isPropertyAssignment(ele) && (ts.isIdentifier(ele.name) || ts.isStringLiteral(ele.name)), `locale对象中的属性定义不正确`);
4017
+ const name = ele.name.text;
4018
+ if (ts.isStringLiteral(ele.initializer)) {
4019
+ result[name] = ele.initializer.text;
4020
+ }
4021
+ else if (ts.isObjectLiteralExpression(ele.initializer)) {
4022
+ const subObj = translateLocaleObject(ele.initializer);
4023
+ result[name] = subObj;
4024
+ }
4025
+ else {
4026
+ throw new Error(`locale对象中的属性${name}的定义不正确`);
4027
+ }
4028
+ });
4029
+ return result;
4030
+ }
4031
+ exports.translateLocaleObject = translateLocaleObject;
3954
4032
  function outputLocale(outputDir, printer) {
3955
4033
  const locales = {};
3956
4034
  const entities = [];
@@ -4768,6 +4846,10 @@ function outputStyleDict(outputDir, printer) {
4768
4846
  const filename = path_1.default.join(outputDir, 'StyleDict.ts');
4769
4847
  (0, fs_1.writeFileSync)(filename, result, { flag: 'w' });
4770
4848
  }
4849
+ const getAnalizedSchema = () => {
4850
+ return Schema;
4851
+ };
4852
+ exports.getAnalizedSchema = getAnalizedSchema;
4771
4853
  function analyzeEntities(inputDir, relativePath) {
4772
4854
  const files = (0, fs_1.readdirSync)(inputDir);
4773
4855
  const fullFilenames = files.map(ele => {
@@ -5474,6 +5556,76 @@ function _outputEntityDict(outputDir, printer) {
5474
5556
  const fileName = path_1.default.join(outputDir, 'EntityDict.ts');
5475
5557
  (0, fs_1.writeFileSync)(fileName, result, { flag: 'w' });
5476
5558
  }
5559
+ function getProjectionKeys(entity) {
5560
+ const keys = [];
5561
+ const { schemaAttrs } = Schema[entity];
5562
+ const { [entity]: manyToOneSet = [] } = ManyToOne;
5563
+ for (const attr of schemaAttrs) {
5564
+ const { type, name } = attr;
5565
+ const attrName = name.text;
5566
+ if (ts.isTypeReferenceNode(type)) {
5567
+ const typeName = type.typeName;
5568
+ if (ts.isIdentifier(typeName)) {
5569
+ const text = typeName.text;
5570
+ switch (text) {
5571
+ case 'String':
5572
+ case 'Text':
5573
+ case 'Int':
5574
+ case 'Uint':
5575
+ case 'Float':
5576
+ case 'Double':
5577
+ case 'Boolean':
5578
+ case 'Datetime':
5579
+ case 'Image':
5580
+ case 'File':
5581
+ case 'SingleGeo':
5582
+ case 'Geo':
5583
+ case 'Price':
5584
+ case 'Decimal':
5585
+ case 'Object':
5586
+ break;
5587
+ default:
5588
+ const text2 = text === 'Schema' ? entity : text;
5589
+ const manyToOneItem = manyToOneSet.find(([refEntity]) => refEntity === text2);
5590
+ if (manyToOneItem) {
5591
+ keys.push(attrName); // 外键属性
5592
+ }
5593
+ }
5594
+ }
5595
+ }
5596
+ }
5597
+ if (ReversePointerRelations[entity]) {
5598
+ for (const one of ReversePointerRelations[entity]) {
5599
+ const text2 = one === 'Schema' ? entity : one;
5600
+ keys.push((0, string_1.firstLetterLowerCase)(one));
5601
+ }
5602
+ }
5603
+ const { [entity]: oneToManySet = [] } = OneToMany;
5604
+ const foreignKeySet = {};
5605
+ for (const [entityName, foreignKey] of oneToManySet) {
5606
+ if (!foreignKeySet[entityName]) {
5607
+ foreignKeySet[entityName] = [];
5608
+ }
5609
+ foreignKeySet[entityName].push(foreignKey);
5610
+ }
5611
+ for (const entityName in foreignKeySet) {
5612
+ const entityNameLc = (0, string_1.firstLetterLowerCase)(entityName);
5613
+ for (const foreignKey of foreignKeySet[entityName]) {
5614
+ const identifier = `${entityNameLc}$${foreignKey}`;
5615
+ keys.push(identifier);
5616
+ const aggrKey = _getAggrKey(entityNameLc, foreignKey);
5617
+ if (typeof aggrKey === 'string') {
5618
+ keys.push(aggrKey);
5619
+ }
5620
+ else {
5621
+ // 如果是 union 类型,用映射表达式模拟(如 ["xxx$$aggr", `xxx$$${number}$$aggr`])
5622
+ keys.push(`${identifier}$$aggr`);
5623
+ }
5624
+ }
5625
+ }
5626
+ return [...new Set([...keys, ...getOpProjectionKeys(entity)])];
5627
+ }
5628
+ exports.getProjectionKeys = getProjectionKeys;
5477
5629
  function _outputSchema(outputDir, printer) {
5478
5630
  for (const entity in Schema) {
5479
5631
  const statements = [
@@ -1045,6 +1045,11 @@ class RelationAuth {
1045
1045
  if (actionAuths && actionAuths.length > 0) {
1046
1046
  return checkChildren(actionAuths);
1047
1047
  }
1048
+ // 如果这个entity是updateFree,直接过掉到子结点判定
1049
+ const { action, entity } = node;
1050
+ if (this.updateFreeDict[entity] && this.updateFreeDict[entity].includes(action)) {
1051
+ return checkChildren([]);
1052
+ }
1048
1053
  // 没有能根据父亲传下来的actionAuth判定,只能自己找
1049
1054
  const result = this.findActionAuthsOnNode(node, context);
1050
1055
  const checkResult = (result2) => {
@@ -1083,9 +1088,6 @@ class RelationAuth {
1083
1088
  }
1084
1089
  checkOperation(entity, operation, context) {
1085
1090
  const { action, filter, data } = operation;
1086
- if (this.updateFreeDict[entity] && this.updateFreeDict[entity].includes(action)) {
1087
- return true;
1088
- }
1089
1091
  const userId = context.getCurrentUserId();
1090
1092
  if (!userId) {
1091
1093
  throw new types_1.OakUnloggedInException();
@@ -3,6 +3,9 @@ import { EntityDict as BaseEntityDict } from "../base-app-domain";
3
3
  import { AttrUpdateMatrix } from './EntityDesc';
4
4
  import { ActionDefDict } from './Action';
5
5
  import { StyleDict } from './Style';
6
+ import type { IKoaBodyOptions } from 'koa-body';
7
+ import Koa from 'koa';
8
+ import KoaRouter from 'koa-router';
6
9
  /**
7
10
  * redis连接信息,如果是Redis集群,可以配置多个
8
11
  */
@@ -43,17 +46,9 @@ export type ServerConfiguration = {
43
46
  methods?: string[];
44
47
  };
45
48
  internalExceptionMask?: string;
46
- koaBody?: {
47
- multipart?: boolean;
48
- formidable?: {
49
- maxFileSize?: number;
50
- maxFields?: number;
51
- maxFieldsSize?: number;
52
- uploadDir?: string;
53
- keepExtensions?: boolean;
54
- hashAlgorithm?: string;
55
- multiples?: boolean;
56
- };
49
+ koaBody?: IKoaBodyOptions;
50
+ socket?: (ctx: Koa.ParameterizedContext<any, KoaRouter.IRouterParamContext<any, {}>, any>) => {
51
+ url?: string;
57
52
  };
58
53
  };
59
54
  /**
@@ -3,10 +3,28 @@ import { IncomingHttpHeaders, IncomingMessage } from "http";
3
3
  import { AsyncContext } from "../store/AsyncRowStore";
4
4
  import { EntityDict } from "./Entity";
5
5
  import { EntityDict as BaseEntityDict } from '../base-app-domain';
6
- export interface EndpointItem<ED extends EntityDict & BaseEntityDict, BackCxt extends AsyncContext<ED>> {
6
+ export type EndpointItem<ED extends EntityDict & BaseEntityDict, BackCxt extends AsyncContext<ED>> = SimpleEndpoint<ED, BackCxt> | FreeEndpoint<ED, BackCxt>;
7
+ export interface SimpleEndpoint<ED extends EntityDict & BaseEntityDict, BackCxt extends AsyncContext<ED>> {
7
8
  name: string;
8
9
  params?: string[];
9
10
  method: 'get' | 'post' | 'put' | 'delete';
11
+ type?: "simple";
10
12
  fn: (context: BackCxt, params: Record<string, string>, headers: IncomingHttpHeaders, req: IncomingMessage, body?: any) => Promise<any>;
11
13
  }
14
+ /**
15
+ * 此类型的接口可能会保持长连接,逐步返回内容,所以直接控制res和req即可
16
+ * backendcontext也不能直接传入,在需要时调用方法开启事务
17
+ * 返回时可以指定contentType
18
+ */
19
+ export interface FreeEndpoint<ED extends EntityDict & BaseEntityDict, BackCxt extends AsyncContext<ED>> {
20
+ name: string;
21
+ params?: string[];
22
+ method: 'get' | 'post' | 'put' | 'delete';
23
+ type: "free";
24
+ fn: (contextBuilder: () => Promise<BackCxt>, params: Record<string, string>, headers: IncomingHttpHeaders, req: IncomingMessage, body?: any) => Promise<{
25
+ headers?: Record<string, string | string[]>;
26
+ statusCode?: number;
27
+ data: any;
28
+ }>;
29
+ }
12
30
  export type Endpoint<ED extends EntityDict & BaseEntityDict, BackCxt extends AsyncContext<ED>> = EndpointItem<ED, BackCxt> | EndpointItem<ED, BackCxt>[];
@@ -1,3 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  ;
4
+ ;
@@ -12,6 +12,7 @@ export declare class OakException<ED extends EntityDict & BaseEntityDict> extend
12
12
  name: string;
13
13
  message: string;
14
14
  _module: string | undefined;
15
+ params: Record<string, any> | undefined;
15
16
  opRecords: OpRecord<ED>[];
16
17
  tag1: string | undefined;
17
18
  tag2: boolean | undefined;
@@ -80,6 +80,7 @@ class OakException extends Error {
80
80
  name: this.constructor.name,
81
81
  message: this.message,
82
82
  _module: this._module,
83
+ params: this.params,
83
84
  opRecords: this.opRecords,
84
85
  tag1: this.tag1,
85
86
  tag2: this.tag2,
@@ -36,7 +36,7 @@ export default class SimpleConnector<ED extends EntityDict & BaseEntityDict, Fro
36
36
  opRecords: any;
37
37
  message: string | null;
38
38
  } | {
39
- result: ArrayBuffer;
39
+ result: ReadableStream<Uint8Array> | null;
40
40
  message: string | null;
41
41
  opRecords?: undefined;
42
42
  }>;
@@ -45,7 +45,7 @@ export default class SimpleConnector<ED extends EntityDict & BaseEntityDict, Fro
45
45
  opRecords: any;
46
46
  message: string | null;
47
47
  } | {
48
- result: ArrayBuffer;
48
+ result: ReadableStream<Uint8Array> | null;
49
49
  message: string | null;
50
50
  opRecords?: undefined;
51
51
  }>;
@@ -81,8 +81,7 @@ class SimpleConnector {
81
81
  throw new types_1.OakServerProxyException(`网络请求返回status是${response.status}`);
82
82
  }
83
83
  const message = response.headers.get('oak-message');
84
- const responseType = response.headers.get('Content-Type') ||
85
- response.headers.get('content-type');
84
+ const responseType = response.headers.get('Content-Type') || response.headers.get('content-type');
86
85
  if (responseType?.toLocaleLowerCase().match(/application\/json/i)) {
87
86
  const { exception, result, opRecords } = await response.json();
88
87
  if (exception) {
@@ -94,18 +93,24 @@ class SimpleConnector {
94
93
  message,
95
94
  };
96
95
  }
97
- else if (responseType
98
- ?.toLocaleLowerCase()
99
- .match(/application\/octet-stream/i)) {
100
- const result = await response.arrayBuffer();
96
+ // else if (
97
+ // responseType
98
+ // ?.toLocaleLowerCase()
99
+ // .match(/application\/octet-stream/i)
100
+ // ) {
101
+ // const result = await response.arrayBuffer();
102
+ // return {
103
+ // result,
104
+ // message,
105
+ // };
106
+ // }
107
+ else {
108
+ const result = response.body;
101
109
  return {
102
110
  result,
103
111
  message,
104
112
  };
105
113
  }
106
- else {
107
- throw new Error(`尚不支持的content-type类型${responseType}`);
108
- }
109
114
  }
110
115
  async callAspect(name, params, context) {
111
116
  const { headers, body } = await this.makeHeadersAndBody(name, params, context);
@@ -153,8 +158,7 @@ class SimpleConnector {
153
158
  throw new types_1.OakServerProxyException(`网络请求返回status是${response.status}`);
154
159
  }
155
160
  const message = response.headers.get('oak-message');
156
- const responseType = response.headers.get('Content-Type') ||
157
- response.headers.get('content-type');
161
+ const responseType = response.headers.get('Content-Type') || response.headers.get('content-type');
158
162
  if (responseType?.toLocaleLowerCase().match(/application\/json/i)) {
159
163
  const { socketUrl, subscribeUrl, path } = await response.json();
160
164
  return {
@@ -185,7 +189,7 @@ class SimpleConnector {
185
189
  };
186
190
  }
187
191
  async serializeResult(result, opRecords, headers, body, message) {
188
- if (result instanceof stream_1.Stream || result instanceof Buffer) {
192
+ if (result instanceof stream_1.Stream || result instanceof Buffer || result instanceof ReadableStream) {
189
193
  return {
190
194
  body: result,
191
195
  headers: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oak-domain",
3
- "version": "5.1.21",
3
+ "version": "5.1.23",
4
4
  "author": {
5
5
  "name": "XuChang"
6
6
  },
@@ -23,6 +23,8 @@
23
23
  "@babel/preset-typescript": "^7.12.13",
24
24
  "@types/assert": "^1.5.6",
25
25
  "@types/cross-spawn": "^6.0.2",
26
+ "@types/koa": "^2.15.0",
27
+ "@types/koa-router": "^7.4.8",
26
28
  "@types/fs-extra": "^9.0.13",
27
29
  "@types/lodash": "^4.14.182",
28
30
  "@types/mocha": "^8.2.0",
@@ -45,6 +47,8 @@
45
47
  },
46
48
  "dependencies": {
47
49
  "dayjs": "^1.11.9",
50
+ "koa": "^2.16.1",
51
+ "koa-body": "^5.0.0",
48
52
  "node-schedule": "^2.1.1",
49
53
  "socket.io": "^4.8.1",
50
54
  "uuid": "^9.0.0",