Browse Source

resolve conflict

tumao 3 years ago
parent
commit
8906ee137e
44 changed files with 1676 additions and 888 deletions
  1. 1 0
      .gitignore
  2. 7 6
      client/src/components/advancedSearch/Condition.tsx
  3. 20 4
      client/src/components/advancedSearch/Filter.tsx
  4. 2 1
      client/src/components/advancedSearch/Types.ts
  5. 1 1
      client/src/components/grid/Types.ts
  6. 1 1
      client/src/http/BaseModel.ts
  7. 9 1
      client/src/http/Collection.ts
  8. 1 0
      client/src/i18n/cn/button.ts
  9. 3 0
      client/src/i18n/cn/collection.ts
  10. 1 0
      client/src/i18n/en/button.ts
  11. 3 0
      client/src/i18n/en/collection.ts
  12. 5 0
      client/src/pages/collections/Collection.tsx
  13. 1 0
      client/src/pages/collections/Collections.tsx
  14. 192 0
      client/src/pages/query/Query.tsx
  15. 51 0
      client/src/pages/query/Styles.ts
  16. 5 0
      client/src/pages/query/Types.ts
  17. 29 4
      express/package.json
  18. 23 16
      express/src/app.ts
  19. 276 0
      express/src/collections/collections.controller.ts
  20. 12 1
      express/src/collections/collections.service.ts
  21. 90 0
      express/src/collections/dto.ts
  22. 5 141
      express/src/collections/index.ts
  23. 40 0
      express/src/crons/crons.controller.ts
  24. 4 3
      express/src/crons/crons.service.ts
  25. 10 0
      express/src/crons/dto.ts
  26. 3 18
      express/src/crons/index.ts
  27. 11 0
      express/src/exception/HttpException.ts
  28. 0 90
      express/src/interceptors/index.ts
  29. 62 0
      express/src/middlewares/index.ts
  30. 34 0
      express/src/middlewares/validation.ts
  31. 17 0
      express/src/milvus/dto.ts
  32. 4 41
      express/src/milvus/index.ts
  33. 81 0
      express/src/milvus/milvus.controller.ts
  34. 30 0
      express/src/partitions/dto.ts
  35. 3 48
      express/src/partitions/index.ts
  36. 93 0
      express/src/partitions/partitions.controller.ts
  37. 63 0
      express/src/schema/dto.ts
  38. 3 53
      express/src/schema/index.ts
  39. 102 0
      express/src/schema/schema.controller.ts
  40. 26 0
      express/src/swagger.ts
  41. 16 3
      express/src/utils/index.ts
  42. 2 0
      express/tsconfig.json
  43. 12 4
      express/tslint.json
  44. 322 452
      express/yarn.lock

+ 1 - 0
.gitignore

@@ -36,6 +36,7 @@ server/vectors.csv
 
 
 express/node_modules
 express/node_modules
 express/dist
 express/dist
+express/build
 
 
 
 
 # package.lock.json
 # package.lock.json

+ 7 - 6
client/src/components/advancedSearch/Condition.tsx

