Skip to content
Snippets Groups Projects
Commit 45d90514 authored by Tuan Hoang Dinh Anh's avatar Tuan Hoang Dinh Anh
Browse files

Merge branch 'main' into 'main'

Refactor the logic of computing VSS/API Tree

See merge request !16
parents 931e685c f39062df
No related branches found
No related tags found
1 merge request!16Refactor the logic of computing VSS/API Tree
...@@ -35,6 +35,7 @@ const createModel = catchAsync(async (req, res) => { ...@@ -35,6 +35,7 @@ const createModel = catchAsync(async (req, res) => {
type: api.type, type: api.type,
datatype: api.datatype, datatype: api.datatype,
isWishlist: api.isWishlist || false, isWishlist: api.isWishlist || false,
unit: api.unit,
}) })
) )
); );
...@@ -64,6 +65,7 @@ const createModel = catchAsync(async (req, res) => { ...@@ -64,6 +65,7 @@ const createModel = catchAsync(async (req, res) => {
type: api.type || 'branch', type: api.type || 'branch',
datatype: api.datatype || (api.type !== 'branch' ? 'string' : null), datatype: api.datatype || (api.type !== 'branch' ? 'string' : null),
isWishlist: api.isWishlist || false, isWishlist: api.isWishlist || false,
unit: api.unit,
}) })
) )
); );
...@@ -105,16 +107,18 @@ const listAllModels = catchAsync(async (req, res) => { ...@@ -105,16 +107,18 @@ const listAllModels = catchAsync(async (req, res) => {
req.user?.id req.user?.id
); );
const contributedModels = await modelService.queryModels( const contributedModels = req.user?.id
{ ? await modelService.queryModels(
is_contributor: req.user?.id, {},
}, {
{ limit: 1000,
limit: 1000, },
}, {
{}, is_contributor: req.user?.id,
req.user?.id },
); req.user?.id
)
: { results: [] };
const publicReleasedModels = await modelService.queryModels( const publicReleasedModels = await modelService.queryModels(
{ {
...@@ -241,6 +245,68 @@ const getComputedVSSApi = catchAsync(async (req, res) => { ...@@ -241,6 +245,68 @@ const getComputedVSSApi = catchAsync(async (req, res) => {
res.send(data); 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);
});
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);
const updateBody = {
custom_apis: [], // Remove all custom_apis
main_api,
api_version: null,
};
if (api_version) {
updateBody.api_version = api_version;
}
// Validate extended_apis
if (Array.isArray(extended_apis)) {
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();
});
module.exports = { module.exports = {
createModel, createModel,
listModels, listModels,
...@@ -251,4 +317,6 @@ module.exports = { ...@@ -251,4 +317,6 @@ module.exports = {
deleteAuthorizedUser, deleteAuthorizedUser,
getComputedVSSApi, getComputedVSSApi,
listAllModels, listAllModels,
getApiDetail,
replaceApi,
}; };
...@@ -201,6 +201,10 @@ const prototypeSchema = mongoose.Schema( ...@@ -201,6 +201,10 @@ const prototypeSchema = mongoose.Schema(
flow: { flow: {
type: mongoose.SchemaTypes.Mixed, type: mongoose.SchemaTypes.Mixed,
}, },
editors_choice: {
type: Boolean,
default: false,
},
}, },
{ {
timestamps: true, timestamps: true,
......
...@@ -50,6 +50,10 @@ router ...@@ -50,6 +50,10 @@ router
modelController.deleteModel modelController.deleteModel
); );
router
.route('/:id/replace-api')
.post(auth(), checkPermission(PERMISSIONS.WRITE_MODEL), validate(modelValidation.replaceApi), modelController.replaceApi);
router.route('/:id/api').get( router.route('/:id/api').get(
auth({ auth({
optional: !config.strictAuth, optional: !config.strictAuth,
...@@ -58,6 +62,8 @@ router.route('/:id/api').get( ...@@ -58,6 +62,8 @@ router.route('/:id/api').get(
modelController.getComputedVSSApi modelController.getComputedVSSApi
); );
router.route('/:id/api/:apiName').get(auth({ optional: !config.strictAuth }), modelController.getApiDetail);
router router
.route('/:id/permissions') .route('/:id/permissions')
.post( .post(
......
...@@ -6,6 +6,8 @@ const fs = require('fs'); ...@@ -6,6 +6,8 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const logger = require('../config/logger'); const logger = require('../config/logger');
const { sortObject } = require('../utils/sort'); const { sortObject } = require('../utils/sort');
const _ = require('lodash');
const crypto = require('crypto');
/** /**
* *
...@@ -184,6 +186,35 @@ const getUsedApis = (code, apiList) => { ...@@ -184,6 +186,35 @@ 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;
};
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}`);
}
}
callback(api, prefix);
};
/** /**
* *
* @param {string} modelId * @param {string} modelId
...@@ -211,22 +242,31 @@ const computeVSSApi = async (modelId) => { ...@@ -211,22 +242,31 @@ const computeVSSApi = async (modelId) => {
const extendedApis = await ExtendedApi.find({ const extendedApis = await ExtendedApi.find({
model: modelId, model: modelId,
isWishlist: true,
}); });
extendedApis.forEach((extendedApi) => { extendedApis.forEach((extendedApi) => {
try { try {
const name = extendedApi.apiName.split('.').slice(1).join('.'); const name = extendedApi.apiName.split('.').slice(1).join('.');
if (!name) return; if (!name) return;
ret[mainApi].children[name] = {
description: extendedApi.description, // Only add the extended API if it doesn't exist in the current CVI
type: extendedApi.type || 'branch', const keys = name.split('.');
id: extendedApi._id, let current = ret[mainApi].children;
datatype: extendedApi.datatype, for (const key of keys) {
name: extendedApi.apiName, if (!current || !current[key]) {
isWishlist: extendedApi.isWishlist, ret[mainApi].children[name] = {
}; description: extendedApi.description,
if (extendedApi.unit) { type: extendedApi.type || 'branch',
ret[mainApi].children[name].unit = extendedApi.unit; 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) { } catch (error) {
logger.warn(`Error while processing extended API ${extendedApi._id} with name ${extendedApi.apiName}: ${error}`); logger.warn(`Error while processing extended API ${extendedApi._id} with name ${extendedApi.apiName}: ${error}`);
...@@ -240,18 +280,70 @@ const computeVSSApi = async (modelId) => { ...@@ -240,18 +280,70 @@ const computeVSSApi = async (modelId) => {
} }
// Nest parent/children apis // Nest parent/children apis
const len = Object.keys(ret[mainApi].children).length; const keys = Object.keys(ret[mainApi]?.children || {}).filter((key) => key.includes('.'));
for (let i = len - 1; i >= 0; i--) {
const key = Object.keys(ret[mainApi].children)[i]; for (const key of keys) {
const parent = key.split('.').slice(0, -1).join('.'); const parts = key.split('.');
if (parent && ret[mainApi].children[parent]) { const parent = parts.slice(0, -1).join('.');
ret[mainApi].children[parent].children = ret[mainApi].children[parent].children || {}; const childKey = parts[parts.length - 1];
const childKey = key.replace(`${parent}.`, '');
ret[mainApi].children[parent].children[childKey] = ret[mainApi].children[key]; const parentNode = ensureParentApiHierarchy(ret[mainApi], parent);
delete ret[mainApi].children[key];
} parentNode.children[childKey] = {
...ret[mainApi].children[key],
children: ret[mainApi].children[key].children || {},
};
delete ret[mainApi].children[key];
} }
// 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');
}
if (!node.description) {
node.description = 'nan';
}
// Ensure datatype
if (node.type === 'branch') {
delete node.datatype;
} else if (!node.datatype) {
node.datatype = 'string';
}
},
mainApi
);
return ret;
};
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; return ret;
}; };
...@@ -265,3 +357,5 @@ module.exports.getVSSVersion = getVSSVersion; ...@@ -265,3 +357,5 @@ module.exports.getVSSVersion = getVSSVersion;
module.exports.computeVSSApi = computeVSSApi; module.exports.computeVSSApi = computeVSSApi;
module.exports.parseCvi = parseCvi; module.exports.parseCvi = parseCvi;
module.exports.getUsedApis = getUsedApis; module.exports.getUsedApis = getUsedApis;
module.exports.getApiDetail = getApiDetail;
module.exports.traverse = traverse;
...@@ -3,6 +3,8 @@ const { ExtendedApi } = require('../models'); ...@@ -3,6 +3,8 @@ const { ExtendedApi } = require('../models');
const ApiError = require('../utils/ApiError'); const ApiError = require('../utils/ApiError');
const { permissionService } = require('.'); const { permissionService } = require('.');
const { PERMISSIONS } = require('../config/roles'); const { PERMISSIONS } = require('../config/roles');
const Joi = require('joi');
const { extendedApiValidation } = require('../validations');
/** /**
* Create a new ExtendedApi * Create a new ExtendedApi
...@@ -82,11 +84,34 @@ const getExtendedApiByApiNameAndModel = async (apiName, model) => { ...@@ -82,11 +84,34 @@ const getExtendedApiByApiNameAndModel = async (apiName, model) => {
return ExtendedApi.findOne({ apiName, model }); return ExtendedApi.findOne({ apiName, model });
}; };
module.exports = { const deleteExtendedApisByModelId = async (modelId) => {
createExtendedApi, await ExtendedApi.deleteMany({ model: modelId });
queryExtendedApis,
getExtendedApiById,
updateExtendedApiById,
deleteExtendedApiById,
getExtendedApiByApiNameAndModel,
}; };
/**
*
* @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;
...@@ -446,7 +446,7 @@ const traverse = (api, callback, prefix = '') => { ...@@ -446,7 +446,7 @@ const traverse = (api, callback, prefix = '') => {
/** /**
* *
* @param {string} apiDataUrl * @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) => { const processApiDataUrl = async (apiDataUrl) => {
try { try {
...@@ -462,7 +462,13 @@ const processApiDataUrl = async (apiDataUrl) => { ...@@ -462,7 +462,13 @@ const processApiDataUrl = async (apiDataUrl) => {
(api, prefix) => { (api, prefix) => {
for (const [key, value] of Object.entries(api.children || {})) { for (const [key, value] of Object.entries(api.children || {})) {
if (value.isWishlist) { if (value.isWishlist) {
extendedApis.push(convertToExtendedApiFormat(value)); const name = value?.name || `${prefix}.${key}`;
extendedApis.push(
convertToExtendedApiFormat({
...value,
name,
})
);
delete api.children[key]; delete api.children[key];
} }
} }
...@@ -491,7 +497,13 @@ const processApiDataUrl = async (apiDataUrl) => { ...@@ -491,7 +497,13 @@ const processApiDataUrl = async (apiDataUrl) => {
data[mainApi], data[mainApi],
(api, prefix) => { (api, prefix) => {
for (const [key, value] of Object.entries(api.children || {})) { 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]; delete api.children[key];
} }
}, },
...@@ -505,7 +517,11 @@ const processApiDataUrl = async (apiDataUrl) => { ...@@ -505,7 +517,11 @@ const processApiDataUrl = async (apiDataUrl) => {
return result; return result;
} catch (error) { } 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?.message || `Error in processing api data. Please check content of the file again.`
);
} }
}; };
......
...@@ -66,7 +66,13 @@ const bulkCreatePrototypes = async (userId, prototypes) => { ...@@ -66,7 +66,13 @@ const bulkCreatePrototypes = async (userId, prototypes) => {
* @returns {Promise<QueryResult>} * @returns {Promise<QueryResult>}
*/ */
const queryPrototypes = async (filter, options) => { 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; return prototypes;
}; };
......
...@@ -21,7 +21,13 @@ const search = async (query, options, userId) => { ...@@ -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 { return {
...@@ -45,7 +51,10 @@ const searchUserByEmail = async (email) => { ...@@ -45,7 +51,10 @@ const searchUserByEmail = async (email) => {
* @param {string} signal * @param {string} signal
*/ */
const searchPrototypesBySignal = async (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)); return prototypes.filter((prototype) => prototype.code?.includes(signal));
}; };
......
/**
* @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 = {};
...@@ -7,7 +7,11 @@ const createExtendedApi = { ...@@ -7,7 +7,11 @@ const createExtendedApi = {
model: Joi.string().custom(objectId).required(), model: Joi.string().custom(objectId).required(),
skeleton: Joi.string().optional(), skeleton: Joi.string().optional(),
type: Joi.string(), type: Joi.string(),
datatype: Joi.string(), datatype: Joi.alternatives().conditional('type', {
is: 'branch',
then: Joi.string().allow(null).optional(),
otherwise: Joi.string().required(),
}),
description: Joi.string().allow('').default(''), description: Joi.string().allow('').default(''),
tags: Joi.array().items( tags: Joi.array().items(
Joi.object().keys({ Joi.object().keys({
...@@ -16,7 +20,7 @@ const createExtendedApi = { ...@@ -16,7 +20,7 @@ const createExtendedApi = {
}) })
), ),
isWishlist: Joi.boolean().default(false), isWishlist: Joi.boolean().default(false),
unit: Joi.string(), unit: Joi.string().allow('', null),
}), }),
}; };
...@@ -47,7 +51,11 @@ const updateExtendedApi = { ...@@ -47,7 +51,11 @@ const updateExtendedApi = {
.message('apiName must start with Vehicle.'), .message('apiName must start with Vehicle.'),
skeleton: Joi.string().optional(), skeleton: Joi.string().optional(),
type: Joi.string(), type: Joi.string(),
datatype: Joi.string(), datatype: Joi.alternatives().conditional('type', {
is: 'branch',
then: Joi.string().allow(null).optional(),
otherwise: Joi.string().required(),
}),
description: Joi.string().allow(''), description: Joi.string().allow(''),
tags: Joi.array().items( tags: Joi.array().items(
Joi.object().keys({ Joi.object().keys({
...@@ -56,6 +64,7 @@ const updateExtendedApi = { ...@@ -56,6 +64,7 @@ const updateExtendedApi = {
}) })
), ),
isWishlist: Joi.boolean(), isWishlist: Joi.boolean(),
unit: Joi.string().allow('', null),
}) })
.min(1), .min(1),
}; };
......
...@@ -118,6 +118,15 @@ const getApiByModelId = { ...@@ -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 = { module.exports = {
createModel, createModel,
listModels, listModels,
...@@ -127,4 +136,5 @@ module.exports = { ...@@ -127,4 +136,5 @@ module.exports = {
addAuthorizedUser, addAuthorizedUser,
deleteAuthorizedUser, deleteAuthorizedUser,
getApiByModelId, getApiByModelId,
replaceApi,
}; };
...@@ -43,6 +43,7 @@ const bodyValidation = Joi.object().keys({ ...@@ -43,6 +43,7 @@ const bodyValidation = Joi.object().keys({
partner_logo: Joi.string().allow(''), partner_logo: Joi.string().allow(''),
language: Joi.string().default('python'), language: Joi.string().default('python'),
requirements: Joi.string().allow(''), requirements: Joi.string().allow(''),
editors_choice: Joi.boolean(),
}); });
const createPrototype = { const createPrototype = {
...@@ -116,6 +117,7 @@ const updatePrototype = { ...@@ -116,6 +117,7 @@ const updatePrototype = {
partner_logo: Joi.string().allow(''), partner_logo: Joi.string().allow(''),
requirements: Joi.string().allow(''), requirements: Joi.string().allow(''),
language: Joi.string(), language: Joi.string(),
editors_choice: Joi.boolean(),
// rated_by: Joi.object().pattern( // rated_by: Joi.object().pattern(
// /^[0-9a-fA-F]{24}$/, // /^[0-9a-fA-F]{24}$/,
// Joi.object() // Joi.object()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment