Browse Source

Merge pull request #250 from nameczz/dto

Refine controller and add dto check
nameczz 3 years ago
parent
commit
c8aa1a1efd

+ 1 - 0
client/src/pages/collections/Collections.tsx

@@ -139,6 +139,7 @@ const Collections = () => {
       const hasLoadingOrBuildingCollection = res.some(
         v => checkLoading(v) || checkIndexBuilding(v)
       );
+
       // if some collection is building index or loading, start pulling data
       if (hasLoadingOrBuildingCollection) {
         MilvusHttp.triggerCron({

+ 10 - 3
express/package.json

@@ -6,6 +6,9 @@
   "dependencies": {
     "@zilliz/milvus2-sdk-node": "^1.0.19",
     "chalk": "^4.1.2",
+    "class-sanitizer": "^1.0.1",
+    "class-transformer": "^0.4.0",
+    "class-validator": "^0.13.1",
     "cors": "^2.8.5",
     "cross-env": "^7.0.3",
     "express": "^4.17.1",
@@ -14,16 +17,20 @@
     "morgan": "^1.10.0",
     "node-cron": "^3.0.0",
     "rimraf": "^3.0.2",
-    "socket.io": "^4.3.1"
+    "socket.io": "^4.3.1",
+    "swagger-jsdoc": "^6.1.0",
+    "swagger-ui-express": "^4.1.6"
   },
   "devDependencies": {
+    "@types/swagger-jsdoc": "^6.0.1",
     "@types/chalk": "^2.2.0",
-    "@types/morgan": "^1.9.3",
     "@types/cors": "^2.8.12",
     "@types/express": "^4.17.13",
     "@types/glob": "^7.2.0",
+    "@types/morgan": "^1.9.3",
     "@types/node": "^16.11.6",
     "@types/node-cron": "^3.0.0",
+    "@types/swagger-ui-express": "^4.1.3",
     "@types/ws": "^8.2.0",
     "nodemon": "^2.0.14",
     "ts-node": "^10.4.0",
@@ -33,7 +40,7 @@
   "scripts": {
     "prebuild": "tslint -c tslint.json -p tsconfig.json --fix",
     "build": "yarn clean && tsc",
-    "prestart": "yarn build",
+    "prestart": "rm -rf dist && yarn build",
     "start": "nodemon ./src/app",
     "start:plugin": "yarn build && cross-env PLUGIN_DEV=1 node dist/milvus-insight/express/src/app.js",
     "start:prod": "node dist/app.js",

+ 6 - 0
express/src/app.ts

@@ -18,6 +18,8 @@ import {
 import { getDirectories, getDirectoriesSync, generateCfgs } from "./utils";
 import * as path from "path";
 import chalk from "chalk";
+import { surveSwaggerSpecification } from "./swagger";
+import swaggerUi from "swagger-ui-express";
 
 const PLUGIN_DEV = process.env?.PLUGIN_DEV;
 const SRC_PLUGIN_DIR = "src/plugins";
@@ -112,6 +114,10 @@ getDirectories(SRC_PLUGIN_DIR, async (dirErr: Error, dirRes: string[]) => {
 
   // Return client build files
   app.use(express.static("build"));
+
+  const data = surveSwaggerSpecification();
+  app.use("/api/v1/swagger", swaggerUi.serve, swaggerUi.setup(data));
+
   // handle every other route with index.html, which will contain
   // a script tag to your application's JavaScript file(s).
   app.get("*", (request, response) => {

+ 276 - 0
express/src/collections/collections.controller.ts

@@ -0,0 +1,276 @@
+import { NextFunction, Request, Response, Router } from "express";
+import { dtoValidationMiddleware } from "../middlewares/validation";
+import { milvusService } from "../milvus";
+import { CollectionsService } from "./collections.service";
+import {
+  CreateAliasDto,
+  CreateCollectionDto,
+  InsertDataDto,
+  ShowCollectionsDto,
+  VectorSearchDto,
+  QueryDto,
+} from "./dto";
+
+export class CollectionController {
+  private collectionsService: CollectionsService;
+  private router: Router;
+
+  constructor() {
+    this.collectionsService = new CollectionsService(milvusService);
+
+    this.router = Router();
+  }
+
+  get collectionsServiceGetter() {
+    return this.collectionsService;
+  }
+
+  generateRoutes() {
+    /**
+     * @swagger
+     * /collections:
+     *   get:
+     *     description: Get all or loaded collection
+     *     responses:
+     *       200:
+     *         Collections List
+     */
+    this.router.get(
+      "/",
+      dtoValidationMiddleware(ShowCollectionsDto),
+      this.showCollections.bind(this)
+    );
+
+    this.router.post(
+      "/",
+      dtoValidationMiddleware(CreateCollectionDto),
+      this.createCollection.bind(this)
+    );
+
+    this.router.get("/statistics", this.getStatistics.bind(this));
+
+    this.router.get(
+      "/:name/statistics",
+      this.getCollectionStatistics.bind(this)
+    );
+
+    this.router.get(
+      "/indexes/status",
+      this.getCollectionsIndexStatus.bind(this)
+    );
+
+    this.router.delete("/:name", this.dropCollection.bind(this));
+
+    this.router.get("/:name", this.describeCollection.bind(this));
+
+    this.router.put("/:name/load", this.loadCollection.bind(this));
+
+    this.router.put("/:name/release", this.releaseCollection.bind(this));
+
+    this.router.post(
+      "/:name/insert",
+      dtoValidationMiddleware(InsertDataDto),
+      this.insert.bind(this)
+    );
+
+    this.router.post(
+      "/:name/search",
+      dtoValidationMiddleware(VectorSearchDto),
+      this.vectorSearch.bind(this)
+    );
+
+    this.router.post(
+      "/:name/query",
+      dtoValidationMiddleware(QueryDto),
+      this.query.bind(this)
+    );
+
+    this.router.post(
+      "/:name/alias",
+      dtoValidationMiddleware(CreateAliasDto),
+      this.createAlias.bind(this)
+    );
+
+    return this.router;
+  }
+
+  async showCollections(req: Request, res: Response, next: NextFunction) {
+    const type = parseInt("" + req.query?.type, 10);
+    try {
+      const result =
+        type === 1
+          ? await this.collectionsService.getLoadedColletions()
+          : await this.collectionsService.getAllCollections();
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async getStatistics(req: Request, res: Response, next: NextFunction) {
+    try {
+      const result = await this.collectionsService.getStatistics();
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async createCollection(req: Request, res: Response, next: NextFunction) {
+    const createCollectionData = req.body;
+    try {
+      const result = await this.collectionsService.createCollection(
+        createCollectionData
+      );
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async dropCollection(req: Request, res: Response, next: NextFunction) {
+    const name = req.params?.name;
+    try {
+      const result = await this.collectionsService.dropCollection({
+        collection_name: name,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async describeCollection(req: Request, res: Response, next: NextFunction) {
+    const name = req.params?.name;
+    try {
+      const result = await this.collectionsService.describeCollection({
+        collection_name: name,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async getCollectionStatistics(
+    req: Request,
+    res: Response,
+    next: NextFunction
+  ) {
+    const name = req.params?.name;
+    try {
+      const result = await this.collectionsService.getCollectionStatistics({
+        collection_name: name,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async getCollectionsIndexStatus(
+    req: Request,
+    res: Response,
+    next: NextFunction
+  ) {
+    try {
+      const result = await this.collectionsService.getCollectionsIndexStatus();
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async loadCollection(req: Request, res: Response, next: NextFunction) {
+    const name = req.params?.name;
+    try {
+      const result = await this.collectionsService.loadCollection({
+        collection_name: name,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async releaseCollection(req: Request, res: Response, next: NextFunction) {
+    const name = req.params?.name;
+    try {
+      const result = await this.collectionsService.releaseCollection({
+        collection_name: name,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async insert(req: Request, res: Response, next: NextFunction) {
+    const name = req.params?.name;
+    const data = req.body;
+    try {
+      const result = await this.collectionsService.insert({
+        collection_name: name,
+        ...data,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async vectorSearch(req: Request, res: Response, next: NextFunction) {
+    const name = req.params?.name;
+    const data = req.body;
+    try {
+      const result = await this.collectionsService.vectorSearch({
+        collection_name: name,
+        ...data,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async query(req: Request, res: Response, next: NextFunction) {
+    const name = req.params?.name;
+    const data = req.body;
+    const resultiLmit: any = req.query?.limit;
+    const resultPage: any = req.query?.page;
+
+    try {
+      const limit = isNaN(resultiLmit) ? 100 : parseInt(resultiLmit, 10);
+      const page = isNaN(resultPage) ? 0 : parseInt(resultPage, 10);
+      // TODO: add page and limit to node SDK
+      // Here may raise "Error: 8 RESOURCE_EXHAUSTED: Received message larger than max"
+      const result = await this.collectionsService.query({
+        collection_name: name,
+        ...data,
+      });
+      const queryResultList = result.data;
+      const queryResultLength = result.data.length;
+      const startNum = page * limit;
+      const endNum = (page + 1) * limit;
+      const slicedResult = queryResultList.slice(startNum, endNum);
+      result.data = slicedResult;
+      res.send({ ...result, limit, page, total: queryResultLength });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async createAlias(req: Request, res: Response, next: NextFunction) {
+    const name = req.params?.name;
+    const data = req.body;
+    try {
+      const result = await this.collectionsService.createAlias({
+        collection_name: name,
+        ...data,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+}

+ 6 - 6
express/src/collections/collections.service.ts

@@ -20,6 +20,7 @@ import {
   ShowCollectionsReq,
   ShowCollectionsType,
 } from "@zilliz/milvus2-sdk-node/dist/milvus/types/Collection";
+import { QueryDto } from "./dto";
 
 export class CollectionsService {
   constructor(private milvusService: MilvusService) {}
@@ -108,12 +109,11 @@ export class CollectionsService {
     return res;
   }
 
-  async query(data: {
-    collection_name: string;
-    expr: string;
-    partitions_names?: string[];
-    output_fields?: string[];
-  }) {
+  async query(
+    data: {
+      collection_name: string;
+    } & QueryDto
+  ) {
     const res = await this.dataManager.query(data);
     throwErrorFromSDK(res.status);
     return res;

+ 90 - 0
express/src/collections/dto.ts

@@ -0,0 +1,90 @@
+import {
+  IsNotEmpty,
+  IsString,
+  IsBoolean,
+  IsOptional,
+  IsArray,
+  ArrayNotEmpty,
+  IsEnum,
+  ArrayMinSize,
+  IsObject,
+} from "class-validator";
+import {
+  FieldType,
+  ShowCollectionsType,
+} from "@zilliz/milvus2-sdk-node/dist/milvus/types/Collection";
+import { DataType } from "@zilliz/milvus2-sdk-node/dist/milvus/types/Common";
+import { SearchParam } from "@zilliz/milvus2-sdk-node/dist/milvus/types";
+
+enum VectorTypes {
+  Binary = DataType.BinaryVector,
+  Float = DataType.FloatVector,
+}
+
+export class CreateCollectionDto {
+  @IsString()
+  readonly collection_name: string;
+
+  @IsBoolean()
+  @IsOptional()
+  readonly autoID: boolean;
+
+  @IsArray()
+  @ArrayNotEmpty()
+  readonly fields: FieldType[];
+}
+
+export class ShowCollectionsDto {
+  @IsOptional()
+  @IsEnum(ShowCollectionsType, { message: "Type allow all->0 inmemory->1" })
+  readonly type: ShowCollectionsType;
+}
+
+export class InsertDataDto {
+  @IsOptional()
+  readonly partition_names?: string[];
+
+  @IsArray()
+  readonly fields_data: any[];
+}
+
+export class VectorSearchDto {
+  @IsOptional()
+  partition_names?: string[];
+
+  @IsString()
+  @IsOptional()
+  expr?: string;
+
+  @IsObject()
+  search_params: SearchParam;
+
+  @IsArray()
+  @ArrayMinSize(1)
+  vectors: number[][];
+
+  @IsArray()
+  @IsOptional()
+  output_fields?: string[];
+
+  @IsEnum(VectorTypes, { message: "Type allow all->0 inmemory->1" })
+  vector_type: DataType.BinaryVector | DataType.FloatVector;
+}
+
+export class CreateAliasDto {
+  @IsString()
+  alias: string;
+}
+
+export class QueryDto {
+  @IsString()
+  readonly expr: string;
+
+  @IsArray()
+  @IsOptional()
+  readonly partitions_names: string[];
+
+  @IsArray()
+  @IsOptional()
+  readonly output_fields: string[];
+}

+ 5 - 166
express/src/collections/index.ts

@@ -1,169 +1,8 @@
-import express from "express";
-import { CollectionsService } from "./collections.service";
-import { milvusService } from "../milvus";
+import { CollectionController } from "./collections.controller";
 
-const router = express.Router();
+const collectionsManager = new CollectionController();
 
-export const collectionsService = new CollectionsService(milvusService);
+const router = collectionsManager.generateRoutes();
+const collectionsService = collectionsManager.collectionsServiceGetter;
 
-router.get("/", async (req, res, next) => {
-  const type = parseInt("" + req.query?.type, 10);
-  try {
-    const result =
-      type === 1
-        ? await collectionsService.getLoadedColletions()
-        : await collectionsService.getAllCollections();
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.get("/statistics", async (req, res, next) => {
-  try {
-    const result = await collectionsService.getStatistics();
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.post("/", async (req, res, next) => {
-  const createCollectionData = req.body;
-  try {
-    const result = await collectionsService.createCollection(
-      createCollectionData
-    );
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.delete("/:name", async (req, res, next) => {
-  const name = req.params?.name;
-  try {
-    const result = await collectionsService.dropCollection({
-      collection_name: name,
-    });
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.get("/:name", async (req, res, next) => {
-  const name = req.params?.name;
-  try {
-    const result = await collectionsService.describeCollection({
-      collection_name: name,
-    });
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.get("/:name/statistics", async (req, res, next) => {
-  const name = req.params?.name;
-  try {
-    const result = await collectionsService.getCollectionStatistics({
-      collection_name: name,
-    });
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.get("/indexes/status", async (req, res, next) => {
-  try {
-    const result = await collectionsService.getCollectionsIndexStatus();
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.put("/:name/load", async (req, res, next) => {
-  const name = req.params?.name;
-  try {
-    const result = await collectionsService.loadCollection({
-      collection_name: name,
-    });
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.put("/:name/release", async (req, res, next) => {
-  const name = req.params?.name;
-  try {
-    const result = await collectionsService.releaseCollection({
-      collection_name: name,
-    });
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.post("/:name/insert", async (req, res, next) => {
-  const name = req.params?.name;
-  const data = req.body;
-  try {
-    const result = await collectionsService.insert({
-      collection_name: name,
-      ...data,
-    });
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.post("/:name/search", async (req, res, next) => {
-  const name = req.params?.name;
-  const data = req.body;
-  try {
-    const result = await collectionsService.vectorSearch({
-      collection_name: name,
-      ...data,
-    });
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.post("/:name/query", async (req, res, next) => {
-  const name = req.params?.name;
-  const data = req.body;
-  const resultiLmit: any = req.query?.limit;
-  const resultPage: any = req.query?.page;
-  try {
-    const limit = isNaN(resultiLmit) ? 100 : parseInt(resultiLmit, 10);
-    const page = isNaN(resultPage) ? 0 : parseInt(resultPage, 10);
-  // TODO: add page and limit to node SDK
-  // Here may raise "Error: 8 RESOURCE_EXHAUSTED: Received message larger than max"
-    const result = await collectionsService.query({
-      collection_name: name,
-      ...data,
-    });
-    const queryResultList = result.data;
-    const queryResultLength = result.data.length;
-    const startNum = page * limit;
-    const endNum = (page + 1) * limit;
-    const slicedResult = queryResultList.slice(startNum, endNum);
-    result.data = slicedResult;
-    res.send({ ...result, limit, page, total: queryResultLength });
-  } catch (error) {
-    next(error);
-  }
-});
-
-router.post("/:name/alias", async (req, res, next) => {
-  const name = req.params?.name;
-  const data = req.body;
-  try {
-    const result = await collectionsService.createAlias({
-      collection_name: name,
-      ...data,
-    });
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-
-export { router };
+export { router, collectionsService };

+ 40 - 0
express/src/crons/crons.controller.ts

@@ -0,0 +1,40 @@
+import { NextFunction, Request, Response, Router } from "express";
+import { dtoValidationMiddleware } from "../middlewares/validation";
+import { CronsService, SchedulerRegistry } from "./crons.service";
+import { collectionsService } from "../collections";
+import { ToggleCronJobByNameDto } from "./dto";
+
+export class CronsController {
+  private router: Router;
+  private schedulerRegistry: SchedulerRegistry;
+  private cronsService: CronsService;
+
+  constructor() {
+    this.schedulerRegistry = new SchedulerRegistry([]);
+    this.cronsService = new CronsService(
+      collectionsService,
+      this.schedulerRegistry
+    );
+    this.router = Router();
+  }
+
+  generateRoutes() {
+    this.router.put(
+      "/",
+      dtoValidationMiddleware(ToggleCronJobByNameDto),
+      this.toggleCronJobByName.bind(this)
+    );
+
+    return this.router;
+  }
+
+  async toggleCronJobByName(req: Request, res: Response, next: NextFunction) {
+    const cronData = req.body;
+    try {
+      const result = await this.cronsService.toggleCronJobByName(cronData);
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+}

+ 10 - 0
express/src/crons/dto.ts

@@ -0,0 +1,10 @@
+import { IsEnum, IsString } from "class-validator";
+import { WS_EVENTS_TYPE } from "../utils/Const";
+
+export class ToggleCronJobByNameDto {
+  @IsString()
+  name: string;
+
+  @IsEnum(WS_EVENTS_TYPE, { message: "Type allow start->0 stop->1" })
+  type: WS_EVENTS_TYPE;
+}

+ 3 - 18
express/src/crons/index.ts

@@ -1,21 +1,6 @@
-import express from "express";
-import { CronsService, SchedulerRegistry } from "./crons.service";
-import { collectionsService } from "../collections";
+import { CronsController } from "./crons.controller";
 
-const router = express.Router();
-
-const schedulerRegistry = new SchedulerRegistry([]);
-
-const cronsService = new CronsService(collectionsService, schedulerRegistry);
-
-router.put("/", async (req, res, next) => {
-  const cronData = req.body;
-  try {
-    const result = await cronsService.toggleCronJobByName(cronData);
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
+const cronsManager = new CronsController();
+const router = cronsManager.generateRoutes();
 
 export { router };

+ 11 - 0
express/src/exception/HttpException.ts

@@ -0,0 +1,11 @@
+class HttpException extends Error {
+  status: number;
+  message: string;
+  constructor(status: number, message: string) {
+    super(message);
+    this.status = status;
+    this.message = message;
+  }
+}
+
+export default HttpException;

+ 34 - 0
express/src/middlewares/validation.ts

@@ -0,0 +1,34 @@
+import { RequestHandler } from "express";
+import { plainToClass } from "class-transformer";
+import { validate, ValidationError } from "class-validator";
+import { sanitize } from "class-sanitizer";
+import HttpException from "../exception/HttpException";
+
+export const dtoValidationMiddleware = (
+  type: any,
+  skipMissingProperties = false
+): RequestHandler => {
+  return (req, res, next) => {
+    const dtoObj = plainToClass(
+      type,
+      req.method === "GET" || req.method === "DELETE" ? req.query : req.body
+    );
+    validate(dtoObj, { skipMissingProperties }).then(
+      (errors: ValidationError[]) => {
+        if (errors.length > 0) {
+          const dtoErrors = errors
+            .map((error: ValidationError) =>
+              (Object as any).values(error.constraints)
+            )
+            .join(", ");
+          next(new HttpException(400, dtoErrors));
+        } else {
+          // sanitize the object and call the next middleware
+          sanitize(dtoObj);
+          req.body = dtoObj;
+          next();
+        }
+      }
+    );
+  };
+};

+ 17 - 0
express/src/milvus/dto.ts

@@ -0,0 +1,17 @@
+import { ArrayMinSize, IsArray, IsString } from "class-validator";
+
+export class ConnectMilvusDto {
+  @IsString()
+  readonly address: string;
+}
+
+export class CheckMilvusDto {
+  @IsString()
+  readonly address: string;
+}
+
+export class FlushDto {
+  @IsArray()
+  @ArrayMinSize(1, { message: "At least need one collection name." })
+  readonly collection_names: string[];
+}

+ 4 - 41
express/src/milvus/index.ts

@@ -1,44 +1,7 @@
-import express from "express";
-import { MilvusService } from "./milvus.service";
+import { MilvusController } from "./milvus.controller";
 
-const router = express.Router();
-
-const milvusService = new MilvusService();
-
-router.post("/connect", async (req, res, next) => {
-  const address = req.body?.address;
-  try {
-    const result = await milvusService.connectMilvus(address);
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.get("/check", async (req, res, next) => {
-  const address = "" + req.query?.address;
-  try {
-    const result = await milvusService.checkConnect(address);
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.put("/flush", async (req, res, next) => {
-  const collectionNames = req.body;
-  try {
-    const result = await milvusService.flush(collectionNames);
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.get("/metrics", async (req, res, next) => {
-  try {
-    const result = await milvusService.getMetrics();
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
+const MilvusManager = new MilvusController();
+const router = MilvusManager.generateRoutes();
+const milvusService = MilvusManager.milvusServiceGetter;
 
 export { router, milvusService };

+ 81 - 0
express/src/milvus/milvus.controller.ts

@@ -0,0 +1,81 @@
+import { NextFunction, Request, Response, Router } from "express";
+import { dtoValidationMiddleware } from "../middlewares/validation";
+import { MilvusService } from "./milvus.service";
+import { CheckMilvusDto, ConnectMilvusDto, FlushDto } from "./dto";
+
+export class MilvusController {
+  private router: Router;
+  private milvusService: MilvusService;
+
+  constructor() {
+    this.milvusService = new MilvusService();
+    this.router = Router();
+  }
+
+  get milvusServiceGetter() {
+    return this.milvusService;
+  }
+
+  generateRoutes() {
+    this.router.post(
+      "/connect",
+      dtoValidationMiddleware(ConnectMilvusDto),
+      this.connectMilvus.bind(this)
+    );
+
+    this.router.get(
+      "/check",
+      dtoValidationMiddleware(CheckMilvusDto),
+      this.checkConnect.bind(this)
+    );
+
+    this.router.put(
+      "/flush",
+      dtoValidationMiddleware(FlushDto),
+      this.flush.bind(this)
+    );
+
+    this.router.get("/metrics", this.getMetrics.bind(this));
+
+    return this.router;
+  }
+
+  async connectMilvus(req: Request, res: Response, next: NextFunction) {
+    const address = req.body?.address;
+    try {
+      const result = await this.milvusService.connectMilvus(address);
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async checkConnect(req: Request, res: Response, next: NextFunction) {
+    const address = "" + req.query?.address;
+    try {
+      const result = await this.milvusService.checkConnect(address);
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async flush(req: Request, res: Response, next: NextFunction) {
+    const collectionNames = req.body;
+    try {
+      const result = await this.milvusService.flush(collectionNames);
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async getMetrics(req: Request, res: Response, next: NextFunction) {
+    try {
+      const result = await this.milvusService.getMetrics();
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+}

+ 30 - 0
express/src/partitions/dto.ts

@@ -0,0 +1,30 @@
+import { IsString, IsEnum, IsArray, ArrayNotEmpty } from "class-validator";
+
+export enum ManageType {
+  DELETE = "delete",
+  CREATE = "create",
+}
+export class GetPartitionsInfoDto {
+  @IsString()
+  readonly collection_name: string;
+}
+
+export class ManagePartitionDto {
+  @IsString()
+  readonly collection_name: string;
+
+  @IsString()
+  readonly partition_name: string;
+
+  @IsEnum(ManageType, { message: "Type allow delete and create" })
+  readonly type: ManageType;
+}
+
+export class LoadPartitionsDto {
+  @IsString()
+  readonly collection_name: string;
+
+  @IsArray()
+  @ArrayNotEmpty()
+  readonly partition_names: string[];
+}

+ 3 - 48
express/src/partitions/index.ts

@@ -1,51 +1,6 @@
-import express from "express";
-import { PartitionsService } from "./partitions.service";
-import { milvusService } from "../milvus";
+import { PartitionController } from "./partitions.controller";
 
-const router = express.Router();
-
-const partitionsService = new PartitionsService(milvusService);
-
-router.get("/", async (req, res, next) => {
-  const collectionName = "" + req.query?.collection_name;
-  try {
-    const result = await partitionsService.getPatitionsInfo({
-      collection_name: collectionName,
-    });
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.post("/", async (req, res, next) => {
-  const { type, ...params } = req.body;
-  try {
-    const result =
-      type.toLocaleLowerCase() === "create"
-        ? await partitionsService.createParition(params)
-        : await partitionsService.deleteParition(params);
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.put("/load", async (req, res, next) => {
-  const loadData = req.body;
-  try {
-    const result = await partitionsService.loadPartitions(loadData);
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.put("/release", async (req, res, next) => {
-  const loadData = req.body;
-  try {
-    const result = await partitionsService.releasePartitions(loadData);
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
+const partitionManager = new PartitionController();
+const router = partitionManager.generateRoutes();
 
 export { router };

+ 93 - 0
express/src/partitions/partitions.controller.ts

@@ -0,0 +1,93 @@
+import { NextFunction, Request, Response, Router } from "express";
+import { dtoValidationMiddleware } from "../middlewares/validation";
+import { PartitionsService } from "./partitions.service";
+import { milvusService } from "../milvus";
+
+import {
+  GetPartitionsInfoDto,
+  ManagePartitionDto,
+  LoadPartitionsDto,
+} from "./dto";
+
+export class PartitionController {
+  private router: Router;
+  private partitionsService: PartitionsService;
+
+  constructor() {
+    this.partitionsService = new PartitionsService(milvusService);
+    this.router = Router();
+  }
+
+  generateRoutes() {
+    this.router.get(
+      "/",
+      dtoValidationMiddleware(GetPartitionsInfoDto),
+      this.getPatitionsInfo.bind(this)
+    );
+
+    this.router.post(
+      "/",
+      dtoValidationMiddleware(ManagePartitionDto),
+      this.managePartition.bind(this)
+    );
+
+    this.router.post(
+      "/load",
+      dtoValidationMiddleware(LoadPartitionsDto),
+      this.loadPartition.bind(this)
+    );
+
+    this.router.post(
+      "/release",
+      dtoValidationMiddleware(LoadPartitionsDto),
+      this.releasePartition.bind(this)
+    );
+
+    return this.router;
+  }
+
+  async getPatitionsInfo(req: Request, res: Response, next: NextFunction) {
+    const collectionName = "" + req.query?.collection_name;
+    try {
+      const result = await this.partitionsService.getPatitionsInfo({
+        collection_name: collectionName,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async managePartition(req: Request, res: Response, next: NextFunction) {
+    const { type, ...params } = req.body;
+    try {
+      const result =
+        type.toLocaleLowerCase() === "create"
+          ? await this.partitionsService.createParition(params)
+          : await this.partitionsService.deleteParition(params);
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async loadPartition(req: Request, res: Response, next: NextFunction) {
+    const data = req.body;
+    try {
+      const result = await this.partitionsService.loadPartitions(data);
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async releasePartition(req: Request, res: Response, next: NextFunction) {
+    const data = req.body;
+    try {
+      const result = await this.partitionsService.releasePartitions(data);
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+}

+ 63 - 0
express/src/schema/dto.ts

@@ -0,0 +1,63 @@
+import { CreateIndexParam } from "@zilliz/milvus2-sdk-node/dist/milvus/types";
+import {
+  IsString,
+  IsEnum,
+  IsOptional,
+  IsObject,
+  IsArray,
+} from "class-validator";
+
+class KeyValuePair {
+  key: string;
+  value: string;
+}
+
+export enum ManageType {
+  DELETE = "delete",
+  CREATE = "create",
+}
+
+export class ManageIndexDto {
+  @IsEnum(ManageType, { message: "Type allow delete and create" })
+  readonly type: ManageType;
+
+  @IsString()
+  readonly collection_name: string;
+
+  @IsString()
+  readonly field_name: string;
+
+  @IsObject()
+  @IsOptional()
+  readonly extra_params?: CreateIndexParam;
+}
+
+export class DescribeIndexDto {
+  @IsString()
+  readonly collection_name: string;
+
+  @IsString()
+  @IsOptional()
+  readonly field_name?: string;
+}
+
+export class GetIndexStateDto {
+  @IsString()
+  readonly collection_name: string;
+
+  @IsString()
+  @IsOptional()
+  readonly field_name?: string;
+}
+
+export class GetIndexProgressDto {
+  @IsString()
+  readonly collection_name: string;
+
+  @IsString()
+  readonly index_name: string;
+
+  @IsString()
+  @IsOptional()
+  readonly field_name?: string;
+}

+ 3 - 53
express/src/schema/index.ts

@@ -1,55 +1,5 @@
-import express from "express";
-import { SchemaService } from "./schema.service";
-import { milvusService } from "../milvus";
-
-const router = express.Router();
-
-const schemaService = new SchemaService(milvusService);
-
-router.post("/index", async (req, res, next) => {
-  const { type, collection_name, extra_params, field_name } = req.body;
-  try {
-    const result =
-      type.toLocaleLowerCase() === "create"
-        ? await schemaService.createIndex({
-            collection_name,
-            extra_params,
-            field_name,
-          })
-        : await schemaService.dropIndex({ collection_name, field_name });
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.get("/index", async (req, res, next) => {
-  const data = "" + req.query?.collection_name;
-  try {
-    const result = await schemaService.describeIndex({ collection_name: data });
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.get("/index/progress", async (req, res, next) => {
-  const data = "" + req.query?.collection_name;
-  try {
-    const result = await schemaService.getIndexBuildProgress({
-      collection_name: data,
-    });
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
-router.get("/index/state", async (req, res, next) => {
-  const data = "" + req.query?.collection_name;
-  try {
-    const result = await schemaService.getIndexState({ collection_name: data });
-    res.send(result);
-  } catch (error) {
-    next(error);
-  }
-});
+import { SchemaController } from "./schema.controller";
+const schemaManager = new SchemaController();
+const router = schemaManager.generateRoutes();
 
 export { router };

+ 102 - 0
express/src/schema/schema.controller.ts

@@ -0,0 +1,102 @@
+import { NextFunction, Request, Response, Router } from "express";
+import { dtoValidationMiddleware } from "../middlewares/validation";
+import { SchemaService } from "./schema.service";
+import { milvusService } from "../milvus";
+
+import {
+  ManageIndexDto,
+  DescribeIndexDto,
+  GetIndexProgressDto,
+  GetIndexStateDto,
+} from "./dto";
+
+export class SchemaController {
+  private router: Router;
+  private schemaService: SchemaService;
+
+  constructor() {
+    this.schemaService = new SchemaService(milvusService);
+    this.router = Router();
+  }
+
+  generateRoutes() {
+    this.router.post(
+      "/index",
+      dtoValidationMiddleware(ManageIndexDto),
+      this.manageIndex.bind(this)
+    );
+
+    this.router.get(
+      "/index",
+      dtoValidationMiddleware(DescribeIndexDto),
+      this.describeIndex.bind(this)
+    );
+
+    this.router.post(
+      "/index/progress",
+      dtoValidationMiddleware(GetIndexProgressDto),
+      this.getIndexBuildProgress.bind(this)
+    );
+
+    this.router.post(
+      "/index/state",
+      dtoValidationMiddleware(GetIndexStateDto),
+      this.getIndexState.bind(this)
+    );
+
+    return this.router;
+  }
+
+  async manageIndex(req: Request, res: Response, next: NextFunction) {
+    const { type, collection_name, extra_params, field_name } = req.body;
+    try {
+      const result =
+        type.toLocaleLowerCase() === "create"
+          ? await this.schemaService.createIndex({
+              collection_name,
+              extra_params,
+              field_name,
+            })
+          : await this.schemaService.dropIndex({ collection_name, field_name });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async describeIndex(req: Request, res: Response, next: NextFunction) {
+    const data = "" + req.query?.collection_name;
+    try {
+      const result = await this.schemaService.describeIndex({
+        collection_name: data,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async getIndexBuildProgress(req: Request, res: Response, next: NextFunction) {
+    const data = "" + req.query?.collection_name;
+    try {
+      const result = await this.schemaService.getIndexBuildProgress({
+        collection_name: data,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async getIndexState(req: Request, res: Response, next: NextFunction) {
+    const data = "" + req.query?.collection_name;
+    try {
+      const result = await this.schemaService.getIndexState({
+        collection_name: data,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+}

+ 26 - 0
express/src/swagger.ts

@@ -0,0 +1,26 @@
+import swaggerJsdoc from "swagger-jsdoc";
+
+export const surveSwaggerSpecification = () => {
+  // Swagger definition
+  // You can set every attribute except paths and swagger
+  // https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md
+
+  // Options for the swagger docs
+  const options = {
+    definition: {
+      openapi: "3.0.0",
+      info: {
+        title: "Insight server",
+        version: "1.0.0",
+      },
+      servers: [{ url: "/api/v1" }],
+    },
+    apis: ["./src/**/*.ts"],
+  };
+  const swaggerSpec = swaggerJsdoc(options);
+
+  // And here we go, we serve it.
+  // res.setHeader("Content-Type", "application/json");
+  // res.send(swaggerSpec);
+  return swaggerSpec;
+};

+ 2 - 0
express/tsconfig.json

@@ -1,10 +1,12 @@
 {
   "compilerOptions": {
+    "experimentalDecorators": true,
     "module": "commonjs",
     "esModuleInterop": true,
     "target": "es6",
     "noImplicitAny": true,
     "moduleResolution": "node",
+    "emitDecoratorMetadata": true,
     "sourceMap": true,
     "outDir": "dist",
     "baseUrl": ".",

+ 12 - 4
express/tslint.json

@@ -1,11 +1,19 @@
 {
   "defaultSeverity": "error",
-  "extends": ["tslint:recommended"],
+  "extends": [
+    "tslint:recommended"
+  ],
   "jsRules": {},
   "rules": {
-    "trailing-comma": [false],
+    "trailing-comma": [
+      false
+    ],
     "no-console": false,
-    "max-classes-per-file": false
+    "max-classes-per-file": false,
+    "variable-name": [
+      false,
+      "allow-leading-underscore"
+    ]
   },
   "rulesDirectory": []
-}
+}

+ 192 - 3
express/yarn.lock

@@ -2,6 +2,38 @@
 # yarn lockfile v1
 
 
+"@apidevtools/json-schema-ref-parser@^9.0.6":
+  version "9.0.9"
+  resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#d720f9256e3609621280584f2b47ae165359268b"
+  integrity sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w==
+  dependencies:
+    "@jsdevtools/ono" "^7.1.3"
+    "@types/json-schema" "^7.0.6"
+    call-me-maybe "^1.0.1"
+    js-yaml "^4.1.0"
+
+"@apidevtools/openapi-schemas@^2.0.4":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17"
+  integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==
+
+"@apidevtools/swagger-methods@^3.0.2":
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267"
+  integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==
+
+"@apidevtools/swagger-parser@10.0.2":
+  version "10.0.2"
+  resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.0.2.tgz#f4145afb7c3a3bafe0376f003b5c3bdeae17a952"
+  integrity sha512-JFxcEyp8RlNHgBCE98nwuTkZT6eNFPc1aosWV6wPcQph72TSEEu1k3baJD4/x1qznU+JiDdz8F5pTwabZh+Dhg==
+  dependencies:
+    "@apidevtools/json-schema-ref-parser" "^9.0.6"
+    "@apidevtools/openapi-schemas" "^2.0.4"
+    "@apidevtools/swagger-methods" "^3.0.2"
+    "@jsdevtools/ono" "^7.1.3"
+    call-me-maybe "^1.0.1"
+    z-schema "^4.2.3"
+
 "@babel/code-frame@^7.0.0":
   version "7.15.8"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.15.8.tgz#45990c47adadb00c03677baa89221f7cc23d2503"
@@ -54,6 +86,11 @@
     protobufjs "^6.10.0"
     yargs "^16.1.1"
 
+"@jsdevtools/ono@^7.1.3":
+  version "7.1.3"
+  resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796"
+  integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==
+
 "@microsoft/api-documenter@^7.13.39":
   version "7.13.65"
   resolved "https://registry.yarnpkg.com/@microsoft/api-documenter/-/api-documenter-7.13.65.tgz#c9c82369046e882d26f557ab187428f254123b62"
@@ -278,7 +315,7 @@
     "@types/qs" "*"
     "@types/range-parser" "*"
 
-"@types/express@^4.17.13":
+"@types/express@*", "@types/express@^4.17.13":
   version "4.17.13"
   resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
   integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
@@ -296,6 +333,11 @@
     "@types/minimatch" "*"
     "@types/node" "*"
 
+"@types/json-schema@^7.0.6":
+  version "7.0.9"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
+  integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
+
 "@types/long@^4.0.1":
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
@@ -351,6 +393,24 @@
     "@types/mime" "^1"
     "@types/node" "*"
 
+"@types/swagger-jsdoc@^6.0.1":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.1.tgz#94a99aca0356cb64ad2a6eb903ed034703453801"
+  integrity sha512-+MUpcbyxD528dECUBCEVm6abNuORdbuGjbrUdHDeAQ+rkPuo2a+L4N02WJHF3bonSSE6SJ3dUJwF2V6+cHnf0w==
+
+"@types/swagger-ui-express@^4.1.3":
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/@types/swagger-ui-express/-/swagger-ui-express-4.1.3.tgz#7adbbbf5343b45869debef1e9ff39c9ba73e380f"
+  integrity sha512-jqCjGU/tGEaqIplPy3WyQg+Nrp6y80DCFnDEAvVKWkJyv0VivSSDCChkppHRHAablvInZe6pijDFMnavtN0vqA==
+  dependencies:
+    "@types/express" "*"
+    "@types/serve-static" "*"
+
+"@types/validator@^13.1.3":
+  version "13.6.6"
+  resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.6.6.tgz#6e6e2d086148db5ae14851614971b715670cbd52"
+  integrity sha512-+qogUELb4gMhrMjSh/seKmGVvN+uQLfyqJAqYRWqVHsvBsUO2xDBCL8CJ/ZSukbd8vXaoYbpIssAmfLEzzBHEw==
+
 "@types/ws@^8.2.0":
   version "8.2.0"
   resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.0.tgz#75faefbe2328f3b833cb8dc640658328990d04f3"
@@ -448,6 +508,11 @@ argparse@^1.0.7, argparse@~1.0.9:
   dependencies:
     sprintf-js "~1.0.2"
 
+argparse@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+  integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
 array-flatten@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
@@ -548,6 +613,11 @@ cacheable-request@^6.0.0:
     normalize-url "^4.1.0"
     responselike "^1.0.2"
 
+call-me-maybe@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
+  integrity sha1-JtII6onje1y95gJQoV8DHBak1ms=
+
 camelcase@^6.2.0:
   version "6.2.0"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
@@ -590,6 +660,27 @@ ci-info@^2.0.0:
   resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
   integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
 
+class-sanitizer@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/class-sanitizer/-/class-sanitizer-1.0.1.tgz#d08f81b509f86697f4b22aab222d1531cf8a9840"
+  integrity sha512-E4lgSXP3nbJo5aflAsV9H/sauXxMbUr8XdriYEI6Cc3L5CtnEkSmxoAS8Rbj90Yq/s/S1ceoXGARjOnOgyyKQQ==
+  dependencies:
+    validator "^13.1.1"
+
+class-transformer@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.4.0.tgz#b52144117b423c516afb44cc1c76dbad31c2165b"
+  integrity sha512-ETWD/H2TbWbKEi7m9N4Km5+cw1hNcqJSxlSYhsLsNjQzWWiZIYA1zafxpK9PwVfaZ6AqR5rrjPVUBGESm5tQUA==
+
+class-validator@^0.13.1:
+  version "0.13.1"
+  resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.13.1.tgz#381b2001ee6b9e05afd133671fbdf760da7dec67"
+  integrity sha512-zWIeYFhUitvAHBwNhDdCRK09hWx+P0HUwFE8US8/CxFpMVzkUK8RJl7yOIE+BVu2lxyPNgeOaFv78tLE47jBIg==
+  dependencies:
+    "@types/validator" "^13.1.3"
+    libphonenumber-js "^1.9.7"
+    validator "^13.5.2"
+
 cli-boxes@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
@@ -640,6 +731,11 @@ colors@~1.2.1:
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.5.tgz#89c7ad9a374bc030df8013241f68136ed8835afc"
   integrity sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==
 
+commander@6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75"
+  integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==
+
 commander@^2.12.1, commander@^2.7.1:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -786,6 +882,13 @@ diff@^4.0.1:
   resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
   integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
 
+doctrine@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
+  integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
+  dependencies:
+    esutils "^2.0.2"
+
 dot-prop@^5.2.0:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
@@ -868,6 +971,11 @@ esprima@^4.0.0:
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
 
+esutils@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+  integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
 etag@~1.8.1:
   version "1.8.1"
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
@@ -999,6 +1107,18 @@ glob-parent@~5.1.2:
   dependencies:
     is-glob "^4.0.1"
 
+glob@7.1.6:
+  version "7.1.6"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
+  integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
 glob@^7.1.1, glob@^7.1.3, glob@^7.2.0:
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
@@ -1253,6 +1373,13 @@ js-yaml@^3.13.1:
     argparse "^1.0.7"
     esprima "^4.0.0"
 
+js-yaml@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+  integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+  dependencies:
+    argparse "^2.0.1"
+
 js-yaml@~3.13.1:
   version "3.13.1"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
@@ -1292,21 +1419,31 @@ latest-version@^5.1.0:
   dependencies:
     package-json "^6.3.0"
 
+libphonenumber-js@^1.9.7:
+  version "1.9.42"
+  resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.42.tgz#41f41d540f89b6e3fd36de7120ddb57b3a468c77"
+  integrity sha512-UBtU0ylpZPKPT8NLIyQJWj/DToMFxmo3Fm5m6qDc0LATvf0SY0qUhaurCEvukAB9Fo+Ia2Anjzqwoupaa64fXg==
+
 lodash.camelcase@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
   integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
 
-lodash.get@^4.0.0:
+lodash.get@^4.0.0, lodash.get@^4.4.2:
   version "4.4.2"
   resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
   integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
 
-lodash.isequal@^4.0.0:
+lodash.isequal@^4.0.0, lodash.isequal@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
   integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
 
+lodash.mergewith@^4.6.2:
+  version "4.6.2"
+  resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
+  integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
+
 lodash@~4.17.15:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
@@ -1880,6 +2017,37 @@ supports-color@^7.1.0:
   dependencies:
     has-flag "^4.0.0"
 
+swagger-jsdoc@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/swagger-jsdoc/-/swagger-jsdoc-6.1.0.tgz#c2b86321f2c4dde8947b418fe8a4bc94431d5522"
+  integrity sha512-xgep5M8Gq31MxpCbQLvJZpNqHfGPfI+sILCzujZbEXIQp2COtkZgoGASs0gacRs4xHmLDH+GuMGdorPITSG4tA==
+  dependencies:
+    commander "6.2.0"
+    doctrine "3.0.0"
+    glob "7.1.6"
+    lodash.mergewith "^4.6.2"
+    swagger-parser "10.0.2"
+    yaml "2.0.0-1"
+
+swagger-parser@10.0.2:
+  version "10.0.2"
+  resolved "https://registry.yarnpkg.com/swagger-parser/-/swagger-parser-10.0.2.tgz#d7f18faa09c9c145e938977c9bd6c3435998b667"
+  integrity sha512-9jHkHM+QXyLGFLk1DkXBwV+4HyNm0Za3b8/zk/+mjr8jgOSiqm3FOTHBSDsBjtn9scdL+8eWcHdupp2NLM8tDw==
+  dependencies:
+    "@apidevtools/swagger-parser" "10.0.2"
+
+swagger-ui-dist@^3.18.1:
+  version "3.52.5"
+  resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.52.5.tgz#9aa8101a2be751f5145195b9e048bc21b12fac60"
+  integrity sha512-8z18eX8G/jbTXYzyNIaobrnD7PSN7yU/YkSasMmajrXtw0FGS64XjrKn5v37d36qmU3o1xLeuYnktshRr7uIFw==
+
+swagger-ui-express@^4.1.6:
+  version "4.1.6"
+  resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz#682294af3d5c70f74a1fa4d6a9b503a9ee55ea82"
+  integrity sha512-Xs2BGGudvDBtL7RXcYtNvHsFtP1DBFPMJFRxHe5ez/VG/rzVOEjazJOOSc/kSCyxreCTKfJrII6MJlL9a6t8vw==
+  dependencies:
+    swagger-ui-dist "^3.18.1"
+
 timsort@~0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
@@ -2044,6 +2212,11 @@ utils-merge@1.0.1:
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
   integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
 
+validator@^13.1.1, validator@^13.5.2, validator@^13.6.0:
+  version "13.7.0"
+  resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857"
+  integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==
+
 validator@^8.0.0:
   version "8.2.0"
   resolved "https://registry.yarnpkg.com/validator/-/validator-8.2.0.tgz#3c1237290e37092355344fef78c231249dab77b9"
@@ -2112,6 +2285,11 @@ yallist@^4.0.0:
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
   integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
 
+yaml@2.0.0-1:
+  version "2.0.0-1"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.0-1.tgz#8c3029b3ee2028306d5bcf396980623115ff8d18"
+  integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==
+
 yargs-parser@^20.2.2:
   version "20.2.9"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
@@ -2135,6 +2313,17 @@ yn@3.1.1:
   resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
   integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
 
+z-schema@^4.2.3:
+  version "4.2.4"
+  resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-4.2.4.tgz#73102a49512179b12a8ec50b1daa676b984da6e4"
+  integrity sha512-YvBeW5RGNeNzKOUJs3rTL4+9rpcvHXt5I051FJbOcitV8bl40pEfcG0Q+dWSwS0/BIYrMZ/9HHoqLllMkFhD0w==
+  dependencies:
+    lodash.get "^4.4.2"
+    lodash.isequal "^4.5.0"
+    validator "^13.6.0"
+  optionalDependencies:
+    commander "^2.7.1"
+
 z-schema@~3.18.3:
   version "3.18.4"
   resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-3.18.4.tgz#ea8132b279533ee60be2485a02f7e3e42541a9a2"