From 855d22bc4bbc17f750364d39344b739bdac7582d Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Tue, 14 Jan 2025 17:53:13 +0700
Subject: [PATCH 01/16] update logic of compute api

---
 src/services/api.service.js | 31 ++++++++++++++++++++-----------
 1 file changed, 20 insertions(+), 11 deletions(-)

diff --git a/src/services/api.service.js b/src/services/api.service.js
index a71fcbb..c6e3656 100644
--- a/src/services/api.service.js
+++ b/src/services/api.service.js
@@ -135,22 +135,31 @@ const computeVSSApi = async (modelId) => {
 
   const extendedApis = await ExtendedApi.find({
     model: modelId,
-    isWishlist: true,
   });
   extendedApis.forEach((extendedApi) => {
     try {
       const name = extendedApi.apiName.split('.').slice(1).join('.');
       if (!name) return;
-      ret[mainApi].children[name] = {
-        description: extendedApi.description,
-        type: extendedApi.type || 'branch',
-        id: extendedApi._id,
-        datatype: extendedApi.datatype,
-        name: extendedApi.apiName,
-        isWishlist: extendedApi.isWishlist,
-      };
-      if (extendedApi.unit) {
-        ret[mainApi].children[name].unit = extendedApi.unit;
+
+      // Only add the extended API if it doesn't exist in the current CVI
+      const keys = name.split('.');
+      let current = ret[mainApi].children;
+      for (const key of keys) {
+        if (!current || !current[key]) {
+          ret[mainApi].children[name] = {
+            description: extendedApi.description,
+            type: extendedApi.type || 'branch',
+            id: extendedApi._id,
+            datatype: extendedApi.datatype,
+            name: extendedApi.apiName,
+            isWishlist: extendedApi.isWishlist,
+          };
+          if (extendedApi.unit) {
+            ret[mainApi].children[name].unit = extendedApi.unit;
+          }
+          break;
+        }
+        current = current[key].children;
       }
     } catch (error) {
       logger.warn(`Error while processing extended API ${extendedApi._id} with name ${extendedApi.apiName}: ${error}`);
-- 
GitLab


From 658774920ae22f36c5178bef49727580ab6f59e1 Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Mon, 20 Jan 2025 12:11:50 +0700
Subject: [PATCH 02/16] fix contributedModels error

---
 src/controllers/model.controller.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/controllers/model.controller.js b/src/controllers/model.controller.js
index c7b1735..ac560e3 100644
--- a/src/controllers/model.controller.js
+++ b/src/controllers/model.controller.js
@@ -107,7 +107,7 @@ const listAllModels = catchAsync(async (req, res) => {
 
   const contributedModels = await modelService.queryModels(
     {
-      is_contributor: req.user?.id,
+      is_contributor: req.user?.id || false,
     },
     {
       limit: 1000,
-- 
GitLab


From fe42e8439b052f9a1a6752ff78368fd1298e75fc Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Mon, 20 Jan 2025 13:05:46 +0700
Subject: [PATCH 03/16] update logic of get contributed models

---
 src/controllers/model.controller.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/controllers/model.controller.js b/src/controllers/model.controller.js
index ac560e3..4659085 100644
--- a/src/controllers/model.controller.js
+++ b/src/controllers/model.controller.js
@@ -106,13 +106,13 @@ const listAllModels = catchAsync(async (req, res) => {
   );
 
   const contributedModels = await modelService.queryModels(
+    {},
     {
-      is_contributor: req.user?.id || false,
+      limit: 1000,
     },
     {
-      limit: 1000,
+      is_contributor: req.user?.id || false,
     },
-    {},
     req.user?.id
   );
 
-- 
GitLab


From b9f3d38bdf2593654985b826d41cf58bb325e27a Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Mon, 20 Jan 2025 13:15:08 +0700
Subject: [PATCH 04/16] update contributor models

---
 src/controllers/model.controller.js | 22 ++++++++++++----------
 1 file changed, 12 insertions(+), 10 deletions(-)

diff --git a/src/controllers/model.controller.js b/src/controllers/model.controller.js
index 4659085..69a3f6f 100644
--- a/src/controllers/model.controller.js
+++ b/src/controllers/model.controller.js
@@ -105,16 +105,18 @@ const listAllModels = catchAsync(async (req, res) => {
     req.user?.id
   );
 
-  const contributedModels = await modelService.queryModels(
-    {},
-    {
-      limit: 1000,
-    },
-    {
-      is_contributor: req.user?.id || false,
-    },
-    req.user?.id
-  );
+  const contributedModels = req.user?.id
+    ? await modelService.queryModels(
+        {},
+        {
+          limit: 1000,
+        },
+        {
+          is_contributor: req.user?.id,
+        },
+        req.user?.id
+      )
+    : { results: [] };
 
   const publicReleasedModels = await modelService.queryModels(
     {
-- 
GitLab


From cac930bfc16d80cc2983995b7998cd54068e34ae Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Wed, 22 Jan 2025 17:08:24 +0700
Subject: [PATCH 05/16] add editors choice feature

---
 src/models/prototype.model.js           |  4 ++++
 src/services/prototype.service.js       |  8 +++++++-
 src/services/search.service.js          | 13 +++++++++++--
 src/validations/prototype.validation.js |  2 ++
 4 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/src/models/prototype.model.js b/src/models/prototype.model.js
index e172d3b..0c14d3a 100644
--- a/src/models/prototype.model.js
+++ b/src/models/prototype.model.js
@@ -201,6 +201,10 @@ const prototypeSchema = mongoose.Schema(
     flow: {
       type: mongoose.SchemaTypes.Mixed,
     },
+    editors_choice: {
+      type: Boolean,
+      default: false,
+    },
   },
   {
     timestamps: true,
diff --git a/src/services/prototype.service.js b/src/services/prototype.service.js
index d2c331c..b3efcd9 100644
--- a/src/services/prototype.service.js
+++ b/src/services/prototype.service.js
@@ -66,7 +66,13 @@ const bulkCreatePrototypes = async (userId, prototypes) => {
  * @returns {Promise<QueryResult>}
  */
 const queryPrototypes = async (filter, options) => {
-  const prototypes = await Prototype.paginate(filter, options);
+  const prototypes = await Prototype.paginate(filter, {
+    ...options,
+    // Default sort by editors_choice and createdAt
+    sortBy: options?.sortBy
+      ? ['editors_choice:desc,createdAt:asc', options.sortBy].join(',')
+      : 'editors_choice:desc,createdAt:asc',
+  });
   return prototypes;
 };
 
diff --git a/src/services/search.service.js b/src/services/search.service.js
index baa9caa..0f57486 100644
--- a/src/services/search.service.js
+++ b/src/services/search.service.js
@@ -21,7 +21,13 @@ const search = async (query, options, userId) => {
         },
       ],
     },
-    options
+    {
+      ...options,
+      // Default sort by editors_choice and createdAt
+      sortBy: options?.sortBy
+        ? ['editors_choice:desc,createdAt:asc', options.sortBy].join(',')
+        : 'editors_choice:desc,createdAt:asc',
+    }
   );
 
   return {
@@ -45,7 +51,10 @@ const searchUserByEmail = async (email) => {
  * @param {string} signal
  */
 const searchPrototypesBySignal = async (signal) => {
-  const prototypes = await Prototype.find().select('model_id code name image_file').populate('model_id');
+  const prototypes = await Prototype.find()
+    .select('model_id code name image_file')
+    .sort('-editors_choice createdAt')
+    .populate('model_id');
   return prototypes.filter((prototype) => prototype.code?.includes(signal));
 };
 
diff --git a/src/validations/prototype.validation.js b/src/validations/prototype.validation.js
index 2e83c2c..bf3cf58 100644
--- a/src/validations/prototype.validation.js
+++ b/src/validations/prototype.validation.js
@@ -43,6 +43,7 @@ const bodyValidation = Joi.object().keys({
   partner_logo: Joi.string().allow(''),
   language: Joi.string().default('python'),
   requirements: Joi.string().allow(''),
+  editors_choice: Joi.boolean(),
 });
 
 const createPrototype = {
@@ -116,6 +117,7 @@ const updatePrototype = {
     partner_logo: Joi.string().allow(''),
     requirements: Joi.string().allow(''),
     language: Joi.string(),
+    editors_choice: Joi.boolean(),
     // rated_by: Joi.object().pattern(
     //   /^[0-9a-fA-F]{24}$/,
     //   Joi.object()
-- 
GitLab


From 873a7ebb814bdcd107bbb9bd84038e147fe53010 Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Wed, 5 Feb 2025 15:45:04 +0700
Subject: [PATCH 06/16] fix unnecessary requirement for 'name' field in API
 data

---
 src/services/model.service.js | 19 ++++++++++++++++---
 1 file changed, 16 insertions(+), 3 deletions(-)

diff --git a/src/services/model.service.js b/src/services/model.service.js
index 52bd38c..4d4c9bd 100644
--- a/src/services/model.service.js
+++ b/src/services/model.service.js
@@ -462,7 +462,13 @@ const processApiDataUrl = async (apiDataUrl) => {
       (api, prefix) => {
         for (const [key, value] of Object.entries(api.children || {})) {
           if (value.isWishlist) {
-            extendedApis.push(convertToExtendedApiFormat(value));
+            const name = value?.name || `${prefix}.${key}`;
+            extendedApis.push(
+              convertToExtendedApiFormat({
+                ...value,
+                name,
+              })
+            );
             delete api.children[key];
           }
         }
@@ -491,7 +497,13 @@ const processApiDataUrl = async (apiDataUrl) => {
         data[mainApi],
         (api, prefix) => {
           for (const [key, value] of Object.entries(api.children || {})) {
-            extendedApis.push(convertToExtendedApiFormat(value));
+            const name = value?.name || `${prefix}.${key}`;
+            extendedApis.push(
+              convertToExtendedApiFormat({
+                ...value,
+                name,
+              })
+            );
             delete api.children[key];
           }
         },
@@ -505,7 +517,8 @@ const processApiDataUrl = async (apiDataUrl) => {
 
     return result;
   } catch (error) {
-    logger.warn(`Error in processing api data url: ${error}`);
+    logger.error(`Error in processing api data: ${error}`);
+    throw new ApiError(httpStatus.BAD_REQUEST, `Error in processing api data`);
   }
 };
 
-- 
GitLab


From 38d51c5d8070b4b7f83736bdb50a603a888ea95d Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Wed, 5 Feb 2025 15:52:57 +0700
Subject: [PATCH 07/16] update error message

---
 src/services/model.service.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/services/model.service.js b/src/services/model.service.js
index 4d4c9bd..f9f6ed9 100644
--- a/src/services/model.service.js
+++ b/src/services/model.service.js
@@ -518,7 +518,7 @@ const processApiDataUrl = async (apiDataUrl) => {
     return result;
   } catch (error) {
     logger.error(`Error in processing api data: ${error}`);
-    throw new ApiError(httpStatus.BAD_REQUEST, `Error in processing api data`);
+    throw new ApiError(httpStatus.BAD_REQUEST, `Error in processing api data. Please check content of the file again.`);
   }
 };
 
-- 
GitLab


From 4c5f2f94ca6e8ff02c4270b9e4e12fcafaecfee0 Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Mon, 10 Feb 2025 14:39:01 +0700
Subject: [PATCH 08/16] update logic of computing VSS/Signals Tree

---
 src/services/api.service.js | 45 ++++++++++++++++++++++++++++---------
 1 file changed, 35 insertions(+), 10 deletions(-)

diff --git a/src/services/api.service.js b/src/services/api.service.js
index 7898e8e..780e32e 100644
--- a/src/services/api.service.js
+++ b/src/services/api.service.js
@@ -184,6 +184,25 @@ const getUsedApis = (code, apiList) => {
   }
 };
 
+const ensureParentApiHierarchy = (root, api) => {
+  if (!api) return root;
+
+  let parentNode = root;
+
+  for (const currentApi of api.split('.')) {
+    parentNode.children ??= {};
+
+    parentNode = parentNode.children[currentApi] ??= {
+      type: 'branch',
+      children: {},
+    };
+  }
+
+  parentNode.children ??= {};
+
+  return parentNode;
+};
+
 /**
  *
  * @param {string} modelId
@@ -249,16 +268,22 @@ const computeVSSApi = async (modelId) => {
   }
 
   // Nest parent/children apis
-  const len = Object.keys(ret[mainApi].children).length;
-  for (let i = len - 1; i >= 0; i--) {
-    const key = Object.keys(ret[mainApi].children)[i];
-    const parent = key.split('.').slice(0, -1).join('.');
-    if (parent && ret[mainApi].children[parent]) {
-      ret[mainApi].children[parent].children = ret[mainApi].children[parent].children || {};
-      const childKey = key.replace(`${parent}.`, '');
-      ret[mainApi].children[parent].children[childKey] = ret[mainApi].children[key];
-      delete ret[mainApi].children[key];
-    }
+
+  const keys = Object.keys(ret[mainApi].children).filter((key) => key.includes('.'));
+
+  for (const key of keys) {
+    const parts = key.split('.');
+    const parent = parts.slice(0, -1).join('.');
+    const childKey = parts[parts.length - 1];
+
+    const parentNode = ensureParentApiHierarchy(ret[mainApi], parent);
+
+    parentNode.children[childKey] = {
+      ...ret[mainApi].children[key],
+      children: ret[mainApi].children[key].children || {},
+    };
+
+    delete ret[mainApi].children[key];
   }
 
   return ret;
-- 
GitLab


From 4bbf5eb30d65b4af4f2231ab2bb046f03343fd38 Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Mon, 10 Feb 2025 18:10:36 +0700
Subject: [PATCH 09/16] add route get api detail by api name

---
 src/controllers/model.controller.js | 12 ++++++++++++
 src/routes/v2/model.route.js        |  2 ++
 src/services/api.service.js         | 30 +++++++++++++++++++++++++++++
 3 files changed, 44 insertions(+)

diff --git a/src/controllers/model.controller.js b/src/controllers/model.controller.js
index 69a3f6f..b04b6d3 100644
--- a/src/controllers/model.controller.js
+++ b/src/controllers/model.controller.js
@@ -243,6 +243,17 @@ const getComputedVSSApi = catchAsync(async (req, res) => {
   res.send(data);
 });
 
+const getApiDetail = catchAsync(async (req, res) => {
+  if (!(await permissionService.canAccessModel(req.user?.id, req.params.id))) {
+    throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden');
+  }
+  const api = await apiService.getApiDetail(req.params.id, req.params.apiName);
+  if (!api) {
+    throw new ApiError(httpStatus.NOT_FOUND, 'Api not found');
+  }
+  res.send(api);
+});
+
 module.exports = {
   createModel,
   listModels,
@@ -253,4 +264,5 @@ module.exports = {
   deleteAuthorizedUser,
   getComputedVSSApi,
   listAllModels,
+  getApiDetail,
 };
diff --git a/src/routes/v2/model.route.js b/src/routes/v2/model.route.js
index 4888c65..b54860e 100644
--- a/src/routes/v2/model.route.js
+++ b/src/routes/v2/model.route.js
@@ -58,6 +58,8 @@ router.route('/:id/api').get(
   modelController.getComputedVSSApi
 );
 
+router.route('/:id/api/:apiName').get(auth({ optional: !config.strictAuth }), modelController.getApiDetail);
+
 router
   .route('/:id/permissions')
   .post(
diff --git a/src/services/api.service.js b/src/services/api.service.js
index 780e32e..ba9c195 100644
--- a/src/services/api.service.js
+++ b/src/services/api.service.js
@@ -289,6 +289,34 @@ const computeVSSApi = async (modelId) => {
   return ret;
 };
 
+const traverse = (api, callback, prefix = '') => {
+  if (api.children) {
+    for (const [key, child] of Object.entries(api.children)) {
+      traverse(child, callback, `${prefix}.${key}`);
+    }
+  }
+  callback(api, prefix);
+};
+
+const getApiDetail = async (modelId, apiName) => {
+  const tree = await computeVSSApi(modelId);
+
+  const mainApi = Object.keys(tree)[0] || 'Vehicle';
+  let ret = null;
+
+  traverse(
+    tree[mainApi],
+    (api, prefix) => {
+      if (prefix === apiName || api?.name === apiName || api?.apiName == apiName) {
+        ret = api;
+      }
+    },
+    mainApi
+  );
+
+  return ret;
+};
+
 module.exports.createApi = createApi;
 module.exports.getApi = getApi;
 module.exports.getApiByModelId = getApiByModelId;
@@ -299,3 +327,5 @@ module.exports.getVSSVersion = getVSSVersion;
 module.exports.computeVSSApi = computeVSSApi;
 module.exports.parseCvi = parseCvi;
 module.exports.getUsedApis = getUsedApis;
+module.exports.getApiDetail = getApiDetail;
+module.exports.traverse = traverse;
-- 
GitLab


From 52c5ed99541cdad599042f2e4db7a30e51347a11 Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Mon, 10 Feb 2025 18:27:49 +0700
Subject: [PATCH 10/16] remove empty children in API

---
 src/services/api.service.js | 26 +++++++++++++++++---------
 1 file changed, 17 insertions(+), 9 deletions(-)

diff --git a/src/services/api.service.js b/src/services/api.service.js
index ba9c195..837cbd7 100644
--- a/src/services/api.service.js
+++ b/src/services/api.service.js
@@ -6,6 +6,7 @@ const fs = require('fs');
 const path = require('path');
 const logger = require('../config/logger');
 const { sortObject } = require('../utils/sort');
+const _ = require('lodash');
 
 /**
  *
@@ -203,6 +204,15 @@ const ensureParentApiHierarchy = (root, api) => {
   return parentNode;
 };
 
+const traverse = (api, callback, prefix = '') => {
+  if (api.children) {
+    for (const [key, child] of Object.entries(api.children)) {
+      traverse(child, callback, `${prefix}.${key}`);
+    }
+  }
+  callback(api, prefix);
+};
+
 /**
  *
  * @param {string} modelId
@@ -286,16 +296,14 @@ const computeVSSApi = async (modelId) => {
     delete ret[mainApi].children[key];
   }
 
-  return ret;
-};
-
-const traverse = (api, callback, prefix = '') => {
-  if (api.children) {
-    for (const [key, child] of Object.entries(api.children)) {
-      traverse(child, callback, `${prefix}.${key}`);
+  // Remove empty children
+  traverse(ret[mainApi], (node, prefix) => {
+    if (_.isEmpty(node.children)) {
+      delete node.children;
     }
-  }
-  callback(api, prefix);
+  });
+
+  return ret;
 };
 
 const getApiDetail = async (modelId, apiName) => {
-- 
GitLab


From 2b66c98a2de6a9750974b76d931bfb755a4d409f Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Mon, 10 Feb 2025 18:44:13 +0700
Subject: [PATCH 11/16] fix missing extended api unit

---
 src/controllers/model.controller.js | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/controllers/model.controller.js b/src/controllers/model.controller.js
index b04b6d3..14fcbf2 100644
--- a/src/controllers/model.controller.js
+++ b/src/controllers/model.controller.js
@@ -35,6 +35,7 @@ const createModel = catchAsync(async (req, res) => {
             type: api.type,
             datatype: api.datatype,
             isWishlist: api.isWishlist || false,
+            unit: api.unit,
           })
         )
       );
@@ -64,6 +65,7 @@ const createModel = catchAsync(async (req, res) => {
               type: api.type || 'branch',
               datatype: api.datatype || (api.type !== 'branch' ? 'string' : null),
               isWishlist: api.isWishlist || false,
+              unit: api.unit,
             })
           )
         );
-- 
GitLab


From 874e52261e4a3f2d98e6682658e4fb67f0f0f7e3 Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Tue, 11 Feb 2025 12:42:06 +0700
Subject: [PATCH 12/16] allow empty string for unit

---
 src/validations/extendedApi.validation.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/validations/extendedApi.validation.js b/src/validations/extendedApi.validation.js
index 8fc0b44..93ac8f7 100644
--- a/src/validations/extendedApi.validation.js
+++ b/src/validations/extendedApi.validation.js
@@ -16,7 +16,7 @@ const createExtendedApi = {
       })
     ),
     isWishlist: Joi.boolean().default(false),
-    unit: Joi.string(),
+    unit: Joi.string().allow(''),
   }),
 };
 
@@ -56,6 +56,7 @@ const updateExtendedApi = {
         })
       ),
       isWishlist: Joi.boolean(),
+      unit: Joi.string().allow(''),
     })
     .min(1),
 };
-- 
GitLab


From bb67f2e6755ed597f5387641ce4342db5f3ef357 Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Tue, 11 Feb 2025 13:40:33 +0700
Subject: [PATCH 13/16] add replace Vehicle APIs route

---
 src/controllers/model.controller.js | 40 +++++++++++++++++++++++++++++
 src/routes/v2/model.route.js        |  4 +++
 src/services/api.service.js         |  3 ++-
 src/services/extendedApi.service.js |  5 ++++
 src/services/model.service.js       |  2 +-
 src/validations/model.validation.js | 10 ++++++++
 6 files changed, 62 insertions(+), 2 deletions(-)

diff --git a/src/controllers/model.controller.js b/src/controllers/model.controller.js
index 14fcbf2..f993f71 100644
--- a/src/controllers/model.controller.js
+++ b/src/controllers/model.controller.js
@@ -256,6 +256,45 @@ const getApiDetail = catchAsync(async (req, res) => {
   res.send(api);
 });
 
+const replaceApi = catchAsync(async (req, res) => {
+  const modelId = req.params.id;
+  const { extended_apis, api_version, main_api } = await modelService.processApiDataUrl(req.body.api_data_url);
+
+  console.log('main api', main_api);
+
+  const updateBody = {
+    custom_apis: [], // Remove all custom_apis
+    main_api,
+    api_version: null,
+  };
+  if (api_version) {
+    updateBody.api_version = api_version;
+  }
+
+  await modelService.updateModelById(modelId, updateBody, req.user?.id);
+
+  await extendedApiService.deleteExtendedApisByModelId(modelId);
+  if (Array.isArray(extended_apis)) {
+    await Promise.all(
+      extended_apis.map((api) =>
+        extendedApiService.createExtendedApi({
+          model: modelId,
+          apiName: api.apiName,
+          description: api.description,
+          skeleton: api.skeleton,
+          tags: api.tags,
+          type: api.type,
+          datatype: api.datatype,
+          isWishlist: api.isWishlist || false,
+          unit: api.unit,
+        })
+      )
+    );
+  }
+
+  res.status(httpStatus.OK).send();
+});
+
 module.exports = {
   createModel,
   listModels,
@@ -267,4 +306,5 @@ module.exports = {
   getComputedVSSApi,
   listAllModels,
   getApiDetail,
+  replaceApi,
 };
diff --git a/src/routes/v2/model.route.js b/src/routes/v2/model.route.js
index b54860e..c0929f3 100644
--- a/src/routes/v2/model.route.js
+++ b/src/routes/v2/model.route.js
@@ -50,6 +50,10 @@ router
     modelController.deleteModel
   );
 
+router
+  .route('/:id/replace-api')
+  .post(auth(), checkPermission(PERMISSIONS.WRITE_MODEL), validate(modelValidation.replaceApi), modelController.replaceApi);
+
 router.route('/:id/api').get(
   auth({
     optional: !config.strictAuth,
diff --git a/src/services/api.service.js b/src/services/api.service.js
index 837cbd7..1c1e92b 100644
--- a/src/services/api.service.js
+++ b/src/services/api.service.js
@@ -205,6 +205,7 @@ const ensureParentApiHierarchy = (root, api) => {
 };
 
 const traverse = (api, callback, prefix = '') => {
+  if (!api) return;
   if (api.children) {
     for (const [key, child] of Object.entries(api.children)) {
       traverse(child, callback, `${prefix}.${key}`);
@@ -279,7 +280,7 @@ const computeVSSApi = async (modelId) => {
 
   // Nest parent/children apis
 
-  const keys = Object.keys(ret[mainApi].children).filter((key) => key.includes('.'));
+  const keys = Object.keys(ret[mainApi]?.children || {}).filter((key) => key.includes('.'));
 
   for (const key of keys) {
     const parts = key.split('.');
diff --git a/src/services/extendedApi.service.js b/src/services/extendedApi.service.js
index 264bb5c..0afa9c1 100644
--- a/src/services/extendedApi.service.js
+++ b/src/services/extendedApi.service.js
@@ -82,6 +82,10 @@ const getExtendedApiByApiNameAndModel = async (apiName, model) => {
   return ExtendedApi.findOne({ apiName, model });
 };
 
+const deleteExtendedApisByModelId = async (modelId) => {
+  await ExtendedApi.deleteMany({ model: modelId });
+};
+
 module.exports = {
   createExtendedApi,
   queryExtendedApis,
@@ -89,4 +93,5 @@ module.exports = {
   updateExtendedApiById,
   deleteExtendedApiById,
   getExtendedApiByApiNameAndModel,
+  deleteExtendedApisByModelId,
 };
diff --git a/src/services/model.service.js b/src/services/model.service.js
index f9f6ed9..f0fb800 100644
--- a/src/services/model.service.js
+++ b/src/services/model.service.js
@@ -446,7 +446,7 @@ const traverse = (api, callback, prefix = '') => {
 /**
  *
  * @param {string} apiDataUrl
- * @returns {Promise<{api_version: string; extended_apis: any[]} | undefined>}
+ * @returns {Promise<{main_api: string; api_version: string; extended_apis: any[]} | undefined>}
  */
 const processApiDataUrl = async (apiDataUrl) => {
   try {
diff --git a/src/validations/model.validation.js b/src/validations/model.validation.js
index 47b1f5b..47b9e3f 100644
--- a/src/validations/model.validation.js
+++ b/src/validations/model.validation.js
@@ -118,6 +118,15 @@ const getApiByModelId = {
   }),
 };
 
+const replaceApi = {
+  params: Joi.object().keys({
+    id: Joi.string().custom(objectId),
+  }),
+  body: Joi.object().keys({
+    api_data_url: Joi.string().required(),
+  }),
+};
+
 module.exports = {
   createModel,
   listModels,
@@ -127,4 +136,5 @@ module.exports = {
   addAuthorizedUser,
   deleteAuthorizedUser,
   getApiByModelId,
+  replaceApi,
 };
-- 
GitLab


From 35d7f88f8951f2b839050c528b217e4c7bc03837 Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Tue, 11 Feb 2025 16:44:07 +0700
Subject: [PATCH 14/16] allow null values for field datatype and field unit of
 extendedapis

---
 src/validations/extendedApi.validation.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/validations/extendedApi.validation.js b/src/validations/extendedApi.validation.js
index 93ac8f7..9d58366 100644
--- a/src/validations/extendedApi.validation.js
+++ b/src/validations/extendedApi.validation.js
@@ -7,7 +7,7 @@ const createExtendedApi = {
     model: Joi.string().custom(objectId).required(),
     skeleton: Joi.string().optional(),
     type: Joi.string(),
-    datatype: Joi.string(),
+    datatype: Joi.string().allow(null),
     description: Joi.string().allow('').default(''),
     tags: Joi.array().items(
       Joi.object().keys({
@@ -16,7 +16,7 @@ const createExtendedApi = {
       })
     ),
     isWishlist: Joi.boolean().default(false),
-    unit: Joi.string().allow(''),
+    unit: Joi.string().allow('', null),
   }),
 };
 
@@ -47,7 +47,7 @@ const updateExtendedApi = {
         .message('apiName must start with Vehicle.'),
       skeleton: Joi.string().optional(),
       type: Joi.string(),
-      datatype: Joi.string(),
+      datatype: Joi.string().allow(null),
       description: Joi.string().allow(''),
       tags: Joi.array().items(
         Joi.object().keys({
@@ -56,7 +56,7 @@ const updateExtendedApi = {
         })
       ),
       isWishlist: Joi.boolean(),
-      unit: Joi.string().allow(''),
+      unit: Joi.string().allow('', null),
     })
     .min(1),
 };
-- 
GitLab


From feb50b91bc73e8b824729fabc67e493bce74757c Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Wed, 12 Feb 2025 13:58:45 +0700
Subject: [PATCH 15/16] ensure validation for extended api when replace Vehicle
 API & refine tree

---
 src/controllers/model.controller.js       | 52 ++++++++++++++---------
 src/services/api.service.js               | 32 +++++++++++---
 src/services/extendedApi.service.js       | 36 ++++++++++++----
 src/services/model.service.js             |  5 ++-
 src/typedefs/index.js                     | 13 ++++++
 src/validations/extendedApi.validation.js | 12 +++++-
 6 files changed, 112 insertions(+), 38 deletions(-)
 create mode 100644 src/typedefs/index.js

diff --git a/src/controllers/model.controller.js b/src/controllers/model.controller.js
index f993f71..f52b416 100644
--- a/src/controllers/model.controller.js
+++ b/src/controllers/model.controller.js
@@ -260,8 +260,6 @@ const replaceApi = catchAsync(async (req, res) => {
   const modelId = req.params.id;
   const { extended_apis, api_version, main_api } = await modelService.processApiDataUrl(req.body.api_data_url);
 
-  console.log('main api', main_api);
-
   const updateBody = {
     custom_apis: [], // Remove all custom_apis
     main_api,
@@ -271,27 +269,41 @@ const replaceApi = catchAsync(async (req, res) => {
     updateBody.api_version = api_version;
   }
 
-  await modelService.updateModelById(modelId, updateBody, req.user?.id);
-
-  await extendedApiService.deleteExtendedApisByModelId(modelId);
+  // Validate extended_apis
   if (Array.isArray(extended_apis)) {
-    await Promise.all(
-      extended_apis.map((api) =>
-        extendedApiService.createExtendedApi({
-          model: modelId,
-          apiName: api.apiName,
-          description: api.description,
-          skeleton: api.skeleton,
-          tags: api.tags,
-          type: api.type,
-          datatype: api.datatype,
-          isWishlist: api.isWishlist || false,
-          unit: api.unit,
-        })
-      )
-    );
+    for (const extended_api of extended_apis) {
+      const error = await extendedApiService.validateExtendedApi({
+        ...extended_api,
+        model: modelId,
+      });
+      if (error) {
+        throw new ApiError(
+          httpStatus.BAD_REQUEST,
+          `Error in validating extended API ${extended_api.name || extended_api.apiName} - ${error.details.join(', ')}`
+        );
+      }
+    }
   }
 
+  await modelService.updateModelById(modelId, updateBody, req.user?.id);
+  await extendedApiService.deleteExtendedApisByModelId(modelId);
+
+  await Promise.all(
+    (extended_apis || []).map((api) =>
+      extendedApiService.createExtendedApi({
+        model: modelId,
+        apiName: api.apiName,
+        description: api.description,
+        skeleton: api.skeleton,
+        tags: api.tags,
+        type: api.type,
+        datatype: api.datatype,
+        isWishlist: api.isWishlist || false,
+        unit: api.unit,
+      })
+    )
+  );
+
   res.status(httpStatus.OK).send();
 });
 
diff --git a/src/services/api.service.js b/src/services/api.service.js
index 1c1e92b..a7a0c6d 100644
--- a/src/services/api.service.js
+++ b/src/services/api.service.js
@@ -7,6 +7,7 @@ const path = require('path');
 const logger = require('../config/logger');
 const { sortObject } = require('../utils/sort');
 const _ = require('lodash');
+const crypto = require('crypto');
 
 /**
  *
@@ -279,7 +280,6 @@ const computeVSSApi = async (modelId) => {
   }
 
   // Nest parent/children apis
-
   const keys = Object.keys(ret[mainApi]?.children || {}).filter((key) => key.includes('.'));
 
   for (const key of keys) {
@@ -297,12 +297,30 @@ const computeVSSApi = async (modelId) => {
     delete ret[mainApi].children[key];
   }
 
-  // Remove empty children
-  traverse(ret[mainApi], (node, prefix) => {
-    if (_.isEmpty(node.children)) {
-      delete node.children;
-    }
-  });
+  // Refine tree
+  traverse(
+    ret[mainApi],
+    (node, prefix) => {
+      // Delete empty children
+      if (_.isEmpty(node.children)) {
+        delete node.children;
+      }
+      // Ensure name and id
+      if (!node.name) {
+        node.name = prefix;
+      }
+      if (!node.id) {
+        node.id = crypto.randomBytes(12).toString('hex');
+      }
+      // Ensure datatype
+      if (node.type === 'branch') {
+        delete node.datatype;
+      } else if (!node.datatype) {
+        node.datatype = 'string';
+      }
+    },
+    mainApi
+  );
 
   return ret;
 };
diff --git a/src/services/extendedApi.service.js b/src/services/extendedApi.service.js
index 0afa9c1..8adb34c 100644
--- a/src/services/extendedApi.service.js
+++ b/src/services/extendedApi.service.js
@@ -3,6 +3,8 @@ const { ExtendedApi } = require('../models');
 const ApiError = require('../utils/ApiError');
 const { permissionService } = require('.');
 const { PERMISSIONS } = require('../config/roles');
+const Joi = require('joi');
+const { extendedApiValidation } = require('../validations');
 
 /**
  * Create a new ExtendedApi
@@ -86,12 +88,30 @@ const deleteExtendedApisByModelId = async (modelId) => {
   await ExtendedApi.deleteMany({ model: modelId });
 };
 
-module.exports = {
-  createExtendedApi,
-  queryExtendedApis,
-  getExtendedApiById,
-  updateExtendedApiById,
-  deleteExtendedApiById,
-  getExtendedApiByApiNameAndModel,
-  deleteExtendedApisByModelId,
+/**
+ *
+ * @param {import('../typedefs').ExtendedApi} extendedApi
+ * @returns {Promise<null | {details: string[]}>}
+ */
+const validateExtendedApi = async (extendedApi) => {
+  const { _, error } = Joi.compile(extendedApiValidation.createExtendedApi.body.unknown())
+    .prefs({ errors: { label: 'key' }, abortEarly: false })
+    .validate(extendedApi);
+
+  if (error) {
+    return {
+      details: error.details.map((details) => details.message),
+    };
+  }
+
+  return null;
 };
+
+module.exports.createExtendedApi = createExtendedApi;
+module.exports.queryExtendedApis = queryExtendedApis;
+module.exports.getExtendedApiById = getExtendedApiById;
+module.exports.updateExtendedApiById = updateExtendedApiById;
+module.exports.deleteExtendedApiById = deleteExtendedApiById;
+module.exports.getExtendedApiByApiNameAndModel = getExtendedApiByApiNameAndModel;
+module.exports.deleteExtendedApisByModelId = deleteExtendedApisByModelId;
+module.exports.validateExtendedApi = validateExtendedApi;
diff --git a/src/services/model.service.js b/src/services/model.service.js
index f0fb800..84b82df 100644
--- a/src/services/model.service.js
+++ b/src/services/model.service.js
@@ -518,7 +518,10 @@ const processApiDataUrl = async (apiDataUrl) => {
     return result;
   } catch (error) {
     logger.error(`Error in processing api data: ${error}`);
-    throw new ApiError(httpStatus.BAD_REQUEST, `Error in processing api data. Please check content of the file again.`);
+    throw new ApiError(
+      httpStatus.BAD_REQUEST,
+      error?.message || `Error in processing api data. Please check content of the file again.`
+    );
   }
 };
 
diff --git a/src/typedefs/index.js b/src/typedefs/index.js
new file mode 100644
index 0000000..b204b09
--- /dev/null
+++ b/src/typedefs/index.js
@@ -0,0 +1,13 @@
+/**
+ * @typedef ExtendedApi
+ * @property apiName: string
+ * @property model: string
+ * @property skeleton: string
+ * @property unit: string
+ * @property type: string
+ * @property datatype: string
+ * @property description: string
+ * @property isWishlist: boolean
+ */
+
+exports.unused = {};
diff --git a/src/validations/extendedApi.validation.js b/src/validations/extendedApi.validation.js
index 9d58366..ac6f56e 100644
--- a/src/validations/extendedApi.validation.js
+++ b/src/validations/extendedApi.validation.js
@@ -7,7 +7,11 @@ const createExtendedApi = {
     model: Joi.string().custom(objectId).required(),
     skeleton: Joi.string().optional(),
     type: Joi.string(),
-    datatype: Joi.string().allow(null),
+    datatype: Joi.alternatives().conditional('type', {
+      is: 'branch',
+      then: Joi.string().allow(null).optional(),
+      otherwise: Joi.string().required(),
+    }),
     description: Joi.string().allow('').default(''),
     tags: Joi.array().items(
       Joi.object().keys({
@@ -47,7 +51,11 @@ const updateExtendedApi = {
         .message('apiName must start with Vehicle.'),
       skeleton: Joi.string().optional(),
       type: Joi.string(),
-      datatype: Joi.string().allow(null),
+      datatype: Joi.alternatives().conditional('type', {
+        is: 'branch',
+        then: Joi.string().allow(null).optional(),
+        otherwise: Joi.string().required(),
+      }),
       description: Joi.string().allow(''),
       tags: Joi.array().items(
         Joi.object().keys({
-- 
GitLab


From f39062dff19a6ec2a6a003d96f02ee2f60d828ad Mon Sep 17 00:00:00 2001
From: tuanh <tuan.hoangdinhanh@vn.bosch.com>
Date: Thu, 13 Feb 2025 15:36:46 +0700
Subject: [PATCH 16/16] add default description for API

---
 src/services/api.service.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/services/api.service.js b/src/services/api.service.js
index a7a0c6d..89d7ca9 100644
--- a/src/services/api.service.js
+++ b/src/services/api.service.js
@@ -312,6 +312,9 @@ const computeVSSApi = async (modelId) => {
       if (!node.id) {
         node.id = crypto.randomBytes(12).toString('hex');
       }
+      if (!node.description) {
+        node.description = 'nan';
+      }
       // Ensure datatype
       if (node.type === 'branch') {
         delete node.datatype;
-- 
GitLab