Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,059 changes: 550 additions & 509 deletions api/package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
"@contentstack/cli-utilities": "^1.17.1",
"@contentstack/json-rte-serializer": "^3.0.5",
"@contentstack/marketplace-sdk": "^1.5.0",
"@emnapi/core": "1.9.1",
"@emnapi/runtime": "1.9.1",
"@emnapi/wasi-threads": "1.2.0",
"@wordpress/block-serialization-default-parser": "^5.39.0",
"axios": "^1.15.0",
"cheerio": "^1.2.0",
Expand All @@ -59,10 +62,7 @@
"php-serialize": "^5.1.3",
"socket.io": "^4.7.5",
"uuid": "^9.0.1",
"winston": "^3.11.0",
"@emnapi/core": "1.9.1",
"@emnapi/runtime" : "1.9.1",
"@emnapi/wasi-threads": "1.2.0"
"winston": "^3.11.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
Expand Down
140 changes: 118 additions & 22 deletions api/src/utils/content-type-creator.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import customLogger from './custom-logger.utils.js';
import { getLogMessage } from './index.js';
import { LIST_EXTENSION_UID, MIGRATION_DATA_CONFIG } from '../constants/index.js';
import { contentMapperService } from "../services/contentMapper.service.js";
import appMeta from '../constants/app/index.json' with { type: 'json' };
import appMeta from '../constants/app/index.json';

const {
GLOBAL_FIELDS_FILE_NAME,
Expand Down Expand Up @@ -1044,10 +1044,37 @@ const resolveIsSsoFlag = (is_sso: any): boolean => {
);
};

/**
* Resolves the Contentstack Management API UID for a content type / global field.
* @param migrationContentstackUid - The UID of the content type in the migration data.
* @param keyMapper - The key mapper object.
* @returns The Contentstack Management API UID.
*/
function resolveStackContentTypeUid(
migrationContentstackUid: string,
keyMapper?: Record<string, string>,
): string {
const mapped = keyMapper?.[migrationContentstackUid];
if (mapped === undefined || mapped === null || mapped === '') {
return migrationContentstackUid;
}

const m = String(mapped).trim();
const looksLikeUuid =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(m);
const looksLikeMongoId = /^[0-9a-f]{24}$/i.test(m);

if (looksLikeUuid || looksLikeMongoId) {
return migrationContentstackUid;
}

return m;
}

