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
3 changes: 2 additions & 1 deletion api/src/models/EntryMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { DATABASE_FILES } from "../constants/index.js";
* Represents an entry mapper object.
*/
export interface EntryMapper {
entry_mapper: {
entry_mapper: {
id: string;
projectId: string;
contentTypeId: string;
Expand All @@ -18,6 +18,7 @@ export interface EntryMapper {
isUpdate: boolean;
contentstackEntryUid: string;
isDuplicateEntry: boolean;
language?: string;
}[];
}

Expand Down
4 changes: 4 additions & 0 deletions api/src/models/project-lowdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ interface Project {
taxonomies?: any[];
isSSO: boolean;
iteration: number;
master_locale?: Record<string, string>;
locales?: Record<string, string>;
source_locales?: string[];
migrated_locales?: string[];
}

interface ProjectDocument {
Expand Down
8 changes: 7 additions & 1 deletion api/src/models/uidMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ import { DATABASE_FILES } from "../constants/index.js";
interface EntryMapper {
entry: Record<string, any>;
assets: Record<string, any>;
/**
* Per-locale entry uid mapping. Source uid β†’ destination uid, scoped by destination
* locale code. Populated by writePerLocaleEntryUidMapping after each CLI import so the
* delta flow can tell which entries actually have a variant in a given locale.
*/
entryByLocale?: Record<string, Record<string, string>>;
}

const defaultData: EntryMapper = { entry: {}, assets: {} };
const defaultData: EntryMapper = { entry: {}, assets: {}, entryByLocale: {} };

/**
* Creates and returns a database instance for the field mapper for a specific project.
Expand Down
27 changes: 23 additions & 4 deletions api/src/services/contentMapper.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import getEntryMapperDb, { EntryMapper } from "../models/EntryMapper.js";
import getContentTypesMapperDb, { ContentTypesMapper } from "../models/contentTypesMapper-lowdb.js";
import getUidMapperDb from "../models/uidMapper.js";
import { isDuplicateEntry } from '../utils/entry-duplicate.utils.js';
import { getSourceLocaleForDestination } from '../utils/locale-migration.utils.js';


const idCorrector = ({ id }: { id: string }) => {
Expand Down Expand Up @@ -1952,6 +1953,8 @@ const updateEntryStatus = async (req: Request) => {
const EntryMapperModel = getEntryMapperDb(projectId, iteration);
await EntryMapperModel.read();
const foundEntry: EntryMapper[] = [];
// Rows in entry_mapper are already per-(entry Γ— source-locale), so each id uniquely
// identifies one locale variant; toggling isUpdate directly is correct.
await EntryMapperModel.update((data: any) => {
data?.entry_mapper?.forEach((entry: any) => {
if (validatedUids.includes(entry?.id)) {
Expand Down Expand Up @@ -2000,6 +2003,8 @@ const getEntryMapping = async (req: Request) => {
const skip: any = req?.params?.skip;
const limit: any = req?.params?.limit;
const search: string = req?.params?.searchText?.toLowerCase();
const locale: string | undefined =
(req?.query?.locale as string) || (req?.params as any)?.locale;

let result: any[] = [];
let filteredResult = [];
Expand Down Expand Up @@ -2033,6 +2038,11 @@ const getEntryMapping = async (req: Request) => {
}
const EntryMapperModel = getEntryMapperDb(projectId, iteration);
await EntryMapperModel.read();
// Convert the destination locale param (e.g. "en-in") to its source locale code
// (e.g. "en-IN") so we can filter the per-source-locale entry_mapper rows.
const sourceLocale = locale
? getSourceLocaleForDestination(projectData ?? {}, locale)
: null;
let entryMapping = contentType?.entryMapping?.map?.((mapperUId: any) => {
const entryMapper = EntryMapperModel.chain
.get("entry_mapper")
Expand Down Expand Up @@ -2062,16 +2072,25 @@ const getEntryMapping = async (req: Request) => {
entryMapping ?? [],
);

if (!isEmpty(enrichedMapping)) {
// entry_mapper rows are already per-source-locale (one row per language variant).
// Filter to just the rows whose `language` matches the selected destination locale's
// source code. Falls open when no locale is provided so legacy callers still work.
const localeFiltered = sourceLocale
? (enrichedMapping ?? []).filter(
(row: any) => row && (row?.language ?? '') === sourceLocale,
)
: enrichedMapping;

if (!isEmpty(localeFiltered)) {
if (search) {
filteredResult = enrichedMapping?.filter?.((item: any) =>
filteredResult = localeFiltered?.filter?.((item: any) =>
item?.entryName?.toLowerCase().includes(search)
);
totalCount = filteredResult?.length;
result = filteredResult?.slice(skip, Number(skip) + Number(limit));
} else {
totalCount = enrichedMapping?.length;
result = enrichedMapping?.slice(skip, Number(skip) + Number(limit));
totalCount = localeFiltered?.length;
result = localeFiltered?.slice(skip, Number(skip) + Number(limit));
}
}
return {
Expand Down
16 changes: 15 additions & 1 deletion api/src/services/runCli.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface TestStack {
isMigrated: boolean;
}
import { setBasicAuthConfig, setOAuthConfig } from '../utils/config-handler.util.js';
import writeUidMapping from '../utils/uid-mapper.utils.js';
import writeUidMapping, { writePerLocaleEntryUidMapping } from '../utils/uid-mapper.utils.js';

/**
* Determines log level based on message content without removing ANSI codes
Expand Down Expand Up @@ -277,6 +277,7 @@ export const runCli = async (
.value();
const iteration = projectData?.iteration || 1;
await writeUidMapping(backupPath, projectId, iteration);
await writePerLocaleEntryUidMapping(backupPath, projectId, iteration);
}

// Keep the project status update code:
Expand Down Expand Up @@ -321,6 +322,19 @@ export const runCli = async (
ProjectModelLowdb.data.projects[projectIndex].current_step =
getStepperSteps(ProjectModelLowdb.data.projects[projectIndex]?.iteration).MIGRATION;
ProjectModelLowdb.data.projects[projectIndex].status = 5;
// Record every locale that just successfully migrated so the next delta restart can
// tell which locales need a full pass vs delta. Set-union with prior value.
const proj: any = ProjectModelLowdb.data.projects[projectIndex];
const ranLocales = Array.from(
new Set([
...Object.keys(proj?.master_locale ?? {}),
...Object.keys(proj?.locales ?? {}),
]),
);
const existing: string[] = Array.isArray(proj?.migrated_locales)
? proj.migrated_locales
: [];
proj.migrated_locales = Array.from(new Set([...existing, ...ranLocales]));
await ProjectModelLowdb.write();
}
} else {
Expand Down
51 changes: 51 additions & 0 deletions api/src/services/wordpress.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,39 @@ async function saveEntry(fields: any, entry: any, file_path: string, assetData
}
return entryData;
}
/**
* Replicate the master-locale entries JSON + index into each additional destination locale
* folder so the CLI import creates entry variants under every mapped locale, not just master.
* WP's WXR format doesn't carry per-locale content, so each locale gets the same payload β€”
* Contentstack's fallback chain handles the read-side behavior and the user can edit the
* variants later.
*/
async function fanOutEntriesToAdditionalLocales(
postFolderPath: string,
masterLocaleCode: string,
contentTypeRoot: string,
project: any,
): Promise<void> {
const additional = project?.locales ?? {};
const masterFilePath = path.join(postFolderPath, `${masterLocaleCode}.json`);
if (!existsSync(masterFilePath)) return;
const masterContent = await fs.promises.readFile(masterFilePath, 'utf-8');
for (const destLocale of Object.keys(additional)) {
if (!destLocale || destLocale === masterLocaleCode) continue;
const localeFolderPath = path.join(contentTypeRoot, destLocale);
if (!existsSync(localeFolderPath)) {
await fs.promises.mkdir(localeFolderPath, { recursive: true });
}
const localeFilePath = path.join(localeFolderPath, `${destLocale}.json`);
await fs.promises.writeFile(localeFilePath, masterContent, 'utf-8');
await fs.promises.writeFile(
path.join(localeFolderPath, 'index.json'),
JSON.stringify({ '1': `${destLocale}.json` }, null, 4),
'utf-8',
);
}
}

async function createEntry(file_path: string, packagePath: string, destinationStackId: string, projectId: string, contentTypes: any, mapperKeys: any, master_locale: string, project: any){
const locale = getLocale(master_locale, project) || master_locale;
const Jsondata = await fs.promises.readFile(packagePath, "utf8");
Expand Down Expand Up @@ -1355,6 +1388,12 @@ async function createEntry(file_path: string, packagePath: string, destinationSt
await fs.promises.writeFile(path.join(postFolderPath, "index.json"),
JSON.stringify({ "1": `${locale}.json` }, null, 4), "utf-8"
);
await fanOutEntriesToAdditionalLocales(
postFolderPath,
locale,
path.join(MIGRATION_DATA_CONFIG.DATA, destinationStackId, MIGRATION_DATA_CONFIG.ENTRIES_DIR_NAME, postsFolderName),
project,
);
}

const termsContentTypes = contentTypes?.filter((contentType: any) => contentType?.contentstackUid === 'terms');
Expand All @@ -1376,6 +1415,12 @@ async function createEntry(file_path: string, packagePath: string, destinationSt
await fs.promises.writeFile(path.join(termsFolderPath, "index.json"),
JSON.stringify({ "1": `${locale}.json` }, null, 4), "utf-8"
);
await fanOutEntriesToAdditionalLocales(
termsFolderPath,
locale,
path.join(MIGRATION_DATA_CONFIG.DATA, destinationStackId, MIGRATION_DATA_CONFIG.ENTRIES_DIR_NAME, termsFolderName),
project,
);
}
const postContentTypes = contentTypes?.filter(
(contentType: any) =>
Expand Down Expand Up @@ -1412,6 +1457,12 @@ async function createEntry(file_path: string, packagePath: string, destinationSt
await fs.promises.writeFile(path.join(postFolderPath, "index.json"),
JSON.stringify({ "1": `${locale}.json` }, null, 4), "utf-8"
);
await fanOutEntriesToAdditionalLocales(
postFolderPath,
locale,
path.join(MIGRATION_DATA_CONFIG.DATA, destinationStackId, MIGRATION_DATA_CONFIG.ENTRIES_DIR_NAME, postsFolderName),
project,
);
console.info(`Processed content for ${contentType?.contentstackTitle}:`, Object?.keys(content)?.length, "items");
}
}
Expand Down
33 changes: 22 additions & 11 deletions api/src/utils/entry-update-script.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const FLAT_PAYLOAD_SKIP = new Set([
'updated_by',
'_content_type_uid',
'content',
'__locale',
'__csUid',
]);

/**
Expand Down Expand Up @@ -66,7 +68,7 @@ const resolveAssetField = (fieldName, entryUid, updateValue, stackValue, oldMapp
* WordPress (and similar) write migration JSON with fields at the root (email, url, …).
* Fetched stack entries keep custom fields under entry.content β€” merge flat updateData there.
*/
const mergeFlatPayloadIntoEntry = async (entry, entryUid, updateData, oldMapping, newMapping) => {
const mergeFlatPayloadIntoEntry = async (entry, entryUid, updateData, oldMapping, newMapping, updateOpts) => {
for (const field of Object.keys(updateData)) {
if (FLAT_PAYLOAD_SKIP.has(field)) {
continue;
Expand All @@ -90,7 +92,7 @@ const mergeFlatPayloadIntoEntry = async (entry, entryUid, updateData, oldMapping
}
entry.content[field] = nextVal;
}
await entry.update();
await entry.update(updateOpts);
};

module.exports = async ({
Expand Down Expand Up @@ -120,13 +122,22 @@ module.exports = async ({
console.info(`Processing content type: ${contentType}, entries: ${entryUids.length}`);

for (const entryUid of entryUids) {
const updateData = JSON.parse(JSON.stringify(config[contentType][entryUid]));
// Per-locale config keys are "<csUid>::<locale>" with __locale/__csUid
// on the payload. Fall back to the bare key for legacy single-locale
// configs.
const locale = updateData?.__locale;
const realEntryUid = updateData?.__csUid || entryUid;
delete updateData?.__locale;
delete updateData?.__csUid;
const fetchOpts = locale ? { locale } : undefined;
const updateOpts = locale ? { locale } : undefined;

const entryRef = stackSDKInstance
.contentType(contentType)
.entry(entryUid);
.entry(realEntryUid);


const entry = await entryRef?.fetch();
const updateData = JSON.parse(JSON.stringify(config[contentType][entryUid]));
const entry = await entryRef?.fetch(fetchOpts);

const hasStackContent = entry?.content && typeof entry?.content === 'object';
const hasNestedUpdate = updateData?.content && typeof updateData?.content === 'object';
Expand All @@ -145,10 +156,10 @@ module.exports = async ({
}
}
Object.assign(entry?.content, updateData?.content);
await entry.update();
await entry.update(updateOpts);
} else if (hasStackContent) {
console.info(`[${entryUid}] Merging flat migration payload into entry.content (e.g. WordPress export)`);
await mergeFlatPayloadIntoEntry(entry, entryUid, updateData, oldMapping, newMapping);
console.info(`[${realEntryUid}] Merging flat migration payload into entry.content (e.g. WordPress export)${locale ? ` for locale "${locale}"` : ''}`);
await mergeFlatPayloadIntoEntry(entry, realEntryUid, updateData, oldMapping, newMapping, updateOpts);
} else {
if (updateData && entry) {
for (const field of Object.keys(updateData)) {
Expand All @@ -165,9 +176,9 @@ module.exports = async ({
}
}
Object.assign(entry, updateData);
await entry.update();
await entry.update(updateOpts);
}
console.info(`Updated entry: ${entryUid}`);
console.info(`Updated entry: ${realEntryUid}${locale ? ` (locale "${locale}")` : ''}`);
}
}
console.info('All entries updated successfully');
Expand Down
Loading
Loading