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