const existingCtMapper = async ({ keyMapper, contentTypeUid, projectId, region, user_id, is_sso, type}: any) => {
try {
const normalizedIsSso = resolveIsSsoFlag(is_sso);
const ctUid = keyMapper?.[contentTypeUid];
const ctUid = resolveStackContentTypeUid(contentTypeUid, keyMapper);

if(type === 'global_field') {

Expand Down Expand Up @@ -1102,6 +1129,92 @@ const mergeArrays = async (a: any[], b: any[]) => {
return a;
}

/**
* Clones a schema branch.
* @param node - The node to clone.
* @returns The cloned node.
*/
function cloneSchemaBranch(node: any): any {
if (node === undefined || node === null) return node;
try {
return structuredClone(node);
} catch {
return JSON.parse(JSON.stringify(node));
}
}

/**
* Finds the target modular blocks field.
* @param field - The field to find the target modular blocks field for.
* @param targetSchema - The target schema.
* @returns The target modular blocks field.
*/
function findTargetModularBlocksField(field: any, targetSchema: any[]): any | undefined {
if (!Array.isArray(targetSchema) || !field || field?.data_type !== 'blocks') return undefined;

const byUid = targetSchema.find(
(mb: any) => mb?.data_type === 'blocks' && mb?.uid === field?.uid,
);
if (byUid) return byUid;

const fd = (field?.display_name ?? '').toString().trim().toLowerCase();
if (fd) {
const byName = targetSchema.find(
(mb: any) =>
mb?.data_type === 'blocks' &&
(mb?.display_name ?? '').toString().trim().toLowerCase() === fd,
);
if (byName) return byName;
}

return undefined;
}

/**
* Merge modular blocks preserving destination block order and UIDs:
* 1. Walk destination blocks — merge matching source blocks, clone unmapped ones.
* 2. Append source-only blocks (uids not on destination) at the end.
*/
function mergeModularBlocksFieldFromDestination(field: any, targetMB: any) {
const targetBlocks = targetMB?.blocks ?? [];
const sourceBlocks = field?.blocks ?? [];
if (!targetBlocks.length) return;

const resultBlocks: any[] = [];
const matchedSourceUids = new Set<string>();

for (const tb of targetBlocks) {
const sb = sourceBlocks.find((b: any) => b?.uid === tb?.uid);
if (sb) {
const tSch = tb?.schema ?? [];
const additional = tSch.filter(
(tField: any) =>
!(sb?.schema ?? []).some(
(sField: any) =>
sField?.uid === tField?.uid && sField?.data_type === tField?.data_type,
),
);
sb.schema = removeDuplicateFields([
...(sb?.schema ?? []),
...additional.map((f: any) => cloneSchemaBranch(f)),
]);
mergeSchemaFields(sb?.schema ?? [], tSch);
resultBlocks.push(sb);
if (sb?.uid) matchedSourceUids.add(sb?.uid);
} else {
resultBlocks.push(cloneSchemaBranch(tb));
}
}

for (const sb of sourceBlocks) {
if (sb?.uid && !matchedSourceUids.has(sb?.uid)) {
resultBlocks.push(sb);
}
}

field.blocks = removeDuplicateFields(resultBlocks);
}

function mergeSchemaFields(sourceSchema: any[], targetSchema: any[]) {
for (const field of sourceSchema) {
if (field?.data_type === 'group') {
Expand All @@ -1119,27 +1232,10 @@ function mergeSchemaFields(sourceSchema: any[], targetSchema: any[]) {
}

if (field?.data_type === 'blocks') {
const targetMB = targetSchema?.find((mb: any) =>
mb?.uid === field?.uid && mb?.data_type === 'blocks'
);
const targetMB = findTargetModularBlocksField(field, targetSchema ?? []);

if (targetMB?.blocks) {
for (const sourceBlock of field?.blocks ?? []) {
const targetBlock = targetMB?.blocks?.find((tb: any) => tb?.uid === sourceBlock?.uid);

if (targetBlock?.schema) {
const additional = (targetBlock?.schema ?? [])?.filter((tField: any) =>
!sourceBlock?.schema?.find((sField: any) => sField?.uid === tField?.uid && sField?.data_type === tField?.data_type)
);
sourceBlock.schema = removeDuplicateFields([...sourceBlock?.schema ?? [], ...additional]);
mergeSchemaFields(sourceBlock.schema, targetBlock.schema ?? []);
}
}

const additionalBlocks = (targetMB?.blocks ?? []).filter((tb: any) =>
!field?.blocks?.find((sb: any) => sb?.uid === tb?.uid)
);
field.blocks = removeDuplicateFields([...field?.blocks ?? [], ...additionalBlocks]);
if (targetMB?.blocks?.length) {
mergeModularBlocksFieldFromDestination(field, targetMB);
}
}
}
Expand Down
24 changes: 12 additions & 12 deletions app.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"timestamp": "2026-02-23T07:26:46.225Z",
"timestamp": "2026-04-08T12:40:32.171Z",
"region": {
"key": "NA",
"name": "North America",
Expand All @@ -14,21 +14,21 @@
}
},
"user": {
"email": "user@example.com",
"uid": "user-uid"
"email": "shobhit.upadhyay@contentstack.com",
"uid": "bltf56d4d5438c9db89"
},
"organization": {
Comment on lines 16 to 20
"name": "Organization Name",
"uid": "organization-uid"
"name": "TSO Migrations",
"uid": "bltd69612a298da913b"
},
"app": {
"name": "Migration Tool",
"uid": "app-uid",
"uid": "69d49c72876ea7f6d37da38f",
"manifest": "Migration Tool"
},
"oauthData": {
"client_id": "client-id",
"client_secret": "client-secret",
"client_id": "enc:23075d487510c9d86ab0ce0b:8f83bd81b7f701f27752e30a6b3ef7de:4420233756ac0d76b87b0dd6baf45d04",
"client_secret": "enc:8171ebf9f22aec92c95bed05:645cb1b32ec31b5a3b5af9a465c82e13:b28516543a6699adfec4d9cccb6fba2d6a7a22be16e1231a393b99bcc2e84bcc",
"redirect_uri": "http://localhost:5001/v2/auth/save-token",
Comment on lines 29 to 32
"user_token_config": {
"enabled": true,
Expand Down Expand Up @@ -182,9 +182,9 @@
}
},
"pkce": {
"code_verifier": "code-verifier",
"code_challenge": "code-challenge"
"code_verifier": "enc:0cc75ad8a6cd4e3437048935:2cd0ed224788349bed069e8440ab436e:3a2a49c71de361cdb38d76ecb27fb2a18b0862a4148a9fe85caf80169793645227a608a5ce3cffc7412cfb4805f51f57457f636c7b37da7857b3ed45686dc64d",
"code_challenge": "enc:e7c2a54b8b29bd025cf2d330:29cd985bcd086af15f6d537f233e174a:85468ed9162da323faf869ce8c84831f36d32e7a5f8156366b3abd460b755a1bdbc4e45ad3664a15f1506b"
},
"authUrl": "auth-url",
"isDefault": true
"authUrl": "https://app.contentstack.com/#!/apps/69d49c72876ea7f6d37da38f/authorize?response_type=code&client_id=rJ0MquYn-eviy7Dq&redirect_uri=http%3A%2F%2Flocalhost%3A5001%2Fv2%2Fauth%2Fsave-token&code_challenge=8HsWZK6XE2O_yyLhKw1CBwxaPeXnAnSUyjQC_Sq0spE&code_challenge_method=S256",
"isDefault": false
}
Loading
Loading