diff --git a/src/constants/index.ts b/src/constants/index.ts index 2239fb213..c5b21e470 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -32,6 +32,11 @@ export const constants = { MIGRATION_DELETED: "Project's migration deleted successfully.", INVALID_ID: "Provided $ ID is invalid.", MIGRATION_EXISTS: "Project's migration already exists.", + PROJECT_NOT_FOUND: "Project does not exist", + CONTENT_TYPE_NOT_FOUND: "ContentType does not exist", + INVALID_CONTENT_TYPE: "Provide valid ContentType data", + RESET_CONTENT_MAPPING: + "ContentType has been successfully restored to its initial mapping", }, HTTP_RESPONSE_HEADERS: { "Access-Control-Allow-Origin": "*", @@ -47,3 +52,6 @@ export const constants = { INVALID_REGION: "Provided region doesn't exists.", }, }; +export const PROJECT_POPULATE_FIELDS = "migration.modules.content_mapper"; +export const CONTENT_TYPE_POPULATE_FIELDS = + "otherCmsTitle otherCmsUid isUpdated updateAt contentstackTitle contnetStackUid"; diff --git a/src/controllers/projects.contentMapper.controller.ts b/src/controllers/projects.contentMapper.controller.ts new file mode 100644 index 000000000..a02fc8491 --- /dev/null +++ b/src/controllers/projects.contentMapper.controller.ts @@ -0,0 +1,42 @@ +import { Request, Response } from "express"; +import { contentMapperService } from "../services/contentMapper.service"; +const putTestData = async (req: Request, res: Response): Promise => { + const resp = await contentMapperService.putTestData(req); + res.status(200).json(resp); +}; + +const getContentTypes = async (req: Request, res: Response): Promise => { + const resp = await contentMapperService.getContentTypes(req); + res.status(200).json(resp); +}; +const getFieldMapping = async (req: Request, res: Response): Promise => { + const resp = await contentMapperService.getFieldMapping(req); + res.status(200).json(resp); +}; +const getExistingContentTypes = async ( + req: Request, + res: Response +): Promise => { + const resp = await contentMapperService.getExistingContentTypes(req); + res.status(201).json(resp); +}; +const putContentTypeFields = async ( + req: Request, + res: Response +): Promise => { + const resp = await contentMapperService.udateContentType(req); + res.status(200).json(resp); +}; +const resetContentType = async (req: Request, res: Response): Promise => { + const resp = await contentMapperService.resetToInitialMapping(req); + res.status(200).json(resp); +}; + +export const contentMapperController = { + getContentTypes, + getFieldMapping, + getExistingContentTypes, + putTestData, + putContentTypeFields, + resetContentType, +}; diff --git a/src/database.ts b/src/database.ts index a30d79976..c4efd3aa8 100644 --- a/src/database.ts +++ b/src/database.ts @@ -5,6 +5,8 @@ import logger from "./utils/logger"; import ProjectModel from "./models/project"; import AuthenticationModel from "./models/authentication"; import AuditLogModel from "./models/auditLog"; +import ContentTypesMapperModel from "./models/contentTypesMapper"; +import FieldMapperModel from "./models/FieldMapper"; const connectToDatabase = async () => { try { @@ -18,6 +20,8 @@ const connectToDatabase = async () => { await ProjectModel.init(); await AuthenticationModel.init(); await AuditLogModel.init(); + await ContentTypesMapperModel.init(); + await FieldMapperModel.init(); } catch (error) { logger.error("Error while connecting to MongoDB:", error); process.exit(1); diff --git a/src/models/FieldMapper.ts b/src/models/FieldMapper.ts new file mode 100644 index 000000000..2bb421f58 --- /dev/null +++ b/src/models/FieldMapper.ts @@ -0,0 +1,32 @@ +import { Schema, model, Document } from "mongoose"; + +interface FieldMapper extends Document { + uid: string; + otherCmsField: string; + otherCmsType: string; + contentstackField: string; + contentstackFieldUid: string; + ContentstackFieldType: string; + isDeleted: boolean; + backupFieldType: string; + refrenceTo: { uid: string; title: string }; +} + +const fieldMapperSchema = new Schema({ + uid: { type: String, required: true }, + otherCmsField: { type: String, required: true }, + otherCmsType: { type: String, required: true }, + contentstackField: { type: String }, + contentstackFieldUid: { type: String }, + ContentstackFieldType: { type: String, required: true }, + isDeleted: { type: Boolean, default: false }, + backupFieldType: { type: String }, + refrenceTo: { + uid: { type: String }, + title: { type: String }, + }, +}); + +const FieldMapperModel = model("FieldMapping", fieldMapperSchema); + +export default FieldMapperModel; diff --git a/src/models/contentTypesMapper.ts b/src/models/contentTypesMapper.ts new file mode 100644 index 000000000..d3dfebd96 --- /dev/null +++ b/src/models/contentTypesMapper.ts @@ -0,0 +1,28 @@ +import { Schema, model, Document } from "mongoose"; + +interface ContentTypesMapper extends Document { + otherCmsTitle: string; + otherCmsUid: string; + isUpdated: boolean; + updateAt: Date; + contentstackTitle: string; + contnetStackUid: string; + fieldMapping: []; +} + +const contentTypesMapperSchema = new Schema({ + otherCmsTitle: { type: String, required: true }, + otherCmsUid: { type: String, required: true }, + isUpdated: { type: Boolean, default: false }, + updateAt: { type: Date }, + contentstackTitle: { type: String }, + contnetStackUid: { type: String }, + fieldMapping: [{ type: Schema.Types.ObjectId, ref: "FieldMapping" }], +}); + +const ContentTypesMapperModel = model( + "ContentTypes Mapper", + contentTypesMapperSchema +); + +export default ContentTypesMapperModel; diff --git a/src/models/project.ts b/src/models/project.ts index 4cc0045a4..5e5f17ab3 100644 --- a/src/models/project.ts +++ b/src/models/project.ts @@ -65,6 +65,9 @@ const projectSchema = new Schema( stack_id: { type: String }, org_id: { type: String }, }, + content_mapper: [ + { type: Schema.Types.ObjectId, ref: "ContentTypes Mapper" }, + ], }, }, execution_log: { diff --git a/src/routes/contentMapper.routes.ts b/src/routes/contentMapper.routes.ts new file mode 100644 index 000000000..8ab1f1132 --- /dev/null +++ b/src/routes/contentMapper.routes.ts @@ -0,0 +1,40 @@ +import express from "express"; +import { contentMapperController } from "../controllers/projects.contentMapper.controller"; +import { asyncRouter } from "../utils/async-router.utils"; + +const router = express.Router({ mergeParams: true }); + +//Developer End Point to create dummy data +router.post( + "/createDummyData/:projectId", + asyncRouter(contentMapperController.putTestData) +); + +//Get ContentTypes List +router.get( + "/contentTypes/:projectId/:skip/:limit/:searchText?", + asyncRouter(contentMapperController.getContentTypes) +); +//Get FieldMapping List +router.get( + "/fieldMappnig/:contentTypeId/:skip/:limit/:searchText?", + asyncRouter(contentMapperController.getFieldMapping) +); +//Get Existing ContentTypes List +//To Do +router.get( + "/:projectId/:stackUid", + asyncRouter(contentMapperController.getExistingContentTypes) +); +//Update FieldMapping or contentType +router.put( + "/contentTypes/:contentTypeId", + asyncRouter(contentMapperController.putContentTypeFields) +); +//Reset FieldMapping or contentType +router.put( + "/resetFields/:contentTypeId", + asyncRouter(contentMapperController.resetContentType) +); + +export default router; diff --git a/src/server.ts b/src/server.ts index 019bfac9c..db7e7ad6a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -14,6 +14,7 @@ import { authenticateUser } from "./middlewares/auth.middleware"; import { requestHeadersMiddleware } from "./middlewares/req-headers.middleware"; import { unmatchedRoutesMiddleware } from "./middlewares/unmatched-routes.middleware"; import logger from "./utils/logger"; +import contentMapperRoutes from "./routes/contentMapper.routes"; try { const app = express(); @@ -34,6 +35,7 @@ try { app.use("/v2/user", authenticateUser, userRoutes); app.use("/v2/org/:orgId", authenticateUser, orgRoutes); app.use("/v2/org/:orgId/project", authenticateUser, projectRoutes); + app.use("/v2/mapper", authenticateUser, contentMapperRoutes); //For unmatched route patterns app.use(unmatchedRoutesMiddleware); diff --git a/src/services/contentMapper.service.ts b/src/services/contentMapper.service.ts new file mode 100644 index 000000000..2ef7fda5b --- /dev/null +++ b/src/services/contentMapper.service.ts @@ -0,0 +1,291 @@ +import { Request } from "express"; +import ContentTypesMapperModel from "../models/contentTypesMapper"; +import FieldMapperModel from "../models/FieldMapper"; +import ProjectModel from "../models/project"; +import { getLogMessage, isEmpty } from "../utils"; +import { + BadRequestError, + ExceptionFunction, +} from "../utils/custom-errors.utils"; +import { + CONTENT_TYPE_POPULATE_FIELDS, + PROJECT_POPULATE_FIELDS, + constants, +} from "../constants"; +import logger from "../utils/logger"; + +// Developer service to create dummy contentmapping data +const putTestData = async (req: Request) => { + const projectId = req.params.projectId; + let contentTypes = req.body; + let updatedTypes: any = []; + + // console.log(contentTypes) + await Promise.all( + contentTypes.map(async (type: any, index: any) => { + const mapperData = await FieldMapperModel.insertMany(type.fieldMapping, { + ordered: true, + }) + .then(function (docs: any) { + // do something with docs + contentTypes[index].fieldMapping = docs.map((item: any) => { + return item._id; + }); + }) + .catch(function (err) { + // console.log("type.fieldMapping") + // console.log(err) + // error handling here + }); + }) + ); + + let typeIds: any = []; + + const postData = await ContentTypesMapperModel.insertMany(contentTypes, { + ordered: true, + }) + .then(async function (docs) { + // do something with docs + typeIds = docs.map((item) => { + return item._id; + }); + }) + .catch(function (err) { + // console.log(err) + // error handling here + }); + + const projectDetails: any = await ProjectModel.findOne({ + _id: projectId, + }); + projectDetails.migration.modules.content_mapper = typeIds; + projectDetails.save(); + //Add logic to get Project from DB + return projectDetails; +}; + +const getContentTypes = async (req: Request) => { + const sourceFn = "getContentTypes"; + const projectId = req?.params?.projectId; + const skip: any = req?.params?.skip; + const limit: any = req?.params?.limit; + const search: string = req?.params?.searchText?.toLowerCase(); + + let result = []; + let totalCount = 0; + const projectDetails = await ProjectModel.findOne({ + _id: projectId, + }).populate({ + path: PROJECT_POPULATE_FIELDS, + select: CONTENT_TYPE_POPULATE_FIELDS, + }); + + if (isEmpty(projectDetails)) { + logger.error( + getLogMessage( + sourceFn, + `${constants.HTTP_TEXTS.PROJECT_NOT_FOUND} projectId: ${projectId}` + ) + ); + throw new BadRequestError(constants.HTTP_TEXTS.PROJECT_NOT_FOUND); + } + const { + migration: { + modules: { content_mapper }, + }, + }: any = projectDetails; + + if (!isEmpty(content_mapper)) { + if (search) { + let filteredResult = content_mapper + .filter((item: any) => + item?.otherCmsTitle?.toLowerCase().includes(search) + ) + ?.sort((a: any, b: any) => + a.otherCmsTitle.localeCompare(b.otherCmsTitle) + ); + totalCount = filteredResult.length; + result = filteredResult.slice(skip, Number(skip) + Number(limit)); + } else { + totalCount = content_mapper.length; + result = content_mapper + ?.sort((a: any, b: any) => + a.otherCmsTitle.localeCompare(b.otherCmsTitle) + ) + ?.slice(skip, Number(skip) + Number(limit)); + } + } + return { count: totalCount, contentTypes: result }; +}; + +const getFieldMapping = async (req: Request) => { + const srcFunc = "getFieldMapping"; + const contentTypeId = req?.params?.contentTypeId; + const skip: any = req?.params?.skip; + const limit: any = req?.params?.limit; + const search: string = req?.params?.searchText?.toLowerCase(); + + let result = []; + let filteredResult = []; + let totalCount = 0; + + const contentType = await ContentTypesMapperModel.findOne({ + _id: contentTypeId, + }).populate("fieldMapping"); + + if (isEmpty(contentType)) { + logger.error( + getLogMessage( + srcFunc, + `${constants.HTTP_TEXTS.CONTENT_TYPE_NOT_FOUND} Id: ${contentTypeId}` + ) + ); + throw new BadRequestError(constants.HTTP_TEXTS.CONTENT_TYPE_NOT_FOUND); + } + + const { fieldMapping }: any = contentType; + if (!isEmpty(fieldMapping)) { + if (search) { + filteredResult = fieldMapping.filter((item: any) => + item?.otherCmsField?.toLowerCase().includes(search) + ); + totalCount = filteredResult.length; + result = filteredResult.slice(skip, Number(skip) + Number(limit)); + } else { + totalCount = fieldMapping.length; + result = fieldMapping.slice(skip, Number(skip) + Number(limit)); + } + } + return { count: totalCount, fieldMapping: result }; +}; + +const getExistingContentTypes = async (req: Request) => { + const orgId = req?.params?.orgId; + const projectId = req?.params?.projectId; + + //Add logic to get Project from DB + return { orgId, projectId }; +}; + +const udateContentType = async (req: Request) => { + const srcFun = "udateContentType"; + const contentTypeId = req?.params?.contentTypeId; + const contentTypeData = req?.body; + const { fieldMapping } = contentTypeData; + + let updatedContentType = {}; + + if (isEmpty(contentTypeData)) { + logger.error( + getLogMessage( + srcFun, + `${constants.HTTP_TEXTS.CONTENT_TYPE_NOT_FOUND} Id: ${contentTypeId}` + ) + ); + throw new BadRequestError(constants.HTTP_TEXTS.INVALID_CONTENT_TYPE); + } + try { + updatedContentType = await ContentTypesMapperModel.findOneAndUpdate( + { + _id: contentTypeId, + }, + { + otherCmsTitle: contentTypeData?.otherCmsTitle, + otherCmsUid: contentTypeData?.otherCmsUid, + isUpdated: contentTypeData?.isUpdated, + updateAt: contentTypeData?.updateAt, + contentstackTitle: contentTypeData?.contentstackTitle, + contnetStackUid: contentTypeData?.contnetStackUid, + }, + { new: true, upsert: true, setDefaultsOnInsert: true } + ); + if (!isEmpty(fieldMapping)) { + const bulkWriteOperations = fieldMapping?.map((doc: any) => ({ + replaceOne: { + filter: { _id: doc._id }, + replacement: doc, + upsert: true, + }, + })); + await FieldMapperModel.bulkWrite(bulkWriteOperations, { ordered: false }); + } + + return { updatedContentType }; + } catch (error: any) { + logger.error( + getLogMessage( + srcFun, + `Error while updating ContentType Id: ${contentTypeId}`, + error + ) + ); + throw new ExceptionFunction( + error?.message || constants.HTTP_TEXTS.INTERNAL_ERROR, + error?.status || constants.HTTP_CODES.SERVER_ERROR, + srcFun + ); + } +}; + +const resetToInitialMapping = async (req: Request) => { + const srcFunc = "resetToInitialMapping"; + const contentTypeId = req?.params?.contentTypeId; + + const contentType = await ContentTypesMapperModel.findOne({ + _id: contentTypeId, + }).populate("fieldMapping"); + + if (isEmpty(contentType)) { + logger.error( + getLogMessage( + srcFunc, + `${constants.HTTP_TEXTS.CONTENT_TYPE_NOT_FOUND} Id: ${contentTypeId}` + ) + ); + throw new BadRequestError(constants.HTTP_TEXTS.INVALID_CONTENT_TYPE); + } + + try { + if (!isEmpty(contentType?.fieldMapping)) { + const bulkWriteOperations: any = contentType?.fieldMapping?.map( + (doc: any) => ({ + updateOne: { + filter: { _id: doc._id }, + update: { + $set: { + contentstackField: "", + contentstackFieldUid: "", + ContentstackFieldType: doc.backupFieldType, + }, + }, + }, + }) + ); + await FieldMapperModel.bulkWrite(bulkWriteOperations, { ordered: false }); + } + return { message: constants.HTTP_TEXTS.RESET_CONTENT_MAPPING }; + } catch (error: any) { + logger.error( + getLogMessage( + srcFunc, + `Error occurred while resetting the field mapping for the ContentType ID: ${contentTypeId}`, + error + ) + ); + throw new ExceptionFunction( + error?.message || constants.HTTP_TEXTS.INTERNAL_ERROR, + error?.status || constants.HTTP_CODES.SERVER_ERROR, + srcFunc + ); + } +}; + +export const contentMapperService = { + putTestData, + getContentTypes, + getFieldMapping, + getExistingContentTypes, + udateContentType, + resetToInitialMapping, +}; diff --git a/src/utils/custom-errors.utils.ts b/src/utils/custom-errors.utils.ts index eb70db8c7..65419e8da 100644 --- a/src/utils/custom-errors.utils.ts +++ b/src/utils/custom-errors.utils.ts @@ -52,4 +52,10 @@ export class UnauthorizedError extends AppError { } } +export class ExceptionFunction extends AppError { + constructor(message: string, httpStatus: number, srcFunc: string) { + super(httpStatus, message, srcFunc); + } +} + // Add more custom error classes as needed