Repository: spaship/spaship
Pull request #1627 (2022-04-13 11:11:09)

feat(api) : operator integration in API
<!--
Follow the steps to create the PR:

Subject: "feat/fix/docs(#issue_id): Operator Integration in API"
Assignees: "Developers"
Projects: ""
-->

1. Operator integrated into API with bare metal
2. Script APIs configured

## Closes / Fixes / Resolves

1. Fixes in Activities Stream
2. Fixes in Analytics APIs

<!-- Comman separated list of GitHub Issue ID(s) -->

## Explain the feature/fix

<!-- Provide a clear explaination of the feature/fix implemented -->

## Does this PR introduce a breaking change

<!-- Yes/No -->

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications here. -->

## Screenshot(s)

<!-- If applicable, add screenshots to help explain your problem. -->

<details>
<summary>View Screenshots</summary>

<!-- Add your screenshot(s) below this line -->

</details>

### Ready-for-merge Checklist

- [ ] Expected files: all files in this pull request are related to one feature request or issue (no stragglers)?
- [ ] Does the change have appropriate unit tests?
- [ ] Did you update or add any necessary documentation (README.md, WHY.md, etc.)?
- [ ] Was this feature demo'd and the design review approved?



Patch:
diff --git a/packages/api/app.js b/packages/api/app.js
index 4ef98702..f6fa36c7 100644
--- a/packages/api/app.js
+++ b/packages/api/app.js
@@ -20,8 +20,8 @@ const swaggerDocument = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "open
 const app = new express();
 const consumeSSE = require("./controllers/operatorServices/consumeEvent.js")
 app
-  .use(bodyParser.json())
-  .use(bodyParser.urlencoded({ extended: true }))
+  .use(bodyParser.json({limit: "50mb"}))
+  .use(bodyParser.urlencoded({limit: "50mb", extended: true, parameterLimit:50000}))
   .use(expressSanitizer())
   .use(cors())
   .use(helmet())
diff --git a/packages/api/controllers/application.js b/packages/api/controllers/application.js
index 19aecdc1..3a051c04 100644
--- a/packages/api/controllers/application.js
+++ b/packages/api/controllers/application.js
@@ -10,6 +10,8 @@ const cliActivities = require("../models/cliActivities");
 const { getUserUUID } = require("../utils/requestUtil");
 const { uuid } = require("uuidv4");
 const jwt = require("jsonwebtoken");
+const alias = require("../models/alias");
+const _ = require("lodash");
 
 const axios = require("axios");
 const FormData = require("form-data");
@@ -107,36 +109,48 @@ module.exports.deploy = async (req, res, next) => {
       return;
     }
   }
-
   const userId = getUserUUID(req);
   const name = getNameRequest(req);
   const ref = getRefRequest(req);
   const { path: appPath } = getRequestBody(req);
   const { path: spaArchive } = getPath(req);
+  const property = req.params.propertyName;
+  const env = req.params.env;
+  const validateFileType = req.file.originalname.split(".").pop();
+  if (validateFileType === "zip" || validateFileType === "tgz") {
+    try {
+      await DeployService.deploy({
+        name,
+        spaArchive,
+        appPath,
+        ref,
+        property,
+        env,
+      });
 
-  try {
-    await DeployService.deploy({
-      name,
-      spaArchive,
-      appPath,
-      ref,
-    });
-
-    const application = await Application.findOne({ name, path: appPath });
-    if (application) {
-      await Application.updateOne({ name, path: appPath }, { ref });
-    } else {
-      await Application.create({ name, path: appPath, ref, userId });
+      const application = await Application.findOne({ name, path: appPath });
+      try {
+        if (application) {
+          await Application.updateOne({ name: name, path: appPath }, { ref });
+        } else {
+          await Application.create({ name: name, path: appPath, ref, userId });
+        }
+      } catch (e) {
+        console.log(e);
+      }
+
+      res.status(201).send({
+        name,
+        path: appPath,
+        ref,
+        timestamp: new Date(),
+      });
+    } catch (err) {
+      console.error(`Failed to deploy "${name}" to ${appPath}: ${err}`);
+      next(new DeployError(err));
     }
-    res.status(201).send({
-      name,
-      path: appPath,
-      ref,
-      timestamp: new Date(),
-    });
-  } catch (err) {
-    console.error(`Failed to deploy "${name}" to ${appPath}: ${err}`);
-    next(new DeployError(err));
+  } else {
+    next(new DeployError("Invalid File Type"));
   }
 };
 
@@ -188,7 +202,8 @@ function getPathRequest(req) {
 }
 
 function getRefRequest(req) {
-  const requestBody = req?.body?.ref || {};
+  if (req?.body?.ref == "undefined") return req.params.env;
+  const requestBody = req?.body?.ref;
   return requestBody;
 }
 
@@ -214,5 +229,6 @@ function getDescription(req) {
 }
 
 function getWebPropertyName(req) {
-  return req?.body?.webPropertyName.trim();
-}
\ No newline at end of file
+
+  return req?.body?.webPropertyName || false;
+}
diff --git a/packages/api/controllers/operatorServices/consumeEvent.js b/packages/api/controllers/operatorServices/consumeEvent.js
index 1828e8bc..cfeb61e4 100644
--- a/packages/api/controllers/operatorServices/consumeEvent.js
+++ b/packages/api/controllers/operatorServices/consumeEvent.js
@@ -53,6 +53,7 @@ async function createEventTimeTraceRequest(response) {
     traceId: response.uuid,
     propertyName: response.websiteName,
     envs: response.environmentName,
+    spaName: response?.spaName,
     initialCode: "WEBSITE_CREATE_STARTED",
     finalCode: "WEBSITE_CREATE",
     failure: true,
diff --git a/packages/api/controllers/operatorServices/saveAlias.js b/packages/api/controllers/operatorServices/saveAlias.js
new file mode 100644
index 00000000..360a48af
--- /dev/null
+++ b/packages/api/controllers/operatorServices/saveAlias.js
@@ -0,0 +1,83 @@
+const alias = require("../../models/alias");
+const { uuid } = require("uuidv4");
+
+module.exports = async function saveAlias(req, res) {
+  console.log(req.body);
+  if (req.body?.id) {
+    const updatedResponse = await updateAlias(req);
+    res.send(updatedResponse);
+  }
+  let id = await getGeneratedAliasId();
+  let aliasRequest = await createAliasRequest(id, req);
+  const createdResponse = await createEvent(aliasRequest);
+  res.send(createdResponse);
+};
+
+async function createEvent(aliasRequest) {
+  try {
+    const saveResponse = await aliasRequest.save();
+    return saveResponse;
+  } catch (e) {
+    return { Error: e };
+  }
+}
+
+async function getGeneratedAliasId() {
+  return uuid();
+}
+
+async function createAliasRequest(id, req) {
+  const currentTime = getCurrentTime();
+  return new alias({
+    id: id,
+    property: getPropertyName(req),
+    env: getEnv(req),
+    namespace: getNameSpace(req),
+    type: getType(req),
+    createdAt: currentTime,
+  });
+}
+
+async function updateAlias(req) {
+  const updateResponse = await alias.findOneAndUpdate({ id: req.body?.id }, updateAliasRequest(req), (error, data) => {
+    if (error) {
+      console.log("error");
+    }
+  });
+  return updateResponse;
+}
+
+async function updateAliasRequest(req) {
+  const updateRequest = {
+    property: getPropertyName(req),
+    env: getEnv(req),
+    namespace: getNameSpace(req),
+    type: getType(req),
+    createdAt: getCurrentTime(),
+  };
+  return JSON.parse(JSON.stringify(updateRequest));
+}
+
+function getCurrentTime() {
+  return new Date();
+}
+
+function getPropertyName(req) {
+  return req.body?.property;
+}
+
+function getName(req) {
+  return req.body?.name || "";
+}
+
+function getEnv(req) {
+  return req.body?.env;
+}
+
+function getNameSpace(req) {
+  return req.body?.namespace;
+}
+
+function getType(req) {
+  return req.body?.type;
+}
diff --git a/packages/api/controllers/scripts/eventDataDump.js b/packages/api/controllers/scripts/eventDataDump.js
index fd703805..6ba54400 100644
--- a/packages/api/controllers/scripts/eventDataDump.js
+++ b/packages/api/controllers/scripts/eventDataDump.js
@@ -1,7 +1,7 @@
 const event = require("../../models/event");
 
 const eventDataDump = async (req, res) => {
-  res.status(200).json(await eventDataDumpService(req.sanitize(getDocument(req))));
+  res.status(200).json(await eventDataDumpService(getDocument(req)));
 };
 
 const eventDataDumpService = async (docs) => {
@@ -29,7 +29,6 @@ function createBulkData(docs, currentDate, bulk) {
     docs[i]._id = null;
     if (docs[i].createdAt) {
       docs[i].createdAt = new Date(docs[i].createdAt.$date);
-      console.log(docs[i]);
       bulk.insert(docs[i]);
     }
   }
diff --git a/packages/api/controllers/scripts/eventTimeMatricesDump.js b/packages/api/controllers/scripts/eventTimeMatricesDump.js
index b115780d..ed12ad22 100644
--- a/packages/api/controllers/scripts/eventTimeMatricesDump.js
+++ b/packages/api/controllers/scripts/eventTimeMatricesDump.js
@@ -1,15 +1,15 @@
 const eventTimeTrace = require("../../models/eventTimeTrace");
 
 const eventTimeMatricesDump = async (req, res) => {
-  res.status(200).json(await eventTimeMatricesDumpService(req.sanitize(getDocument(req))));
+  res.status(200).json(await eventTimeMatricesDumpService(getDocument(req)));
 };
 
 const eventTimeMatricesDumpService = async (docs) => {
   try {
     const bulk = await eventTimeTrace.collection.initializeUnorderedBulkOp();
     const currentDate = new Date();
-    createBulkRequest(docs, currentDate, bulk);
-    bulk.execute();
+    await createBulkRequest(docs, currentDate, bulk);
+    await bulk.execute();
     return { Success: "All data successfully updated", "Records Number": getDocumentCounts(docs) };
   } catch (e) {
     console.log(e);
@@ -24,13 +24,12 @@ function getDocument(req) {
   return req?.body || [];
 }
 
-function createBulkRequest(docs, currentDate, bulk) {
+async function createBulkRequest(docs, currentDate, bulk) {
   for (i = 0; i < getDocumentCounts(docs); i += 1) {
     docs[i]._id = null;
     if (docs[i].createdAt && docs[i].completedAt) {
       docs[i].createdAt = new Date(docs[i].createdAt.$date);
       docs[i].completedAt = new Date(docs[i].completedAt.$date);
-      console.log(docs[i]);
       bulk.insert(docs[i]);
     }
   }
diff --git a/packages/api/controllers/scripts/webPropertyDataDump.js b/packages/api/controllers/scripts/webPropertyDataDump.js
index 105ecac7..7bf35fdf 100644
--- a/packages/api/controllers/scripts/webPropertyDataDump.js
+++ b/packages/api/controllers/scripts/webPropertyDataDump.js
@@ -2,7 +2,7 @@ const webProperty = require("../../models/webProperty");
 const mongoose = require("mongoose");
 
 const webPropertyDataDump = async (req, res) => {
-  res.status(200).json(await webPropertyDataDumpService(req.sanitize(getDocument(req))));
+  res.status(200).json(await webPropertyDataDumpService(getDocument(req)));
 };
 
 const webPropertyDataDumpService = async (docs) => {
diff --git a/packages/api/controllers/services/analyticsServiceFilter.js b/packages/api/controllers/services/analyticsServiceFilter.js
index 74c71c34..c1dd86a1 100644
--- a/packages/api/controllers/services/analyticsServiceFilter.js
+++ b/packages/api/controllers/services/analyticsServiceFilter.js
@@ -87,7 +87,6 @@ const analyticsOperations = async (request) => {
           count: "$count",
         }
       );
-      response = response.filter((data) => data.length > 0);
       return response;
     } else if (request?.chart.month == true && getPropertyName(request?.chart)) {
       let response = await getCountByEnvWeeklyChart.getCountByEnvWeeklyChartService(
@@ -106,7 +105,6 @@ const analyticsOperations = async (request) => {
           count: "$count",
         }
       );
-      response = response.filter((data) => data.length > 0);
       return response;
     } else if (getPropertyName(request?.chart) && getSpaName(request?.chart)) {
       return await getCounts.getCountService(
diff --git a/packages/api/controllers/webProperties/getAliasList.js b/packages/api/controllers/webProperties/getAliasList.js
new file mode 100644
index 00000000..673b5419
--- /dev/null
+++ b/packages/api/controllers/webProperties/getAliasList.js
@@ -0,0 +1,11 @@
+const alias = require("../../models/alias");
+
+module.exports = async function getAliasList(req,res, next) {
+  try {
+    const response = await alias.find();
+    if (response.length === 0) return res.status(200).json({ message: "No data avaliable." });
+    res.status(200).json(response);
+  } catch (e) {
+    next(err);
+  }
+}
\ No newline at end of file
diff --git a/packages/api/controllers/webProperties/saveWebProperty.js b/packages/api/controllers/webProperties/saveWebProperty.js
index 8b29c6ea..7c79d811 100644
--- a/packages/api/controllers/webProperties/saveWebProperty.js
+++ b/packages/api/controllers/webProperties/saveWebProperty.js
@@ -1,7 +1,7 @@
 const webProperty = require('../../models/webProperty')
 const { uuid } = require('uuidv4');
 
-module.exports = async function saveWebProperty(req, res) {
+module.exports = async function saveWebProperty(req, res, next) {
     if (getWebPropertyId(req)) {
         const updatedResponse = await updateWebProperty(req);
         return updatedResponse;
diff --git a/packages/api/controllers/webPropertyServices/getCountByEnvWeeklyChart.js b/packages/api/controllers/webPropertyServices/getCountByEnvWeeklyChart.js
index 6338349e..4ffc9ac8 100644
--- a/packages/api/controllers/webPropertyServices/getCountByEnvWeeklyChart.js
+++ b/packages/api/controllers/webPropertyServices/getCountByEnvWeeklyChart.js
@@ -43,7 +43,17 @@ async function fetchResponse(dateFrame, matchRequest, groupRequest, projectReque
     });
     response.push(responseChart);
   }
-  return response;
+  const mappedResponse = {};
+  for (item of response) {
+    for (obj of item) {
+      if (mappedResponse[obj.envs]) {
+        mappedResponse[obj.envs].push(obj);
+      } else {
+        mappedResponse[obj.envs] = [obj];
+      }
+    }
+  }
+  return mappedResponse;
 }
 
 async function getWeeklyReport(startDate, endDate, matchRequest, groupRequest, projectRequest) {
diff --git a/packages/api/controllers/webPropertyServices/utils/getTimeFrameForPropertyChart.js b/packages/api/controllers/webPropertyServices/utils/getTimeFrameForPropertyChart.js
index e1be027c..e12573a9 100644
--- a/packages/api/controllers/webPropertyServices/utils/getTimeFrameForPropertyChart.js
+++ b/packages/api/controllers/webPropertyServices/utils/getTimeFrameForPropertyChart.js
@@ -14,10 +14,10 @@ const getTimeFrameForPropertyChartService = async (propertyName, weekRange) => {
   }
 };
 
-function createDateFrame(weekRange) {
+function createDateFrame() {
   const dateFrame = [];
   let recentDate = new Date();
-  for (let i = 1; i <= weekRange; i++) {
+  for (let i = 1; i <= 4; i++) {
     const endDate = recentDate;
     const startDate = new Date(recentDate);
     startDate.setDate(recentDate.getDate() - 7);
@@ -41,7 +41,21 @@ async function fetchResponse(dateFrame, propertyName) {
     });
     response.push(responseChart);
   }
-  return response;
+
+
+  const mappedResponse = {};
+  for(item of response){
+    for(obj of item){
+      if(mappedResponse[obj.envs]){
+        mappedResponse[obj.envs].push(obj);
+      }
+      else{
+        mappedResponse[obj.envs] = [obj];
+      }
+    }
+  }
+  
+  return mappedResponse;
 }
 
 async function getWeeklyReport(startDate, endDate, propertyName) {
diff --git a/packages/api/index.js b/packages/api/index.js
index 159ace24..6adbd151 100644
--- a/packages/api/index.js
+++ b/packages/api/index.js
@@ -27,7 +27,7 @@ Configuration:`
 (async () => {
   await db.connect();
   let server = app.listen(config.get("port"), () => {
-    log.info("Server started !");
+    log.info("Server started ! - Docker Test module");
   });
 
   server.timeout = 0; // no timeout
diff --git a/packages/api/models/alias.js b/packages/api/models/alias.js
new file mode 100644
index 00000000..1c3b30cd
--- /dev/null
+++ b/packages/api/models/alias.js
@@ -0,0 +1,36 @@
+const mongoose = require("mongoose");
+
+const alias = new mongoose.Schema({
+  id: {
+    type: String,
+    required: true,
+    unique: true,
+  },
+  property: {
+    type: String,
+    required: false,
+  },
+  name: {
+    type: String,
+    required: false,
+  },
+  env: {
+    type: String,
+    required: false,
+  },
+  namespace: {
+    type: String,
+    required: false,
+  },
+  type: {
+    type: String,
+    required: false,
+  },
+  createdAt: {
+    type: Date,
+    required: false,
+  },
+ 
+});
+
+module.exports = mongoose.model("alias", alias);
diff --git a/packages/api/package.json b/packages/api/package.json
index 0b241919..7ea84171 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -18,7 +18,7 @@
     "spaship-api": "./index.js"
   },
   "scripts": {
-    "start": "[ -f ../../.env ] && source ../../.env; node index.js",
+    "start": "node index.js",
     "dev": "nodemon index.js",
     "test": "../../node_modules/.bin/jest --restoreMocks",
     "get-pubkey": "node scripts/fetchpk.js",
diff --git a/packages/api/routes/applications.js b/packages/api/routes/applications.js
index 8d6da789..09ebfa8e 100644
--- a/packages/api/routes/applications.js
+++ b/packages/api/routes/applications.js
@@ -1,7 +1,6 @@
 const { Router } = require("express");
 const application = require("../controllers/application");
 const upload = require("../utils/multerUtil");
-
 const router = new Router();
 
 /**
@@ -24,7 +23,7 @@ router.post("/", application.post);
 /**
  * Deploy an application
  */
-router.post("/deploy", upload.single("upload"), application.deploy);
+router.post("/deploy/:propertyName/:env", upload.single("upload"), application.deploy);
 
 /**
  * Update an application by name
diff --git a/packages/api/routes/webProperty.js b/packages/api/routes/webProperty.js
index a24a8dc2..c32d6b97 100644
--- a/packages/api/routes/webProperty.js
+++ b/packages/api/routes/webProperty.js
@@ -3,18 +3,26 @@ const produceEvent = require("../controllers/operatorServices/produceEvent");
 const analyzeRepository = require("../controllers/webProperties/analyzeRepository");
 const getSPAList = require("../controllers/webProperties/getSPAList");
 const getWebPropertyList = require("../controllers/webProperties/getWebPropertyList");
+const getAliasList = require("../controllers/webProperties/getAliasList");
 const gitOperations = require("../controllers/webProperties/gitOperations");
+const saveAlias = require("../controllers/operatorServices/saveAlias");
 
 const router = new Router();
 
+
+
 router.post("/", gitOperations);
 
 router.get("/list", getWebPropertyList.getWebPropertyList);
 
-router.get("/getspalist/:webPropertyPropertyName", getSPAList.getSPAList);
+router.get("/getspalist/:webPropertyName", getSPAList.getSPAList);
 
 router.post("/analyze/repository", analyzeRepository);
 
 router.get("/activities/:id", produceEvent);
 
+router.post("/alias", saveAlias);
+
+router.get("/alias/list", getAliasList);
+
 module.exports = router;
diff --git a/packages/api/services/deployService.js b/packages/api/services/deployService.js
index d28b9c7b..db22bc52 100644
--- a/packages/api/services/deployService.js
+++ b/packages/api/services/deployService.js
@@ -6,7 +6,14 @@ const common = require("@spaship/common");
 const decompress = require("decompress");
 const config = require("../config");
 const fileService = require("./fileService");
-
+const alias = require("../models/alias");
+const os = require("os");
+const archiver = require("archiver");
+const cliActivities = require("../models/cliActivities");
+const { uuid } = require("uuidv4");
+const FormData = require("form-data");
+const axios = require("axios");
+const zip = require("zip-a-folder").zip;
 /**
  * detect if the archive was created with `npm pack`.  npm pack creates a
  * tarball with a "package" dir.  we want what's in the package dir but not
@@ -23,14 +30,14 @@ const isNPMPack = async (dir) => {
   return false;
 };
 
-async function deploy({ name, spaArchive, appPath, ref } = {}) {
+async function deploy({ name, spaArchive, appPath, ref, property, env } = {}) {
   // create a dir in the tmp_dir location, but keep the random archive name
   let tmpDir = `${spaArchive}-extracted`;
   await fsp.mkdir(tmpDir);
-
+  console.log("Deployment Started  : ");
+  console.log(tmpDir);
   // extract the archive
   await decompress(spaArchive, tmpDir);
-
   // if the archive was generated by `npm pack`, move into the "package"
   // directory where all the goodies are.
   const shouldRepath = await isNPMPack(tmpDir);
@@ -38,6 +45,41 @@ async function deploy({ name, spaArchive, appPath, ref } = {}) {
     tmpDir = path.join(tmpDir, "package");
   }
 
+  //to check operator
+  console.log("Checking the property is Operator or Baremetal : ");
+  const responseAlias = await alias.find({ property: property, env: env });
+  if (responseAlias?.length > 0) {
+    const operatorAlias = responseAlias[0]._doc;
+    console.log(operatorAlias);
+    if (operatorAlias?.type === "operator") {
+      const zipPath = await createSPAshipTemplateRequest(operatorAlias, name, appPath, tmpDir);
+
+      const formData = new FormData();
+      try {
+        const fileStream = await fs.createReadStream(zipPath);
+        formData.append("spa", fileStream);
+        formData.append("description", property);
+      } catch (err) {
+        console.log(err);
+        return;
+      }
+      formData.append("website", property);
+
+      try {
+        const response = await axios.post(config.get("cli:base_path"), formData, {
+          maxBodyLength: Infinity,
+          headers: formData.getHeaders(),
+        });
+        console.log(response?.data);
+        return;
+      } catch (err) {
+        console.log(err);
+        next(err);
+        return;
+      }
+    }
+  }
+
   let spaConfig = { name, path: appPath };
   let hasYaml = false;
   const yamlFilePath = path.join(tmpDir, "spaship.yaml");
@@ -85,3 +127,46 @@ async function deploy({ name, spaArchive, appPath, ref } = {}) {
 }
 
 module.exports = { deploy };
+
+async function createSPAshipTemplateRequest(operatorAlias, name, appPath, tmpDir) {
+  if (appPath.charAt(0) == "/" && appPath.length === 1) appPath = ".";
+  else if (appPath.charAt(0) == "/") appPath = appPath.substr(1);
+  const spaShipFile = {
+    websiteVersion: "v1",
+    websiteName: operatorAlias.property,
+    name: name,
+    mapping: appPath,
+    environments: [{ name: operatorAlias.namespace, updateRestriction: false, exclude: false }],
+  };
+  console.log("Operator Config : ");
+  console.log(spaShipFile);
+  try {
+    let zipPath;
+    if (await fileExists(path.join(tmpDir, "dist"))) {
+      await fs.writeFileSync(path.join(tmpDir, "dist/.spaship"), JSON.stringify(spaShipFile, null, "\t"));
+      zipPath = path.join(tmpDir, "../SPAship" + uuid() + ".zip");
+      await zip(path.join(tmpDir, "dist"), zipPath);
+    } 
+    else if (await fileExists(path.join(tmpDir, "build"))) {
+      await fs.writeFileSync(path.join(tmpDir, "build/.spaship"), JSON.stringify(spaShipFile, null, "\t"));
+      zipPath = path.join(tmpDir, "../SPAship" + uuid() + ".zip");
+      await zip(path.join(tmpDir, "build"), zipPath);
+    } 
+    else {
+      await fs.writeFileSync(path.join(tmpDir, ".spaship"), JSON.stringify(spaShipFile, null, "\t"));
+      zipPath = path.join(tmpDir, "../SPAship" + uuid() + ".zip");
+      await zip(tmpDir, zipPath);
+    }
+    return zipPath;
+  } catch (err) {
+    console.log(err);
+    throw new Error("Invalid SPA Path in request body.");
+  }
+}
+
+const delay = (millis) =>
+  new Promise((resolve, reject) => {
+    setTimeout((_) => resolve(), millis);
+  });
+
+const fileExists = async (path) => !!(await fs.promises.stat(path).catch((e) => false));


Test patch:
diff --git a/packages/api/controllers/webPropertyServices/getLatestActivities.js b/packages/api/controllers/webPropertyServices/getLatestActivities.js
index ff988966..1377ab7d 100644
--- a/packages/api/controllers/webPropertyServices/getLatestActivities.js
+++ b/packages/api/controllers/webPropertyServices/getLatestActivities.js
@@ -19,6 +19,7 @@ function bindResponse(response) {
   let i = 1;
   response.forEach((item) => {
     item.id = i++;
+    item.createdAt = item.createdAt.toTimeString().substring(0, 8) + " " + item.createdAt.toDateString();
     actvitiesText(item, codeMap);
   });
 }
@@ -45,13 +46,13 @@ async function fetchLatestActivitiesByProperty(matchRequest) {
       },
     },
     {
-      $limit: 10,
+      $limit: 15,
     },
   ]);
 }
 
 function actvitiesText(item, codeMap) {
-  item.latestActivityHead = " has been " + codeMap[item.code] + " ";
+  item.latestActivityHead = " has been " + item.code + " ";
   item.latestActivityTail = " at " + item.createdAt.toString().slice(0, 24) + " in " + item.envs;
 }