@@ -71,23 +71,24 @@ const Condition: FC<ConditionProps> = props => {
   useEffect(() => {
   useEffect(() => {
     const regInt = /^\d+$/;
     const regInt = /^\d+$/;
     const regFloat = /^\d+\.\d+$/;
     const regFloat = /^\d+\.\d+$/;
-    const regIntInterval = /^\[\d+,\d+\]$/;
-    const regFloatInterval = /^\[\d+\.\d+,\d+\.\d+]$/;
+    const regIntInterval = /^\[\d+(,\d+)*\]$/;
+    const regFloatInterval = /^\[\d+\.\d+(,\d+\.\d+)*\]$/;
 
 
     const type = conditionField?.type;
     const type = conditionField?.type;
     const isIn = operator === 'in';
     const isIn = operator === 'in';
     let isLegal = false;
     let isLegal = false;
+    const conditionValueWithNoSpace = conditionValue.replaceAll(' ', '');
 
 
     switch (type) {
     switch (type) {
       case 'int':
       case 'int':
         isLegal = isIn
         isLegal = isIn
-          ? regIntInterval.test(conditionValue)
-          : regInt.test(conditionValue);
+          ? regIntInterval.test(conditionValueWithNoSpace)
+          : regInt.test(conditionValueWithNoSpace);
         break;
         break;
       case 'float':
       case 'float':
         isLegal = isIn
         isLegal = isIn
-          ? regFloatInterval.test(conditionValue)
-          : regFloat.test(conditionValue);
+          ? regFloatInterval.test(conditionValueWithNoSpace)
+          : regFloat.test(conditionValueWithNoSpace);
         break;
         break;
       default:
       default:
         isLegal = false;
         isLegal = false;

+ 20 - 4
client/src/components/advancedSearch/Filter.tsx

@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react';
+import { forwardRef, useState, useEffect, useImperativeHandle } from 'react';
 import {
 import {
   makeStyles,
   makeStyles,
   Theme,
   Theme,
@@ -12,10 +12,11 @@ import { FilterProps, ConditionData } from './Types';
 import { generateIdByHash } from '../../utils/Common';
 import { generateIdByHash } from '../../utils/Common';
 import CustomButton from '../customButton/CustomButton';
 import CustomButton from '../customButton/CustomButton';
 
 
-const Filter = function Filter(props: FilterProps) {
+const Filter = forwardRef((props: FilterProps, ref) => {
   const {
   const {
     title = 'title',
     title = 'title',
     showTitle = true,
     showTitle = true,
+    showTooltip = true,
     className = '',
     className = '',
     filterDisabled = false,
     filterDisabled = false,
     tooltipPlacement = 'top',
     tooltipPlacement = 'top',
@@ -245,6 +246,7 @@ const Filter = function Filter(props: FilterProps) {
     setInitConditions(flatConditions);
     setInitConditions(flatConditions);
     setOpen(false);
     setOpen(false);
   };
   };
+  // Only reset current conditions. Former conditions are remained.
   const handleReset = () => {
   const handleReset = () => {
     setFilteredFlatConditions([
     setFilteredFlatConditions([
       {
       {
@@ -256,9 +258,23 @@ const Filter = function Filter(props: FilterProps) {
       },
       },
     ]);
     ]);
   };
   };
+  // Reset all conditions(both current and former).
+  const handleHardReset = () => {
+    setInitConditions([]);
+    handleReset();
+  };
   const handleFallback = () => {
   const handleFallback = () => {
     setFilteredFlatConditions(initConditions);
     setFilteredFlatConditions(initConditions);
   };
   };
+  // Expose func
+  // useImperativeHandle customizes the instance value that is exposed to parent components when using ref.
+  // https://reactjs.org/docs/hooks-reference.html#useimperativehandle
+  useImperativeHandle(ref, () => ({
+    // Expose handleHardReset, parent components can call it by `ref.current.getReset()`
+    getReset() {
+      handleHardReset();
+    },
+  }));
 
 
   return (
   return (
     <>
     <>
@@ -271,7 +287,7 @@ const Filter = function Filter(props: FilterProps) {
           <FilterListIcon />
           <FilterListIcon />
           {showTitle ? title : ''}
           {showTitle ? title : ''}
         </CustomButton>
         </CustomButton>
-        {initConditions.length > 0 && (
+        {showTooltip && initConditions.length > 0 && (
           <Tooltip
           <Tooltip
             arrow
             arrow
             interactive
             interactive
@@ -304,7 +320,7 @@ const Filter = function Filter(props: FilterProps) {
       </div>
       </div>
     </>
     </>
   );
   );
-};
+});
 
 
 Filter.displayName = 'AdvancedFilter';
 Filter.displayName = 'AdvancedFilter';
 
 

+ 2 - 1
client/src/components/advancedSearch/Types.ts

@@ -69,8 +69,9 @@ export interface FilterProps {
   className?: string;
   className?: string;
   title: string;
   title: string;
   showTitle?: boolean;
   showTitle?: boolean;
+  showTooltip?: boolean;
   filterDisabled?: boolean;
   filterDisabled?: boolean;
-  others?: object;
+  others?: { [key: string]: any };
   onSubmit: (data: any) => void;
   onSubmit: (data: any) => void;
   tooltipPlacement?: 'left' | 'right' | 'bottom' | 'top';
   tooltipPlacement?: 'left' | 'right' | 'bottom' | 'top';
   fields: Field[];
   fields: Field[];

+ 1 - 1
client/src/components/grid/Types.ts

@@ -41,7 +41,7 @@ export type ToolBarConfig = Partial<TableSwitchType> &
     // when disabled "disabledTooltip" will replace "tooltip"
     // when disabled "disabledTooltip" will replace "tooltip"
     disabledTooltip?: string;
     disabledTooltip?: string;
     hidden?: boolean;
     hidden?: boolean;
-    type?: 'iconBtn' | 'buttton' | 'switch' | 'select' | 'groupSelect';
+    type?: 'iconBtn' | 'button' | 'switch' | 'select' | 'groupSelect';
     position?: 'right' | 'left';
     position?: 'right' | 'left';
     component?: ReactElement;
     component?: ReactElement;
     btnVariant?: 'contained' | 'outlined' | 'text';
     btnVariant?: 'contained' | 'outlined' | 'text';

+ 1 - 1
client/src/http/BaseModel.ts

@@ -79,7 +79,7 @@ export default class BaseModel {
     return res.data;
     return res.data;
   }
   }
 
 
-  static async vectorSearch(options: updateParamsType) {
+  static async query(options: updateParamsType) {
     const { path, data } = options;
     const { path, data } = options;
     const res = await http.post(path, data);
     const res = await http.post(path, data);
     return res.data.data;
     return res.data.data;

+ 9 - 1
client/src/http/Collection.ts

@@ -2,6 +2,7 @@ import { ChildrenStatusType } from '../components/status/Types';
 import { CollectionView, InsertDataParam } from '../pages/collections/Types';
 import { CollectionView, InsertDataParam } from '../pages/collections/Types';
 import { Field } from '../pages/schema/Types';
 import { Field } from '../pages/schema/Types';
 import { VectorSearchParam } from '../pages/seach/Types';
 import { VectorSearchParam } from '../pages/seach/Types';
+import { QueryParam } from '../pages/query/Types';
 import { IndexState, ShowCollectionsType } from '../types/Milvus';
 import { IndexState, ShowCollectionsType } from '../types/Milvus';
 import { formatNumber } from '../utils/Common';
 import { formatNumber } from '../utils/Common';
 import BaseModel from './BaseModel';
 import BaseModel from './BaseModel';
@@ -85,7 +86,7 @@ export class CollectionHttp extends BaseModel implements CollectionView {
   }
   }
 
 
   static vectorSearchData(collectionName: string, params: VectorSearchParam) {
   static vectorSearchData(collectionName: string, params: VectorSearchParam) {
-    return super.vectorSearch({
+    return super.query({
       path: `${this.COLLECTIONS_URL}/${collectionName}/search`,
       path: `${this.COLLECTIONS_URL}/${collectionName}/search`,
       data: params,
       data: params,
     });
     });
@@ -98,6 +99,13 @@ export class CollectionHttp extends BaseModel implements CollectionView {
     });
     });
   }
   }
 
 
+  static queryData(collectionName: string, params: QueryParam) {
+    return super.query({
+      path: `${this.COLLECTIONS_URL}/${collectionName}/query`,
+      data: params,
+    });
+  }
+
   get _autoId() {
   get _autoId() {
     return this.autoID;
     return this.autoID;
   }
   }

+ 1 - 0
client/src/i18n/cn/button.ts

@@ -16,6 +16,7 @@ const btnTrans = {
   previous: 'Previous',
   previous: 'Previous',
   done: 'Done',
   done: 'Done',
   vectorSearch: 'Vector search',
   vectorSearch: 'Vector search',
+  query: 'Query',
 };
 };
 
 
 export default btnTrans;
 export default btnTrans;

+ 3 - 0
client/src/i18n/cn/collection.ts

@@ -58,6 +58,9 @@ const collectionTrans = {
   // collection tabs
   // collection tabs
   partitionTab: 'Partitions',
   partitionTab: 'Partitions',
   schemaTab: 'Schema',
   schemaTab: 'Schema',
+  queryTab: 'Data Query',
+  startTip: 'Start your data query',
+  exprPlaceHolder: 'Please enter your query by using advanced filter ->',
 };
 };
 
 
 export default collectionTrans;
 export default collectionTrans;

+ 1 - 0
client/src/i18n/en/button.ts

@@ -16,6 +16,7 @@ const btnTrans = {
   previous: 'Previous',
   previous: 'Previous',
   done: 'Done',
   done: 'Done',
   vectorSearch: 'Vector search',
   vectorSearch: 'Vector search',
+  query: 'Query',
 };
 };
 
 
 export default btnTrans;
 export default btnTrans;

+ 3 - 0
client/src/i18n/en/collection.ts

@@ -58,6 +58,9 @@ const collectionTrans = {
   // collection tabs
   // collection tabs
   partitionTab: 'Partitions',
   partitionTab: 'Partitions',
   schemaTab: 'Schema',
   schemaTab: 'Schema',
+  queryTab: 'Data Query',
+  startTip: 'Start your data query',
+  exprPlaceHolder: 'Please enter your query by using advanced filter ->',
 };
 };
 
 
 export default collectionTrans;
 export default collectionTrans;

+ 5 - 0
client/src/pages/collections/Collection.tsx

@@ -8,6 +8,7 @@ import { useHistory, useLocation, useParams } from 'react-router-dom';
 import { useMemo } from 'react';
 import { useMemo } from 'react';
 import { parseLocationSearch } from '../../utils/Format';
 import { parseLocationSearch } from '../../utils/Format';
 import Schema from '../schema/Schema';
 import Schema from '../schema/Schema';
+import Query from '../query/Query';
 
 
 enum TAB_EMUM {
 enum TAB_EMUM {
   'schema',
   'schema',
@@ -39,6 +40,10 @@ const Collection = () => {
   };
   };
 
 
   const tabs: ITab[] = [
   const tabs: ITab[] = [
+    {
+      label: collectionTrans('queryTab'),
+      component: <Query collectionName={collectionName} />,
+    },
     {
     {
       label: collectionTrans('schemaTab'),
       label: collectionTrans('schemaTab'),
       component: <Schema collectionName={collectionName} />,
       component: <Schema collectionName={collectionName} />,

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

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

+ 192 - 0
client/src/pages/query/Query.tsx

@@ -0,0 +1,192 @@
+import { FC, useEffect, useState, useRef, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import EmptyCard from '../../components/cards/EmptyCard';
+import icons from '../../components/icons/Icons';
+import CustomButton from '../../components/customButton/CustomButton';
+import MilvusGrid from '../../components/grid/Grid';
+import { getQueryStyles } from './Styles';
+import Filter from '../../components/advancedSearch';
+import { CollectionHttp } from '../../http/Collection';
+import { FieldHttp } from '../../http/Field';
+import { usePaginationHook } from '../../hooks/Pagination';
+import CopyButton from '../../components/advancedSearch/CopyButton';
+
+const Query: FC<{
+  collectionName: string;
+}> = ({ collectionName }) => {
+  const [fields, setFields] = useState<any[]>([]);
+  const [expression, setExpression] = useState('');
+  const [tableLoading, setTableLoading] = useState<any>();
+  const [queryResult, setQueryResult] = useState<any>();
+
+  const VectorSearchIcon = icons.vectorSearch;
+  const ResetIcon = icons.refresh;
+
+  const { t: searchTrans } = useTranslation('search');
+  const { t: collectionTrans } = useTranslation('collection');
+  const { t: btnTrans } = useTranslation('btn');
+  const { t: commonTrans } = useTranslation();
+  const copyTrans = commonTrans('copy');
+
+  const classes = getQueryStyles();
+
+  // Format result list
+  const queryResultMemo = useMemo(
+    () =>
+      queryResult?.map((resultItem: { [key: string]: any }) => {
+        // Iterate resultItem keys, then format vector(array) items.
+        const tmp = Object.keys(resultItem).reduce(
+          (prev: { [key: string]: any }, item: string) => {
+            if (Array.isArray(resultItem[item])) {
+              const list2Str = `[${resultItem[item]}]`;
+              prev[item] = (
+                <div className={classes.vectorTableCell}>
+                  <div>{list2Str}</div>
+                  <CopyButton
+                    label={copyTrans.label}
+                    value={list2Str}
+                    className={classes.copyBtn}
+                  />
+                </div>
+              );
+            } else {
+              prev[item] = resultItem[item];
+            }
+            return prev;
+          },
+          {}
+        );
+        return tmp;
+      }),
+    [queryResult, classes.vectorTableCell, classes.copyBtn, copyTrans.label]
+  );
+
+  const {
+    pageSize,
+    handlePageSize,
+    currentPage,
+    handleCurrentPage,
+    total,
+    data: result,
+    order,
+    orderBy,
+    handleGridSort,
+  } = usePaginationHook(queryResultMemo || []);
+
+  const handlePageChange = (e: any, page: number) => {
+    handleCurrentPage(page);
+  };
+
+  const getFields = async (collectionName: string) => {
+    const schemaList = await FieldHttp.getFields(collectionName);
+    const nameList = schemaList.map(i => ({
+      name: i.name,
+      type: i.data_type.includes('Int') ? 'int' : 'float',
+    }));
+    setFields(nameList);
+  };
+
+  // Get fields at first or collection name changed.
+  useEffect(() => {
+    collectionName && getFields(collectionName);
+  }, [collectionName]);
+
+  const filterRef = useRef();
+
+  const handleFilterReset = () => {
+    const currentFilter: any = filterRef.current;
+    currentFilter?.getReset();
+    setExpression('');
+    setTableLoading(null);
+    setQueryResult(null);
+  };
+  const handleFilterSubmit = (expression: string) => {
+    setExpression(expression);
+    setQueryResult(null);
+  };
+  const handleQuery = async () => {
+    setTableLoading(true);
+    try {
+      const res = await CollectionHttp.queryData(collectionName, {
+        expr: expression,
+        output_fields: fields.map(i => i.name),
+      });
+      const result = res.data;
+      setQueryResult(result);
+    } catch (err) {
+      setQueryResult([]);
+    } finally {
+      setTableLoading(false);
+    }
+  };
+
+  return (
+    <div className={classes.root}>
+      <div className={classes.toolbar}>
+        <div className="left">
+          <div>{`${
+            expression || collectionTrans('exprPlaceHolder')
+          }`}</div>
+          <Filter
+            ref={filterRef}
+            title="Advanced Filter"
+            fields={fields}
+            filterDisabled={false}
+            onSubmit={handleFilterSubmit}
+            showTitle={false}
+            showTooltip={false}
+          />
+        </div>
+        <div className="right">
+          <CustomButton className="btn" onClick={handleFilterReset}>
+            <ResetIcon classes={{ root: 'icon' }} />
+            {btnTrans('reset')}
+          </CustomButton>
+          <CustomButton
+            variant="contained"
+            disabled={!expression}
+            onClick={() => handleQuery()}
+          >
+            {btnTrans('query')}
+          </CustomButton>
+        </div>
+      </div>
+      {tableLoading || queryResult?.length ? (
+        <MilvusGrid
+          toolbarConfigs={[]}
+          colDefinitions={fields.map(i => ({
+            id: i.name,
+            align: 'left',
+            disablePadding: false,
+            label: i.name,
+          }))}
+          primaryKey={fields.find(i => i.is_primary_key)?.name}
+          openCheckBox={false}
+          isLoading={!!tableLoading}
+          rows={result}
+          rowCount={total}
+          page={currentPage}
+          onChangePage={handlePageChange}
+          rowsPerPage={pageSize}
+          setRowsPerPage={handlePageSize}
+          orderBy={orderBy}
+          order={order}
+          handleSort={handleGridSort}
+        />
+      ) : (
+        <EmptyCard
+          wrapperClass={`page-empty-card ${classes.emptyCard}`}
+          icon={<VectorSearchIcon />}
+          text={
+            queryResult?.length === 0
+              ? searchTrans('empty')
+              : collectionTrans('startTip')
+          }
+        />
+      )}
+    </div>
+  );
+};
+
+export default Query;

+ 51 - 0
client/src/pages/query/Styles.ts

@@ -0,0 +1,51 @@
+import { makeStyles, Theme } from '@material-ui/core';
+
+export const getQueryStyles = makeStyles((theme: Theme) => ({
+  root: {
+    display: 'flex',
+    flexDirection: 'column',
+    height: '100%',
+  },
+  emptyCard: {
+    height: '100%',
+    borderRadius: theme.spacing(0, 0, 0.5, 0.5),
+    boxShadow: 'none',
+  },
+  toolbar: {
+    display: 'flex',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    backgroundColor: 'white',
+    padding: theme.spacing(1, 2),
+    gap: theme.spacing(2),
+    borderRadius: theme.spacing(0.5, 0.5, 0, 0),
+
+    '& .left': {
+      display: 'flex',
+      justifyContent: 'space-between',
+      alignItems: 'center',
+      width: 'calc(100% - 206px)',
+      padding: theme.spacing(0, 0, 0, 2),
+      fontSize: theme.spacing(2),
+      backgroundColor: '#F9F9F9',
+    },
+
+    '& .right': {
+      display: 'flex',
+      justifyContent: 'space-between',
+      alignItems: 'center',
+      gap: theme.spacing(2),
+    },
+  },
+  vectorTableCell: {
+    '& >div': {
+      maxWidth: theme.spacing(50),
+      overflow: 'hidden',
+      textOverflow: 'ellipsis',
+    },
+    display: 'flex',
+    flexDirection: 'row',
+    alignItems: 'center',
+  },
+  copyBtn: {},
+}));

+ 5 - 0
client/src/pages/query/Types.ts

@@ -0,0 +1,5 @@
+export interface QueryParam {
+  expr: string;
+  partitions_names?: string[];
+  output_fields?: string[];
+}

+ 29 - 4
express/package.json

@@ -5,14 +5,21 @@
   "license": "MIT",
   "license": "MIT",
   "dependencies": {
   "dependencies": {
     "@zilliz/milvus2-sdk-node": "^1.0.19",
     "@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",
     "cors": "^2.8.5",
     "cross-env": "^7.0.3",
     "cross-env": "^7.0.3",
     "express": "^4.17.1",
     "express": "^4.17.1",
     "glob": "^7.2.0",
     "glob": "^7.2.0",
     "helmet": "^4.6.0",
     "helmet": "^4.6.0",
+    "morgan": "^1.10.0",
     "node-cron": "^3.0.0",
     "node-cron": "^3.0.0",
     "rimraf": "^3.0.2",
     "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"
   },
   },
   "jest": {
   "jest": {
     "testEnvironment": "node",
     "testEnvironment": "node",
@@ -31,6 +38,8 @@
     "coverageDirectory": "../coverage/"
     "coverageDirectory": "../coverage/"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "@types/swagger-jsdoc": "^6.0.1",
+    "@types/chalk": "^2.2.0",
     "@types/cors": "^2.8.12",
     "@types/cors": "^2.8.12",
     "@types/express": "^4.17.13",
     "@types/express": "^4.17.13",
     "@types/glob": "^7.2.0",
     "@types/glob": "^7.2.0",
@@ -42,6 +51,9 @@
     "jest": "^27.3.1",
     "jest": "^27.3.1",
     "supertest": "^6.1.6",
     "supertest": "^6.1.6",
     "ts-jest": "^27.0.7",
     "ts-jest": "^27.0.7",
+    "@types/morgan": "^1.9.3",
+    "@types/swagger-ui-express": "^4.1.3",
+    "nodemon": "^2.0.14",
     "ts-node": "^10.4.0",
     "ts-node": "^10.4.0",
     "tslint": "^6.1.3",
     "tslint": "^6.1.3",
     "typescript": "^4.4.4"
     "typescript": "^4.4.4"
@@ -49,8 +61,8 @@
   "scripts": {
   "scripts": {
     "prebuild": "tslint -c tslint.json -p tsconfig.json --fix",
     "prebuild": "tslint -c tslint.json -p tsconfig.json --fix",
     "build": "yarn clean && tsc",
     "build": "yarn clean && tsc",
-    "prestart": "yarn build",
-    "start": "node .",
+    "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:plugin": "yarn build && cross-env PLUGIN_DEV=1 node dist/milvus-insight/express/src/app.js",
     "start:prod": "node dist/app.js",
     "start:prod": "node dist/app.js",
     "test": "cross-env NODE_ENV=test jest --passWithNoTests",
     "test": "cross-env NODE_ENV=test jest --passWithNoTests",
@@ -58,5 +70,18 @@
     "test:cov": "cross-env NODE_ENV=test jest --passWithNoTests --coverage",
     "test:cov": "cross-env NODE_ENV=test jest --passWithNoTests --coverage",
     "test:report": "cross-env NODE_ENV=test jest --watchAll=false --coverage --coverageReporters='text-summary'",
     "test:report": "cross-env NODE_ENV=test jest --watchAll=false --coverage --coverageReporters='text-summary'",
     "clean": "rimraf dist"
     "clean": "rimraf dist"
+  },
+  "nodemonConfig": {
+    "ignore": [
+      "**/*.test.ts",
+      "**/*.spec.ts",
+      "build",
+      ".git",
+      "node_modules"
+    ],
+    "watch": [
+      "src"
+    ],
+    "ext": "ts"
   }
   }
-}
+}

+ 23 - 16
express/src/app.ts

@@ -10,12 +10,16 @@ import { router as schemaRouter } from "./schema";
 import { router as cronsRouter } from "./crons";
 import { router as cronsRouter } from "./crons";
 import { pubSub } from "./events";
 import { pubSub } from "./events";
 import {
 import {
-  TransformResInterceptor,
-  LoggingInterceptor,
-  ErrorInterceptor,
-} from "./interceptors";
-import { getDirectories, generateCfgs } from "./utils";
+  TransformResMiddlerware,
+  LoggingMiddleware,
+  ErrorMiddleware,
+} from "./middlewares";
+
+import { getDirectories, getDirectoriesSync, generateCfgs } from "./utils";
 import * as path from "path";
 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 PLUGIN_DEV = process.env?.PLUGIN_DEV;
 const SRC_PLUGIN_DIR = "src/plugins";
 const SRC_PLUGIN_DIR = "src/plugins";
@@ -42,11 +46,10 @@ app.use(
   })
   })
 );
 );
 app.use(express.json({ limit: "150MB" }));
 app.use(express.json({ limit: "150MB" }));
-
 // TransformResInterceptor
 // TransformResInterceptor
-app.use(TransformResInterceptor);
+app.use(TransformResMiddlerware);
 // LoggingInterceptor
 // LoggingInterceptor
-app.use(LoggingInterceptor);
+app.use(LoggingMiddleware);
 
 
 const router = express.Router();
 const router = express.Router();
 const pluginsRouter = express.Router();
 const pluginsRouter = express.Router();
@@ -65,8 +68,8 @@ io.on("connection", (socket: Socket) => {
 
 
 // Read plugin files and start express server
 // Read plugin files and start express server
 // Import all plguins under "src/plugins"
 // Import all plguins under "src/plugins"
-getDirectories(SRC_PLUGIN_DIR, async (dirErr: Error, dirRes: [string]) => {
-  const cfgs: any = [];
+getDirectories(SRC_PLUGIN_DIR, async (dirErr: Error, dirRes: string[]) => {
+  const cfgs: any[] = [];
   if (dirErr) {
   if (dirErr) {
     console.log("Reading plugin directory Error", dirErr);
     console.log("Reading plugin directory Error", dirErr);
   } else {
   } else {
@@ -74,18 +77,18 @@ getDirectories(SRC_PLUGIN_DIR, async (dirErr: Error, dirRes: [string]) => {
   }
   }
   // If under plugin dev mode, import all plugins under "../../src/*/server"
   // If under plugin dev mode, import all plugins under "../../src/*/server"
   if (PLUGIN_DEV) {
   if (PLUGIN_DEV) {
-    await getDirectories(
+    getDirectoriesSync(
       DEV_PLUGIN_DIR,
       DEV_PLUGIN_DIR,
-      (devDirErr: Error, devDirRes: [string]) => {
+      (devDirErr: Error, devDirRes: string[]) => {
         if (devDirErr) {
         if (devDirErr) {
-          console.log("Reading plugin directory Error", dirErr);
+          console.log("Reading dev plugin directory Error", devDirErr);
         } else {
         } else {
           generateCfgs(cfgs, devDirRes, false);
           generateCfgs(cfgs, devDirRes, false);
         }
         }
       }
       }
     );
     );
   }
   }
-  console.log(cfgs);
+  console.log("======/api/plugins configs======", cfgs);
   cfgs.forEach(async (cfg: any) => {
   cfgs.forEach(async (cfg: any) => {
     const { api: pluginPath, componentPath } = cfg;
     const { api: pluginPath, componentPath } = cfg;
     if (!pluginPath) return;
     if (!pluginPath) return;
@@ -111,6 +114,10 @@ getDirectories(SRC_PLUGIN_DIR, async (dirErr: Error, dirRes: [string]) => {
 
 
   // Return client build files
   // Return client build files
   app.use(express.static("build"));
   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
   // handle every other route with index.html, which will contain
   // a script tag to your application's JavaScript file(s).
   // a script tag to your application's JavaScript file(s).
   app.get("*", (request, response) => {
   app.get("*", (request, response) => {
@@ -118,9 +125,9 @@ getDirectories(SRC_PLUGIN_DIR, async (dirErr: Error, dirRes: [string]) => {
   });
   });
 
 
   // ErrorInterceptor
   // ErrorInterceptor
-  app.use(ErrorInterceptor);
+  app.use(ErrorMiddleware);
   // start server
   // start server
   server.listen(PORT, () => {
   server.listen(PORT, () => {
-    console.log(`Server started on port ${PORT} :)`);
+    console.log(chalk.green.bold(`Insight Server started on port ${PORT} :)`));
   });
   });
 });
 });

+ 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);
+    }
+  }
+}

+ 12 - 1
express/src/collections/collections.service.ts

@@ -19,7 +19,8 @@ import {
   DropAliasReq,
   DropAliasReq,
   ShowCollectionsReq,
   ShowCollectionsReq,
   ShowCollectionsType,
   ShowCollectionsType,
-} from '@zilliz/milvus2-sdk-node/dist/milvus/types/Collection';
+} from "@zilliz/milvus2-sdk-node/dist/milvus/types/Collection";
+import { QueryDto } from "./dto";
 
 
 export class CollectionsService {
 export class CollectionsService {
   constructor(private milvusService: MilvusService) {}
   constructor(private milvusService: MilvusService) {}
@@ -108,6 +109,16 @@ export class CollectionsService {
     return res;
     return res;
   }
   }
 
 
+  async query(
+    data: {
+      collection_name: string;
+    } & QueryDto
+  ) {
+    const res = await this.dataManager.query(data);
+    throwErrorFromSDK(res.status);
+    return res;
+  }
+
   /**
   /**
    * We do not throw error for this.
    * We do not throw error for this.
    * Because if collection dont have index, it will throw error.
    * Because if collection dont have index, it will throw error.

+ 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 - 141
express/src/collections/index.ts

@@ -1,144 +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/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);
+    }
+  }
+}

+ 4 - 3
express/src/crons/crons.service.ts

@@ -7,13 +7,14 @@ export class CronsService {
   constructor(
   constructor(
     private collectionService: CollectionsService,
     private collectionService: CollectionsService,
     private schedulerRegistry: SchedulerRegistry
     private schedulerRegistry: SchedulerRegistry
-  ) {
-    this.getCollections(WS_EVENTS.COLLECTION + "");
-  }
+  ) {}
 
 
   async toggleCronJobByName(data: { name: string; type: WS_EVENTS_TYPE }) {
   async toggleCronJobByName(data: { name: string; type: WS_EVENTS_TYPE }) {
     const { name, type } = data;
     const { name, type } = data;
     const cronJobEntity = this.schedulerRegistry.getCronJob(name);
     const cronJobEntity = this.schedulerRegistry.getCronJob(name);
+    if (!cronJobEntity && Number(type) === WS_EVENTS_TYPE.START) {
+      return this.getCollections(WS_EVENTS.COLLECTION);
+    }
     return Number(type) === WS_EVENTS_TYPE.STOP
     return Number(type) === WS_EVENTS_TYPE.STOP
       ? cronJobEntity.stop()
       ? cronJobEntity.stop()
       : cronJobEntity.start();
       : cronJobEntity.start();

+ 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 };
 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;

+ 0 - 90
express/src/interceptors/index.ts

@@ -1,90 +0,0 @@
-import { Request, Response, NextFunction, Errback } from "express";
-
-// TransformResInterceptor
-export const TransformResInterceptor = (
-  req: Request,
-  res: Response,
-  next: NextFunction
-) => {
-  const oldSend = res.json;
-  res.json = (data) => {
-    // console.log(data); // do something with the data
-    const statusCode = data?.statusCode;
-    const message = data?.message;
-    const error = data?.error;
-    res.json = oldSend; // set function back to avoid the 'double-send'
-    if (statusCode || message || error) {
-      return res.json({ statusCode, message, error });
-    }
-    return res.json({ data, statusCode: 200 }); // just call as normal with data
-  };
-  next();
-};
-
-const getDurationInMilliseconds = (start: any) => {
-  const NS_PER_SEC = 1e9;
-  const NS_TO_MS = 1e6;
-  const diff = process.hrtime(start);
-
-  return (diff[0] * NS_PER_SEC + diff[1]) / NS_TO_MS;
-};
-
-/**
- * Add spent time looger when accessing milvus.
- */
-export const LoggingInterceptor = (
-  req: Request,
-  res: Response,
-  next: NextFunction
-) => {
-  console.log(`${req.method} ${req.originalUrl} [STARTED]`);
-  const start = process.hrtime();
-  const { ip = "", method = "", originalUrl = "", headers = {} } = req;
-  const ua = headers["user-agent"] || "";
-
-  res.on("finish", () => {
-    const durationInMilliseconds = getDurationInMilliseconds(start);
-    console.log(
-      `${req.method} ${
-        req.originalUrl
-      } [FINISHED] ${durationInMilliseconds.toLocaleString()} ms`
-    );
-  });
-
-  res.on("close", () => {
-    const durationInMilliseconds = getDurationInMilliseconds(start);
-    const { statusCode = "" } = res;
-    // TODO: Need some special log instead of console.log
-    console.log(
-      `${req.method} ${
-        req.originalUrl
-      } [CLOSED] ${durationInMilliseconds.toLocaleString()} ms ip:${ip} ua:${ua} status:${statusCode}`
-    );
-  });
-
-  next();
-};
-
-/**
- * Handle error in here.
- * Normally depend on status which from milvus service.
- */
-export const ErrorInterceptor = (
-  err: Error,
-  req: Request,
-  res: Response,
-  next: NextFunction
-) => {
-  console.log("---error interceptor---\n%s", err);
-  // Boolean property that indicates if the app sent HTTP headers for the response.
-  // Here to prevent sending response after header has been sent.
-  if (res.headersSent) {
-    return next(err);
-  }
-  if (err) {
-    res
-      .status(500)
-      .json({ message: `${err}`, error: "Bad Request", statusCode: 500 });
-  }
-  next();
-};

+ 62 - 0
express/src/middlewares/index.ts

@@ -0,0 +1,62 @@
+import { Request, Response, NextFunction, Errback } from "express";
+import morgan from "morgan";
+import chalk from "chalk";
+
+export const TransformResMiddlerware = (
+  req: Request,
+  res: Response,
+  next: NextFunction
+) => {
+  const oldSend = res.json;
+  res.json = (data) => {
+    // console.log(data); // do something with the data
+    const statusCode = data?.statusCode;
+    const message = data?.message;
+    const error = data?.error;
+    res.json = oldSend; // set function back to avoid the 'double-send'
+    if (statusCode || message || error) {
+      return res.json({ statusCode, message, error });
+    }
+    return res.json({ data, statusCode: 200 }); // just call as normal with data
+  };
+  next();
+};
+
+/**
+ * Handle error in here.
+ * Normally depend on status which from milvus service.
+ */
+export const ErrorMiddleware = (
+  err: Error,
+  req: Request,
+  res: Response,
+  next: NextFunction
+) => {
+  console.log(chalk.blue.bold(req.method, req.url), chalk.red.bold(err));
+  // Boolean property that indicates if the app sent HTTP headers for the response.
+  // Here to prevent sending response after header has been sent.
+  if (res.headersSent) {
+    return next(err);
+  }
+  if (err) {
+    res
+      .status(500)
+      .json({ message: `${err}`, error: "Bad Request", statusCode: 500 });
+  }
+  next();
+};
+
+export const LoggingMiddleware = morgan((tokens, req, res) => {
+  return [
+    "\n",
+    chalk.blue.bold(tokens.method(req, res)),
+    chalk.magenta.bold(tokens.status(req, res)),
+    chalk.green.bold(tokens.url(req, res)),
+    chalk.green.bold(tokens["response-time"](req, res) + " ms"),
+    chalk.green.bold("@ " + tokens.date(req, res)),
+    chalk.yellow(tokens["remote-addr"](req, res)),
+    chalk.hex("#fffa65").bold("from " + tokens.referrer(req, res)),
+    chalk.hex("#1e90ff")(tokens["user-agent"](req, res)),
+    "\n",
+  ].join(" ");
+});

+ 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 };
 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 };
 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 };
 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;
+};

+ 16 - 3
express/src/utils/index.ts

@@ -4,14 +4,27 @@ import fs from "fs";
 // Utils: read files under specified directories
 // Utils: read files under specified directories
 export const getDirectories = (
 export const getDirectories = (
   src: string,
   src: string,
-  callback: (err: Error, res: [string]) => void
+  callback: (err: Error, res: string[]) => void
 ) => {
 ) => {
   glob(src + "/**/*", callback);
   glob(src + "/**/*", callback);
 };
 };
 
 
+// sync: read files under specified directories
+export const getDirectoriesSync = (
+  src: string,
+  callback: (err: Error, res: string[]) => void
+) => {
+  try {
+    const results = glob.sync(src + "/**/*");
+    callback(undefined, results);
+  } catch (error) {
+    callback(error, []);
+  }
+};
+
 export const generateCfgs = (
 export const generateCfgs = (
-  cfgs: [any],
-  dirRes: [string],
+  cfgs: any[],
+  dirRes: string[],
   isSrcPlugin: boolean = true
   isSrcPlugin: boolean = true
 ) => {
 ) => {
   dirRes.forEach((item: string) => {
   dirRes.forEach((item: string) => {

+ 2 - 0
express/tsconfig.json

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

+ 12 - 4
express/tslint.json

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

File diff suppressed because it is too large
+ 322 - 452
express/yarn.lock


Some files were not shown because too many files changed in this diff