diff --git a/README.md b/README.md index 5cf73e39..d74e4288 100644 --- a/README.md +++ b/README.md @@ -300,7 +300,7 @@ agility-files/ > **Recommended Practices:** > > - **Persist your mappings** through shared file storage or a repository (e.g., Git) when working on a team -> - **Do not have multiple instances of the CLI syncing the same source→target instance pairs simultaneously** - this can cause mapping conflicts and duplicate content +> - **Do not run multiple CLI processes against the same source→target pair at the same time** - each invocation handles exactly one source/target pair; running two overlapping processes against that same pair can still cause mapping conflicts and duplicate content > - **Back up your `agility-files/mappings/` directory** before performing destructive operations ## File Structure diff --git a/src/core/auth.ts b/src/core/auth.ts index 1cc72a04..5a7f6dc1 100644 --- a/src/core/auth.ts +++ b/src/core/auth.ts @@ -131,7 +131,7 @@ export class Auth { determineBaseUrl(guid?: string): string { let baseGUID = guid; if (!baseGUID) { - baseGUID = state.sourceGuid[0]; + baseGUID = state.sourceGuid; } if (state.dev) { @@ -167,7 +167,7 @@ export class Auth { determineFetchUrl(guid?: string): string { let baseGUID = guid; if (!baseGUID) { - baseGUID = state.sourceGuid[0]; + baseGUID = state.sourceGuid; } // Content Fetch API URLs are determined by GUID suffix only @@ -200,7 +200,7 @@ export class Auth { determineCloudMgmtUrl(guid?: string): string { let baseGUID = guid; if (!baseGUID) { - baseGUID = state.sourceGuid[0]; + baseGUID = state.sourceGuid; } // Cloud Management API URLs are determined by GUID suffix only @@ -351,18 +351,25 @@ export class Auth { } } - // Step 3: Get API keys for all GUIDs - const allGuids = [...state.sourceGuid, ...state.targetGuid]; - state.apiKeys = []; + // Step 3: Get API keys for the source and target GUIDs + state.sourceApiKeys = null; + state.targetApiKeys = null; const failedGuids: string[] = []; - for (const guid of allGuids) { + for (const [role, guid] of [ + ["source", state.sourceGuid], + ["target", state.targetGuid], + ] as const) { if (guid) { try { const previewKey = await this.getPreviewKey(guid); const fetchKey = await this.getFetchKey(guid); - state.apiKeys.push({ guid, previewKey, fetchKey }); + if (role === "source") { + state.sourceApiKeys = { previewKey, fetchKey }; + } else { + state.targetApiKeys = { previewKey, fetchKey }; + } } catch (error) { failedGuids.push(guid); } @@ -401,9 +408,9 @@ export class Auth { state.cachedApiClient = new mgmtApi.ApiClient(state.mgmtApiOptions); // Load user data for interactive prompts and general use - if (state.sourceGuid.length > 0) { + if (state.sourceGuid) { try { - const primaryGuid = state.sourceGuid[0]; + const primaryGuid = state.sourceGuid; const user = await this.getUser(primaryGuid); if (user) { state.user = user; @@ -415,13 +422,13 @@ export class Auth { } } - // Step 6: Auto-detect available locales for ALL GUIDs in the matrix - if (allGuids.length > 0) { + // Step 6: Auto-detect available locales for the source and target GUIDs + if (state.sourceGuid || state.targetGuid) { try { //Get the locales for the SOURCE GUID let sourceLocales: string[] = []; - if (state.sourceGuid.length > 0) { - sourceLocales = (await state.cachedApiClient.instanceMethods.getLocales(state.sourceGuid[0])).map( + if (state.sourceGuid) { + sourceLocales = (await state.cachedApiClient.instanceMethods.getLocales(state.sourceGuid)).map( (locale: any) => locale.localeCode ); state.availableLocales = sourceLocales; @@ -429,8 +436,8 @@ export class Auth { //Get the locales for the TARGET GUID let targetLocales: string[] = []; - if (state.targetGuid.length > 0) { - targetLocales = (await state.cachedApiClient.instanceMethods.getLocales(state.targetGuid[0])).map( + if (state.targetGuid) { + targetLocales = (await state.cachedApiClient.instanceMethods.getLocales(state.targetGuid)).map( (locale: any) => locale.localeCode ); @@ -451,7 +458,7 @@ export class Auth { const validationScope = state.locale.length > 0 ? "specified" : "source"; console.log( ansiColors.yellow( - `⚠️ Target instance ${state.targetGuid[0]}: Missing ${validationScope} locales ${missingLocales.join(", ")} (available: ${targetLocales.join(", ")})` + `⚠️ Target instance ${state.targetGuid}: Missing ${validationScope} locales ${missingLocales.join(", ")} (available: ${targetLocales.join(", ")})` ) ); return false; // Cannot proceed with missing locales @@ -465,7 +472,7 @@ export class Auth { if (validLocales.length === 0) { console.log( ansiColors.yellow( - `⚠️ None of the specified locales exist in the source instance ${state.sourceGuid[0]}. Using all available locales.` + `⚠️ None of the specified locales exist in the source instance ${state.sourceGuid}. Using all available locales.` ) ); } else { @@ -474,11 +481,11 @@ export class Auth { } const guidLocaleMap = new Map(); - guidLocaleMap.set(state.sourceGuid[0], localesToUse); + guidLocaleMap.set(state.sourceGuid, localesToUse); - if (state.targetGuid.length > 0) { + if (state.targetGuid) { //if we have a target... - guidLocaleMap.set(state.targetGuid[0], localesToUse); + guidLocaleMap.set(state.targetGuid, localesToUse); } state.locale = localesToUse; // Set the state locale list to the determined locales @@ -499,11 +506,8 @@ export class Auth { if (state.locale.length > 0) { // User specified locales explicitly, use those const guidLocaleMap = new Map(); - for (const guid of allGuids) { - if (guid) { - guidLocaleMap.set(guid, state.locale); - } - } + if (state.sourceGuid) guidLocaleMap.set(state.sourceGuid, state.locale); + if (state.targetGuid) guidLocaleMap.set(state.targetGuid, state.locale); state.guidLocaleMap = guidLocaleMap; state.availableLocales = state.locale; console.log(`📝 Using user-specified locales: ${state.locale.join(", ")}`); @@ -995,7 +999,7 @@ export class Auth { // Check command-specific requirements switch (commandType) { case "pull": - if (!state.sourceGuid || state.sourceGuid.length === 0) + if (!state.sourceGuid) missingFields.push("sourceGuid (use --sourceGuid or AGILITY_GUID in .env)"); // Check for locales: either user-specified OR auto-detected per-GUID mappings @@ -1011,9 +1015,9 @@ export class Auth { case "push": case "sync": // Both push and sync require source and target GUIDs - if (!state.sourceGuid || state.sourceGuid.length === 0) + if (!state.sourceGuid) missingFields.push("sourceGuid (use --sourceGuid or AGILITY_GUID in .env)"); - if (!state.targetGuid || state.targetGuid.length === 0) + if (!state.targetGuid) missingFields.push("targetGuid (use --targetGuid or AGILITY_TARGET_GUID in .env)"); // Check for locales: either user-specified OR auto-detected per-GUID mappings @@ -1046,21 +1050,21 @@ export class Auth { // Validate instance access and set up API configuration try { - if (commandType === "sync" && state.targetGuid && state.targetGuid.length > 0) { - // Sync operation - validate access to both source and target (use first GUID for validation) + if (commandType === "sync" && state.targetGuid) { + // Sync operation - validate access to both source and target if (!state.isAgilityDev && !state.dev) { - await this.validateInstanceAccess(state.sourceGuid[0], "source"); + await this.validateInstanceAccess(state.sourceGuid, "source"); } - await this.validateInstanceAccess(state.targetGuid[0], "target"); + await this.validateInstanceAccess(state.targetGuid, "target"); - // Configure for target instance (sync writes to target - use first target GUID) - const targetBaseUrl = state.baseUrl || this.determineBaseUrl(state.targetGuid[0]); + // Configure for target instance (sync writes to target) + const targetBaseUrl = state.baseUrl || this.determineBaseUrl(state.targetGuid); state.mgmtApiOptions!.baseUrl = targetBaseUrl; state.baseUrl = targetBaseUrl; - // Get API keys for source instance (needed for pull phase of sync - use first source GUID) - const previewKey = await this.getPreviewKey(state.sourceGuid[0]); - const fetchKey = await this.getFetchKey(state.sourceGuid[0]); + // Get API keys for source instance (needed for pull phase of sync) + const previewKey = await this.getPreviewKey(state.sourceGuid); + const fetchKey = await this.getFetchKey(state.sourceGuid); state.previewKey = previewKey; state.fetchKey = fetchKey; @@ -1069,22 +1073,22 @@ export class Auth { if (!state.apiKeyForPull) { console.log( ansiColors.red( - `Could not retrieve the required API key (preview: ${state.preview}) for source instance ${state.sourceGuid[0]}. Check API key configuration in Agility.` + `Could not retrieve the required API key (preview: ${state.preview}) for source instance ${state.sourceGuid}. Check API key configuration in Agility.` ) ); return false; } - } else if (commandType === "pull" && state.sourceGuid && state.sourceGuid.length > 0) { - // Pull operation - validate source access and get API keys (use first source GUID for validation) - await this.validateInstanceAccess(state.sourceGuid[0], "instance"); + } else if (commandType === "pull" && state.sourceGuid) { + // Pull operation - validate source access and get API keys + await this.validateInstanceAccess(state.sourceGuid, "instance"); - const baseUrl = state.baseUrl || this.determineBaseUrl(state.sourceGuid[0]); + const baseUrl = state.baseUrl || this.determineBaseUrl(state.sourceGuid); state.mgmtApiOptions!.baseUrl = baseUrl; state.baseUrl = baseUrl; - // Get API keys for pull operations (use first source GUID) - const previewKey = await this.getPreviewKey(state.sourceGuid[0]); - const fetchKey = await this.getFetchKey(state.sourceGuid[0]); + // Get API keys for pull operations + const previewKey = await this.getPreviewKey(state.sourceGuid); + const fetchKey = await this.getFetchKey(state.sourceGuid); state.previewKey = previewKey; state.fetchKey = fetchKey; @@ -1093,7 +1097,7 @@ export class Auth { if (!state.apiKeyForPull) { console.log( ansiColors.red( - `Could not retrieve the required API key (preview: ${state.preview}) for instance ${state.sourceGuid[0]}. Check API key configuration in Agility.` + `Could not retrieve the required API key (preview: ${state.preview}) for instance ${state.sourceGuid}. Check API key configuration in Agility.` ) ); return false; diff --git a/src/core/batch-workflows.ts b/src/core/batch-workflows.ts index 7a1bc364..d9e89e2e 100644 --- a/src/core/batch-workflows.ts +++ b/src/core/batch-workflows.ts @@ -299,7 +299,7 @@ export async function batchWorkflow( if (!apiClient) { throw new Error("API client not available in state"); } - if (!targetGuid || targetGuid.length === 0) { + if (!targetGuid) { throw new Error("Target GUID not available in state"); } if (!locale) { @@ -313,8 +313,8 @@ export async function batchWorkflow( // Get batch ID immediately using returnBatchId=true const batchIdResult = type === "content" - ? await apiClient.contentMethods.batchWorkflowContent(ids, targetGuid[0], locale, operation, true) - : await apiClient.pageMethods.batchWorkflowPages(ids, targetGuid[0], locale, operation, true); + ? await apiClient.contentMethods.batchWorkflowContent(ids, targetGuid, locale, operation, true) + : await apiClient.pageMethods.batchWorkflowPages(ids, targetGuid, locale, operation, true); const batchID = Array.isArray(batchIdResult) ? batchIdResult[0] : batchIdResult; @@ -323,7 +323,7 @@ export async function batchWorkflow( } // Custom polling with batch ID tracking and progress display - const pollResult = await pollBatchWorkflow(batchID, targetGuid[0], type, ids.length); + const pollResult = await pollBatchWorkflow(batchID, targetGuid, type, ids.length); if (pollResult.success) { // Handle partial success diff --git a/src/core/logs.ts b/src/core/logs.ts index cd4ff13e..efbb92dd 100644 --- a/src/core/logs.ts +++ b/src/core/logs.ts @@ -457,7 +457,7 @@ export class Logs { if (this.logs.length > 0) { // Count GUID occurrences in log messages to identify which GUID this logger belongs to const guidCounts = new Map(); - const allGuids = [...(state.sourceGuid || []), ...(state.targetGuid || [])]; + const allGuids = [state.sourceGuid, state.targetGuid].filter(Boolean); this.logs.forEach((log) => { allGuids.forEach((guid) => { @@ -479,8 +479,8 @@ export class Logs { // Build filename with GUID if (this.operationType === "push" || this.operationType === "sync") { - const sourceGuid = state.sourceGuid?.[0] || "unknown"; - const targetGuid = state.targetGuid?.[0] || "unknown"; + const sourceGuid = state.sourceGuid || "unknown"; + const targetGuid = state.targetGuid || "unknown"; filename = `${sourceGuid}-${targetGuid}-${this.operationType}-${timestamp}.txt`; } else { // For pull operations, use the specific GUID this logger is for @@ -591,7 +591,7 @@ export class Logs { */ private initializeGuidColors(): void { const state = getState(); - const allGuids = [...(state.sourceGuid || []), ...(state.targetGuid || [])]; + const allGuids = [state.sourceGuid, state.targetGuid].filter(Boolean); // Assign unique colors to each GUID allGuids.forEach((guid, index) => { @@ -1045,8 +1045,8 @@ export class Logs { GUID: this.guid || "Not specified", "Operation Type": this.operationType, "Entity Type": this.entityType || "All entities", - "Source GUIDs": state.sourceGuid?.join(", ") || "None", - "Target GUIDs": state.targetGuid?.join(", ") || "None", + "Source GUID": state.sourceGuid || "None", + "Target GUID": state.targetGuid || "None", Locales: this.guid ? state.guidLocaleMap?.get(this.guid)?.join(", ") || "Not specified" : "Multiple", Channel: state.channel || "Not specified", Elements: state.elements || "All", diff --git a/src/core/publish.ts b/src/core/publish.ts index 8516a585..fd01c943 100644 --- a/src/core/publish.ts +++ b/src/core/publish.ts @@ -41,12 +41,12 @@ export class PublishService { constructor(options: PublishOptions = {}) { const state = getState(); - if (!state.targetGuid?.length) { + if (!state.targetGuid) { throw new Error("PublishService requires targetGuid to be set in state"); } this.apiClient = getApiClient(); - this.targetGuid = state.targetGuid[0]; + this.targetGuid = state.targetGuid; this.options = { verbose: false, ...options }; } diff --git a/src/core/pull.ts b/src/core/pull.ts index 17865161..ff9fda95 100644 --- a/src/core/pull.ts +++ b/src/core/pull.ts @@ -22,17 +22,12 @@ export class Pull { initializeLogger("pull"); } - // TODO: Add support for multiple GUIDs, multiple locales, multiple chanels - // Currently only supports one GUID, one locale, one channel - // Get all GUIDs to process (both source and target) - let allGuids = []; - if (fromPush === true) { - allGuids = [...state.sourceGuid, ...state.targetGuid]; - } else { - allGuids = [...state.sourceGuid]; - } + // Get the GUIDs to process: both source and target when called from push, source only for a standalone pull + const guidsToProcess: string[] = fromPush + ? [state.sourceGuid, state.targetGuid].filter(Boolean) + : [state.sourceGuid].filter(Boolean); - if (allGuids.length === 0) { + if (guidsToProcess.length === 0) { throw new Error("No GUIDs specified for pull operation"); } @@ -40,7 +35,7 @@ export class Pull { let totalOperations = 0; const operationDetails: string[] = []; - for (const guid of allGuids) { + for (const guid of guidsToProcess) { const guidLocales = state.guidLocaleMap.get(guid) || ["en-us"]; totalOperations += guidLocales.length; operationDetails.push(`${guid}: ${guidLocales.join(", ")}`); @@ -57,7 +52,7 @@ export class Pull { // This ensures we're pulling the latest data from the CDN // Skip when called from push - the refresh-mappings workflow handles this separately if (!fromPush) { - for (const guid of allGuids) { + for (const guid of guidsToProcess) { try { await waitForFetchApiSync(guid, "fetch", false); } catch (error: any) { diff --git a/src/core/push.ts b/src/core/push.ts index ecd53914..96d41c21 100644 --- a/src/core/push.ts +++ b/src/core/push.ts @@ -48,13 +48,8 @@ export class Push { initializeLogger(isSync ? "sync" : "push"); const logger = getLogger(); - // TODO: Add support for multiple GUIDs, multiple locales, multiple chanels - // Currently only supports one GUID, one locale, one channel - // Get all GUIDs to process (both source and target) - const allGuids = [...sourceGuid, ...targetGuid]; - - if (allGuids.length === 0) { - throw new Error("No GUIDs specified for push operation"); + if (!sourceGuid || !targetGuid) { + throw new Error("No source or target GUID specified for push operation"); } // IMPORTANT: For sync operations, we need ALL elements downloaded to enable proper change detection @@ -80,7 +75,7 @@ export class Push { // CONSOLE.LOG - Calculate total operations using per-GUID locale mapping let totalOperations = 0; const operationDetails: string[] = []; - for (const guid of allGuids) { + for (const guid of [sourceGuid, targetGuid]) { const guidLocales = state.guidLocaleMap.get(guid) || ["en-us"]; totalOperations += guidLocales.length; operationDetails.push(`${guid}: ${guidLocales.join(", ")}`); @@ -417,8 +412,8 @@ export class Push { // Refresh target instance data and update mappings after publishing // This ensures the mappings are up-to-date with the newly published content - const targetGuid = state.targetGuid?.[0]; - const sourceGuid = state.sourceGuid?.[0]; + const targetGuid = state.targetGuid; + const sourceGuid = state.sourceGuid; if (targetGuid && sourceGuid) { const hasPublishedItems = publishedContentIdsByLocale.size > 0 || publishedPageIdsByLocale.size > 0; diff --git a/src/core/state.ts b/src/core/state.ts index 410af6c5..0aaa1629 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -18,8 +18,8 @@ export interface State { verbose: boolean; // Instance/Connection - sourceGuid: string[]; // Array of source GUIDs - targetGuid: string[]; // Array of target GUIDs + sourceGuid: string; // Source instance GUID + targetGuid: string; // Target instance GUID locale: string[]; // Array of locales (for backward compatibility / user-specified) availableLocales: string[]; // Detected locales from getLocales() during auth guidLocaleMap: Map; // Per-GUID locale mapping for matrix operations @@ -65,7 +65,8 @@ export interface State { currentWebsite?: any; // API Keys for download operations (simplified approach) - apiKeys: Array<{ guid: string; previewKey: string; fetchKey: string }>; + sourceApiKeys: { previewKey: string; fetchKey: string } | null; + targetApiKeys: { previewKey: string; fetchKey: string } | null; // Cached API client instance (to prevent connection pool exhaustion) cachedApiClient?: mgmtApi.ApiClient; @@ -99,12 +100,13 @@ export const state: State = { verbose: false, // Instance/Connection - sourceGuid: [], - targetGuid: [], + sourceGuid: "", + targetGuid: "", locale: [], availableLocales: [], guidLocaleMap: new Map(), - apiKeys: [], + sourceApiKeys: null, + targetApiKeys: null, channel: "website", preview: true, elements: "Models,Galleries,Assets,Containers,Content,Templates,Pages,Sitemaps,UrlRedirections", @@ -161,31 +163,23 @@ export function setState(argv: any) { if (argv.headless !== undefined) state.headless = argv.headless; if (argv.verbose !== undefined) state.verbose = argv.verbose; - // Instance/Connection - Multi-GUID parsing logic + // Instance/Connection if (argv.sourceGuid !== undefined) { if (argv.sourceGuid.includes(",")) { - // Multi-GUID specification - state.sourceGuid = argv.sourceGuid - .split(",") - .map((g: string) => g.trim()) - .filter((g: string) => g.length > 0); - } else { - // Single GUID - state.sourceGuid = [argv.sourceGuid]; + throw new Error( + `--sourceGuid no longer supports multiple comma-separated GUIDs (got "${argv.sourceGuid}"). Each run now handles exactly one source instance.` + ); } + state.sourceGuid = argv.sourceGuid; } if (argv.targetGuid !== undefined) { if (argv.targetGuid.includes(",")) { - // Multi-GUID specification - state.targetGuid = argv.targetGuid - .split(",") - .map((g: string) => g.trim()) - .filter((g: string) => g.length > 0); - } else { - // Single GUID - state.targetGuid = [argv.targetGuid]; + throw new Error( + `--targetGuid no longer supports multiple comma-separated GUIDs (got "${argv.targetGuid}"). Each run now handles exactly one target instance.` + ); } + state.targetGuid = argv.targetGuid; } // Multi-locale parsing logic @@ -289,8 +283,8 @@ export function primeFromEnv(): { hasEnvFile: boolean; primedValues: string[] } }; // Only prime state values that aren't already set from command line - if (envVars.AGILITY_GUID && envVars.AGILITY_GUID[1] && state.sourceGuid.length === 0) { - state.sourceGuid = [envVars.AGILITY_GUID[1].trim()]; + if (envVars.AGILITY_GUID && envVars.AGILITY_GUID[1] && !state.sourceGuid) { + state.sourceGuid = envVars.AGILITY_GUID[1].trim(); primedValues.push("sourceGuid"); } @@ -326,8 +320,8 @@ export function primeFromEnv(): { hasEnvFile: boolean; primedValues: string[] } } // Additional system args - if (envVars.AGILITY_TARGET_GUID && envVars.AGILITY_TARGET_GUID[1] && state.targetGuid.length === 0) { - state.targetGuid = [envVars.AGILITY_TARGET_GUID[1].trim()]; + if (envVars.AGILITY_TARGET_GUID && envVars.AGILITY_TARGET_GUID[1] && !state.targetGuid) { + state.targetGuid = envVars.AGILITY_TARGET_GUID[1].trim(); primedValues.push("targetGuid"); } @@ -380,12 +374,13 @@ export function resetState() { state.verbose = false; // Instance/Connection - state.sourceGuid = []; - state.targetGuid = []; + state.sourceGuid = ""; + state.targetGuid = ""; state.locale = []; state.availableLocales = []; state.guidLocaleMap = new Map(); - state.apiKeys = []; + state.sourceApiKeys = null; + state.targetApiKeys = null; state.channel = "website"; state.preview = true; state.elements = "Models,Galleries,Assets,Containers,Content,Templates,Pages,Sitemaps,UrlRedirections"; @@ -500,15 +495,23 @@ export function getUIMode() { * Get API keys for a specific GUID */ export function getApiKeysForGuid(guid: string): { previewKey: string; fetchKey: string } | null { - const apiKeyEntry = state.apiKeys.find((item) => item.guid === guid); - return apiKeyEntry ? { previewKey: apiKeyEntry.previewKey, fetchKey: apiKeyEntry.fetchKey } : null; + if (guid === state.sourceGuid) return state.sourceApiKeys; + if (guid === state.targetGuid) return state.targetApiKeys; + return null; } /** * Get all API keys */ export function getAllApiKeys(): Array<{ guid: string; previewKey: string; fetchKey: string }> { - return state.apiKeys; + const keys: Array<{ guid: string; previewKey: string; fetchKey: string }> = []; + if (state.sourceGuid && state.sourceApiKeys) { + keys.push({ guid: state.sourceGuid, ...state.sourceApiKeys }); + } + if (state.targetGuid && state.targetApiKeys) { + keys.push({ guid: state.targetGuid, ...state.targetApiKeys }); + } + return keys; } /** @@ -615,32 +618,6 @@ export function finalizeGuidLogger(guid: string): string | null { return null; } -/** - * Save and clear all GUID loggers and merge into global log - */ -export function finalizeAllGuidLoggers(): string[] { - const results: string[] = []; - - if (state.loggerRegistry) { - const entries = Array.from(state.loggerRegistry.entries()); - - for (const [guid, logger] of entries) { - const logCount = logger.getLogCount(); - - if (logCount > 0) { - const result = logger.saveLogs(); - if (result) { - results.push(result); - console.log(`${result}`); - } - } - } - state.loggerRegistry.clear(); - } - - return results; -} - /** * Finalize and save the global logger */ diff --git a/src/core/system-args.ts b/src/core/system-args.ts index a3bebe1f..44518471 100644 --- a/src/core/system-args.ts +++ b/src/core/system-args.ts @@ -109,39 +109,15 @@ export const systemArgs = { // Instance identification args sourceGuid: { describe: - "The source Agility instance GUID — the instance you pull from (and the source for a sync). Comma-separated for multiple instances (e.g., 'guid1,guid2,guid3'). Required for pull and sync; falls back to AGILITY_GUID from your .env file when omitted.", - alias: [ - "source-guid", - "sourceguid", - "source", - "SourceGuid", - "SourceGUID", - "SOURCE", - "SOURCEGUID", - "sourceGuids", - "source-guids", - "SourceGuids", - "SOURCEGUIDS", - ], + "The source Agility instance GUID — the instance you pull from (and the source for a sync). Required for pull and sync; falls back to AGILITY_GUID from your .env file when omitted.", + alias: ["source-guid", "sourceguid", "source", "SourceGuid", "SourceGUID", "SOURCE", "SOURCEGUID"], demandOption: false, type: "string" as const, }, targetGuid: { describe: - "The target Agility instance GUID — the instance you push/sync to. Comma-separated for multiple instances (e.g., 'guid1,guid2,guid3'). Required for sync and push; falls back to AGILITY_TARGET_GUID from your .env file when omitted.", - alias: [ - "target-guid", - "targetguid", - "target", - "TargetGuid", - "TargetGUID", - "TARGET", - "TARGETGUID", - "targetGuids", - "target-guids", - "TargetGuids", - "TARGETGUIDS", - ], + "The target Agility instance GUID — the instance you push/sync to. Required for sync and push; falls back to AGILITY_TARGET_GUID from your .env file when omitted.", + alias: ["target-guid", "targetguid", "target", "TargetGuid", "TargetGUID", "TARGET", "TARGETGUID"], demandOption: false, type: "string" as const, }, diff --git a/src/core/tests/auth.test.ts b/src/core/tests/auth.test.ts index 810b2764..3aac1aa0 100644 --- a/src/core/tests/auth.test.ts +++ b/src/core/tests/auth.test.ts @@ -88,7 +88,7 @@ describe("Auth.determineBaseUrl", () => { expect(auth.determineBaseUrl()).toBe("https://mgmt.aglty.io"); }); - it("falls back to sourceGuid[0] when no explicit guid is provided", () => { + it("falls back to sourceGuid when no explicit guid is provided", () => { setState({ sourceGuid: "my-guid-c" }); const auth = new Auth(); expect(auth.determineBaseUrl()).toBe("https://mgmt-ca.aglty.io"); diff --git a/src/core/tests/push.test.ts b/src/core/tests/push.test.ts index 03504dde..cbc72881 100644 --- a/src/core/tests/push.test.ts +++ b/src/core/tests/push.test.ts @@ -25,16 +25,16 @@ describe("Push constructor", () => { describe("Push.pushInstances", () => { it("throws when neither sourceGuid nor targetGuid are set", async () => { const push = new Push(); - await expect(push.pushInstances()).rejects.toThrow("No GUIDs specified"); + await expect(push.pushInstances()).rejects.toThrow("No source or target GUID specified"); }); it("resolves (passes the GUID guard) when sourceGuid and targetGuid are both set", async () => { setState({ sourceGuid: "source-guid-u", targetGuid: "target-guid-u" }); const push = new Push(); - // Should not throw "No GUIDs specified" — it may resolve or fail later for other reasons + // Should not throw the GUID guard error — it may resolve or fail later for other reasons const result = await push.pushInstances().catch((err: Error) => err); if (result instanceof Error) { - expect(result.message).not.toContain("No GUIDs specified"); + expect(result.message).not.toContain("No source or target GUID specified"); } else { expect(result).toBeDefined(); } diff --git a/src/core/tests/state.test.ts b/src/core/tests/state.test.ts index b1dc06f3..15807204 100644 --- a/src/core/tests/state.test.ts +++ b/src/core/tests/state.test.ts @@ -27,27 +27,20 @@ beforeEach(() => { describe("setState – GUID parsing", () => { it("sets a single sourceGuid", () => { setState({ sourceGuid: "abc123u" }); - expect(getState().sourceGuid).toEqual(["abc123u"]); + expect(getState().sourceGuid).toBe("abc123u"); }); - it("splits comma-separated sourceGuids into an array", () => { - setState({ sourceGuid: "guid1u,guid2u, guid3u" }); - expect(getState().sourceGuid).toEqual(["guid1u", "guid2u", "guid3u"]); + it("throws when sourceGuid contains a comma", () => { + expect(() => setState({ sourceGuid: "guid1u,guid2u, guid3u" })).toThrow(); }); it("sets a single targetGuid", () => { setState({ targetGuid: "xyz789u" }); - expect(getState().targetGuid).toEqual(["xyz789u"]); + expect(getState().targetGuid).toBe("xyz789u"); }); - it("splits comma-separated targetGuids", () => { - setState({ targetGuid: "a1u,b2u" }); - expect(getState().targetGuid).toEqual(["a1u", "b2u"]); - }); - - it("ignores empty segments in comma-separated GUIDs", () => { - setState({ sourceGuid: "a1u,,b2u," }); - expect(getState().sourceGuid).toEqual(["a1u", "b2u"]); + it("throws when targetGuid contains a comma", () => { + expect(() => setState({ targetGuid: "a1u,b2u" })).toThrow(); }); }); @@ -144,8 +137,8 @@ describe("resetState", () => { it("clears sourceGuid and targetGuid", () => { setState({ sourceGuid: "abc", targetGuid: "xyz" }); resetState(); - expect(getState().sourceGuid).toEqual([]); - expect(getState().targetGuid).toEqual([]); + expect(getState().sourceGuid).toBe(""); + expect(getState().targetGuid).toBe(""); }); it("resets boolean flags to defaults", () => { @@ -287,10 +280,9 @@ describe("getContentCmsLink", () => { describe("getApiKeysForGuid / getAllApiKeys", () => { beforeEach(() => { - getState().apiKeys = [ - { guid: "guid-a", previewKey: "prev-a", fetchKey: "fetch-a" }, - { guid: "guid-b", previewKey: "prev-b", fetchKey: "fetch-b" }, - ]; + setState({ sourceGuid: "guid-a", targetGuid: "guid-b" }); + getState().sourceApiKeys = { previewKey: "prev-a", fetchKey: "fetch-a" }; + getState().targetApiKeys = { previewKey: "prev-b", fetchKey: "fetch-b" }; }); it("returns keys for a known GUID", () => { diff --git a/src/index.ts b/src/index.ts index 614cbc1d..43722af5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,7 @@ import { Pull } from "./core/pull"; import { Push } from "./core/push"; import { WorkflowOperation } from "./lib/workflows"; -import { initializeLogger, getLogger, finalizeLogger, finalizeAllGuidLoggers } from "./core/state"; +import { initializeLogger, getLogger, finalizeLogger } from "./core/state"; let auth: Auth; diff --git a/src/lib/downloaders/orchestrate-downloaders.ts b/src/lib/downloaders/orchestrate-downloaders.ts index a165a51a..e7556821 100644 --- a/src/lib/downloaders/orchestrate-downloaders.ts +++ b/src/lib/downloaders/orchestrate-downloaders.ts @@ -84,19 +84,19 @@ export class Downloader { } /** - * Orchestrate multiple GUIDs (DEFAULT METHOD) + * Orchestrate the source and target GUIDs (DEFAULT METHOD) * Uses sequential mode when --local flag is set to prevent overwhelming local API */ async instanceOrchestrator(fromPush: boolean): Promise { const state = getState(); - const allGuids = [...state.sourceGuid, ...state.targetGuid]; + const targets = [state.sourceGuid, state.targetGuid].filter(Boolean); - if (allGuids.length === 0) { + if (targets.length === 0) { throw new Error("No GUIDs available for download operation"); } // Start ALL downloads simultaneously (true parallel execution) for cloud APIs - const downloadTasks = allGuids.map((guid) => this.guidDownloader(guid, fromPush)); + const downloadTasks = targets.map((guid) => this.guidDownloader(guid, fromPush)); const results = await Promise.allSettled(downloadTasks); @@ -104,7 +104,7 @@ export class Downloader { const successfulResults: DownloadResults[] = []; const failedResults: Array<{ guid: string; error: string }> = []; - allGuids.forEach((guid, index) => { + targets.forEach((guid, index) => { const result = results[index]; if (result.status === "fulfilled") { successfulResults.push(result.value); diff --git a/src/lib/downloaders/tests/orchestrate-downloaders.test.ts b/src/lib/downloaders/tests/orchestrate-downloaders.test.ts index b1507da6..4c77cc8f 100644 --- a/src/lib/downloaders/tests/orchestrate-downloaders.test.ts +++ b/src/lib/downloaders/tests/orchestrate-downloaders.test.ts @@ -190,8 +190,12 @@ describe("Downloader.guidDownloader", () => { // ─── Downloader.instanceOrchestrator — parallel execution ───────────────────── describe("Downloader.instanceOrchestrator with GUIDs set", () => { - it("processes all GUIDs and returns one result per GUID", async () => { - setState({ sourceGuid: "guid-a-u,guid-b-u" }); + it("throws when sourceGuid contains a comma (multi-GUID no longer supported)", () => { + expect(() => setState({ sourceGuid: "guid-a-u,guid-b-u" })).toThrow(); + }); + + it("processes both source and target GUIDs and returns one result per GUID", async () => { + setState({ sourceGuid: "guid-a-u", targetGuid: "guid-b-u" }); const downloader = new Downloader(); const results = await downloader.instanceOrchestrator(false); diff --git a/src/lib/publishers/batch-publisher.ts b/src/lib/publishers/batch-publisher.ts index 2a513afb..a2cf7f92 100644 --- a/src/lib/publishers/batch-publisher.ts +++ b/src/lib/publishers/batch-publisher.ts @@ -16,14 +16,14 @@ export async function publishBatch(batchId: number): Promise<{ success: boolean; if (!apiClient) { throw new Error("API client not available in state"); } - if (!targetGuid?.length) { + if (!targetGuid) { throw new Error("Target GUID not available in state"); } // Try different batch publishing API methods depending on SDK version let result; - result = await apiClient.batchMethods.publishBatch(batchId, targetGuid[0], true); + result = await apiClient.batchMethods.publishBatch(batchId, targetGuid, true); return { success: true, diff --git a/src/lib/publishers/content-item-publisher.ts b/src/lib/publishers/content-item-publisher.ts index ffc91217..6e3cf466 100644 --- a/src/lib/publishers/content-item-publisher.ts +++ b/src/lib/publishers/content-item-publisher.ts @@ -25,14 +25,14 @@ export async function publishContentItem( if (!apiClient) { throw new Error("API client not available in state"); } - if (!targetGuid?.length) { + if (!targetGuid) { throw new Error("Target GUID not available in state"); } if (!locale) { throw new Error("Locale not available in state"); } - const result = await apiClient.contentMethods.publishContent(contentId, targetGuid[0], locale); + const result = await apiClient.contentMethods.publishContent(contentId, targetGuid, locale); return { success: true, diff --git a/src/lib/publishers/content-list-publisher.ts b/src/lib/publishers/content-list-publisher.ts index f5485ddf..204acffd 100644 --- a/src/lib/publishers/content-list-publisher.ts +++ b/src/lib/publishers/content-list-publisher.ts @@ -25,7 +25,7 @@ export async function publishContentList( if (!apiClient) { throw new Error("API client not available in state"); } - if (!targetGuid?.length) { + if (!targetGuid) { throw new Error("Target GUID not available in state"); } if (!locale) { @@ -33,7 +33,7 @@ export async function publishContentList( } // Content lists use the same publish API as content items - await apiClient.contentMethods.publishContent(contentListId, targetGuid[0], locale); + await apiClient.contentMethods.publishContent(contentListId, targetGuid, locale); return { success: true, diff --git a/src/lib/publishers/page-publisher.ts b/src/lib/publishers/page-publisher.ts index 175b5ece..207a65b3 100644 --- a/src/lib/publishers/page-publisher.ts +++ b/src/lib/publishers/page-publisher.ts @@ -19,14 +19,14 @@ export async function publishPage( if (!apiClient) { throw new Error("API client not available in state"); } - if (!targetGuid?.length) { + if (!targetGuid) { throw new Error("Target GUID not available in state"); } if (!locale) { throw new Error("Locale not available in state"); } - const result = await apiClient.pageMethods.publishPage(pageId, targetGuid[0], locale); + const result = await apiClient.pageMethods.publishPage(pageId, targetGuid, locale); return { success: true, diff --git a/src/lib/publishers/tests/content-item-publisher.test.ts b/src/lib/publishers/tests/content-item-publisher.test.ts index 85bfcf0d..263b92f7 100644 --- a/src/lib/publishers/tests/content-item-publisher.test.ts +++ b/src/lib/publishers/tests/content-item-publisher.test.ts @@ -64,7 +64,7 @@ describe("publishContentItem", () => { expect(result.error).toBeUndefined(); }); - it("calls contentMethods.publishContent with (contentId, targetGuid[0], locale)", async () => { + it("calls contentMethods.publishContent with (contentId, targetGuid, locale)", async () => { setState({ targetGuid: "my-target" }); const mockPublish = jest.fn().mockResolvedValue({}); jest.spyOn(require("core/state"), "getApiClient").mockReturnValue({ diff --git a/src/lib/publishers/tests/content-list-publisher.test.ts b/src/lib/publishers/tests/content-list-publisher.test.ts index d5e4bdaf..c9373a76 100644 --- a/src/lib/publishers/tests/content-list-publisher.test.ts +++ b/src/lib/publishers/tests/content-list-publisher.test.ts @@ -64,7 +64,7 @@ describe("publishContentList", () => { expect(result.error).toBeUndefined(); }); - it("calls contentMethods.publishContent with (contentListId, targetGuid[0], locale)", async () => { + it("calls contentMethods.publishContent with (contentListId, targetGuid, locale)", async () => { setState({ targetGuid: "list-target" }); const mockPublish = jest.fn().mockResolvedValue({}); jest.spyOn(require("core/state"), "getApiClient").mockReturnValue({ diff --git a/src/lib/publishers/tests/page-publisher.test.ts b/src/lib/publishers/tests/page-publisher.test.ts index 4cad7a14..6df854a9 100644 --- a/src/lib/publishers/tests/page-publisher.test.ts +++ b/src/lib/publishers/tests/page-publisher.test.ts @@ -64,7 +64,7 @@ describe("publishPage", () => { expect(result.error).toBeUndefined(); }); - it("calls pageMethods.publishPage with (pageId, targetGuid[0], locale)", async () => { + it("calls pageMethods.publishPage with (pageId, targetGuid, locale)", async () => { setState({ targetGuid: "page-target" }); const mockPublish = jest.fn().mockResolvedValue({}); jest.spyOn(require("core/state"), "getApiClient").mockReturnValue({ diff --git a/src/lib/pushers/asset-pusher.ts b/src/lib/pushers/asset-pusher.ts index b21e24b5..a7667355 100644 --- a/src/lib/pushers/asset-pusher.ts +++ b/src/lib/pushers/asset-pusher.ts @@ -78,7 +78,7 @@ export async function pushAssets( // Get state values and logger const { sourceGuid, targetGuid, locale, preview: isPreview } = state; - const logger = getLoggerForGuid(sourceGuid[0]); + const logger = getLoggerForGuid(sourceGuid); if (!assets || assets.length === 0) { logger.log("INFO", "No assets found to process."); @@ -90,11 +90,11 @@ export async function pushAssets( // Initialize reference mapper and asset mapper // const referenceMapper = new ReferenceMapperV2(); - const referenceMapper = new AssetMapper(sourceGuid[0], targetGuid[0]); + const referenceMapper = new AssetMapper(sourceGuid, targetGuid); let defaultContainer: mgmtApi.assetContainer | null = null; try { - defaultContainer = await apiClient.assetMethods.getDefaultContainer(targetGuid[0]); + defaultContainer = await apiClient.assetMethods.getDefaultContainer(targetGuid); } catch (err: any) { console.error("✗ Error fetching default asset container:", err.message); return { status: "error", successful: 0, failed: 0, skipped: 0 }; @@ -107,7 +107,7 @@ export async function pushAssets( let processedAssetsCount = 0; let overallStatus: "success" | "error" = "success"; - const fileOps = new fileOperations(sourceGuid[0]); + const fileOps = new fileOperations(sourceGuid); const basePath = fileOps.getDataFolderPath(); for (const media of assets) { @@ -138,7 +138,7 @@ export async function pushAssets( // If no mapping but asset exists by originKey in target, create mapping and skip if (!existingMapping && targetAssetByOriginKey) { referenceMapper.addMapping(media, targetAssetByOriginKey); - logger.asset.skipped(media, "already exists in target by path", targetGuid[0]); + logger.asset.skipped(media, "already exists in target by path", targetGuid); preflightReport.record({ phase: "Assets", action: "skip", @@ -178,8 +178,8 @@ export async function pushAssets( absoluteLocalFilePath, folderPath, apiClient, - sourceGuid[0], - targetGuid[0], + sourceGuid, + targetGuid, referenceMapper, logger ); @@ -196,8 +196,8 @@ export async function pushAssets( absoluteLocalFilePath, folderPath, apiClient, - sourceGuid[0], - targetGuid[0], + sourceGuid, + targetGuid, referenceMapper, logger ); @@ -206,7 +206,7 @@ export async function pushAssets( successful++; } else if (shouldSkip) { // Asset exists and is up to date - skip - logger.asset.skipped(media, "up to date, skipping", targetGuid[0]); + logger.asset.skipped(media, "up to date, skipping", targetGuid); preflightReport.record({ phase: "Assets", action: "skip", name: media.fileName, detail: "up to date" }); skipped++; } else if (isConflict) { @@ -221,7 +221,7 @@ export async function pushAssets( } } catch (error: any) { const errorMsg = extractErrorMessage(error); - logger.asset.error(media, errorMsg, targetGuid[0]); + logger.asset.error(media, errorMsg, targetGuid); failed++; currentStatus = "error"; diff --git a/src/lib/pushers/container-pusher.ts b/src/lib/pushers/container-pusher.ts index 218d9016..e7a2af49 100644 --- a/src/lib/pushers/container-pusher.ts +++ b/src/lib/pushers/container-pusher.ts @@ -31,7 +31,7 @@ export async function pushContainers( // Extract data from sourceData - unified parameter pattern const sourceContainers: mgmtApi.Container[] = sourceData || []; const { sourceGuid, targetGuid, cachedApiClient: apiClient, overwrite } = state; - const logger = getLoggerForGuid(sourceGuid[0]); + const logger = getLoggerForGuid(sourceGuid); if (!sourceContainers || sourceContainers.length === 0) { logger.log("INFO", "No containers found to process."); @@ -45,8 +45,8 @@ export async function pushContainers( let overallStatus: "success" | "error" = "success"; const failureDetails: FailureDetail[] = []; - const containerMapper = new ContainerMapper(sourceGuid[0], targetGuid[0]); - const modelMapper = new ModelMapper(sourceGuid[0], targetGuid[0]); + const containerMapper = new ContainerMapper(sourceGuid, targetGuid); + const modelMapper = new ModelMapper(sourceGuid, targetGuid); for (const sourceContainer of sourceContainers) { //SPECIAL CASE for fixed Agility containers @@ -115,9 +115,9 @@ export async function pushContainers( if (!state.preflight) { containerMapper.addMapping(sourceContainer, targetByRef); - cacheTargetContainer(targetGuid[0], targetByRef); + cacheTargetContainer(targetGuid, targetByRef); } - logger.container.skipped(sourceContainer, "already exists on target; mapping row created", targetGuid[0]); + logger.container.skipped(sourceContainer, "already exists on target; mapping row created", targetGuid); preflightReport.record({ phase: "Containers", action: "skip", @@ -142,7 +142,7 @@ export async function pushContainers( logger.container.skipped( sourceContainer, `target container: ${existingMapping.targetReferenceName} was deleted, skipping!`, - targetGuid[0] + targetGuid ); preflightReport.record({ phase: "Containers", @@ -165,7 +165,7 @@ export async function pushContainers( } if (targetModelID < 1) { - logger.container.skipped(sourceContainer, "Target model mapping not found", targetGuid[0]); + logger.container.skipped(sourceContainer, "Target model mapping not found", targetGuid); preflightReport.record({ phase: "Containers", action: "skip", @@ -175,7 +175,7 @@ export async function pushContainers( skipped++; } else if (shouldSkip) { // Container exists and is up to date - skip - logger.container.skipped(sourceContainer, "up to date, skipping", targetGuid[0]); + logger.container.skipped(sourceContainer, "up to date, skipping", targetGuid); preflightReport.record({ phase: "Containers", action: "skip", @@ -185,7 +185,7 @@ export async function pushContainers( skipped++; } else if (hasTargetChanges && !overwrite) { // Container exists and is up to date - skip - logger.container.error(sourceContainer, "Conflict detected, use --overwrite to force changes", targetGuid[0]); + logger.container.error(sourceContainer, "Conflict detected, use --overwrite to force changes", targetGuid); preflightReport.record({ phase: "Containers", action: "conflict", @@ -203,13 +203,13 @@ export async function pushContainers( sourceContainer, targetContainer, apiClient, - targetGuid[0], + targetGuid, targetModelID, logger ); if (updateResult) { - logger.container.updated(sourceContainer, "updated", targetGuid[0]); + logger.container.updated(sourceContainer, "updated", targetGuid); const sourceMapping = containerMapper.getContainerMapping(sourceContainer, "source"); const targetMapping = containerMapper.getContainerMapping(targetContainer, "target"); @@ -220,17 +220,17 @@ export async function pushContainers( } containerMapper.updateMapping(sourceContainer, updateResult, sourceMapping); - cacheTargetContainer(targetGuid[0], updateResult); + cacheTargetContainer(targetGuid, updateResult); successful++; } else { - logger.container.error(sourceContainer, "Failed to update container", targetGuid[0]); + logger.container.error(sourceContainer, "Failed to update container", targetGuid); failed++; currentStatus = "error"; overallStatus = "error"; failureDetails.push({ name: sourceContainer.referenceName, error: `Failed to update container "${sourceContainer.referenceName}" (ID: ${sourceContainer.contentViewID})`, - guid: sourceGuid[0], + guid: sourceGuid, }); } } @@ -239,7 +239,7 @@ export async function pushContainers( if (shouldCreate) { // Container doesn't exist - create new one if (targetModelID < 1) { - logger.container.skipped(sourceContainer, "Target model mapping not found", targetGuid[0]); + logger.container.skipped(sourceContainer, "Target model mapping not found", targetGuid); preflightReport.record({ phase: "Containers", action: "skip", @@ -256,38 +256,38 @@ export async function pushContainers( const createResult = await createNewContainer( sourceContainer, apiClient, - targetGuid[0], + targetGuid, targetModelID, logger ); if (createResult) { - logger.container.created(sourceContainer, "created", targetGuid[0]); + logger.container.created(sourceContainer, "created", targetGuid); containerMapper.addMapping(sourceContainer, createResult); - cacheTargetContainer(targetGuid[0], createResult); + cacheTargetContainer(targetGuid, createResult); successful++; } else { - logger.container.error(sourceContainer, "Failed to create container", targetGuid[0]); + logger.container.error(sourceContainer, "Failed to create container", targetGuid); failed++; currentStatus = "error"; overallStatus = "error"; failureDetails.push({ name: sourceContainer.referenceName, error: `Failed to create container "${sourceContainer.referenceName}"`, - guid: sourceGuid[0], + guid: sourceGuid, }); } } } } catch (error: any) { - logger.container.error(sourceContainer, error, targetGuid[0]); + logger.container.error(sourceContainer, error, targetGuid); failed++; currentStatus = "error"; overallStatus = "error"; failureDetails.push({ name: sourceContainer.referenceName, error: error?.message || String(error), - guid: sourceGuid[0], + guid: sourceGuid, }); } finally { processedCount++; diff --git a/src/lib/pushers/content-pusher/content-batch-processor.ts b/src/lib/pushers/content-pusher/content-batch-processor.ts index cbd4c48b..e57c8385 100644 --- a/src/lib/pushers/content-pusher/content-batch-processor.ts +++ b/src/lib/pushers/content-pusher/content-batch-processor.ts @@ -226,7 +226,7 @@ export class ContentBatchProcessor { item.originalContent, `Type: ${batchType} - created`, this.config.locale, - state.targetGuid[0] + state.targetGuid ); }); } @@ -235,7 +235,7 @@ export class ContentBatchProcessor { console.log(`❌ Batch ${batchNumber} failed items:`); batchResult.failedItems.forEach((item) => { // const modelName = item.originalContent.properties.definitionName || "Unknown"; - logger.content.error(item.originalContent, item.error, this.config.locale, state.targetGuid[0]); + logger.content.error(item.originalContent, item.error, this.config.locale, state.targetGuid); }); } diff --git a/src/lib/pushers/content-pusher/content-pusher.ts b/src/lib/pushers/content-pusher/content-pusher.ts index adc91ffd..e1ab92ac 100644 --- a/src/lib/pushers/content-pusher/content-pusher.ts +++ b/src/lib/pushers/content-pusher/content-pusher.ts @@ -18,10 +18,10 @@ export async function pushContent(sourceData: ContentItem[], targetData: Content const { ContentBatchProcessor } = await import("./content-batch-processor"); const { sourceGuid, targetGuid, overwrite, cachedApiClient: apiClient } = state; - const logger = getLoggerForGuid(sourceGuid[0]); + const logger = getLoggerForGuid(sourceGuid); - const sourceGuidStr = sourceGuid[0]; - const targetGuidStr = targetGuid[0]; + const sourceGuidStr = sourceGuid; + const targetGuidStr = targetGuid; const modelMapper = new ModelMapper(sourceGuidStr, targetGuidStr); const containerMapper = new ContainerMapper(sourceGuidStr, targetGuidStr); diff --git a/src/lib/pushers/content-pusher/util/change-detection.ts b/src/lib/pushers/content-pusher/util/change-detection.ts index e80c9ec0..4082175f 100644 --- a/src/lib/pushers/content-pusher/util/change-detection.ts +++ b/src/lib/pushers/content-pusher/util/change-detection.ts @@ -67,10 +67,8 @@ export function changeDetection( // and target version is newer than mapped target version //build the url to the source and target entity - //TODO: if there are multiple guids we need to handle that - - const sourceUrl = `https://app.agilitycms.com/instance/${state.sourceGuid[0]}/${locale}/content/listitem-${sourceEntity.contentID}`; - const targetUrl = `https://app.agilitycms.com/instance/${state.targetGuid[0]}/${locale}/content/listitem-${targetEntity.contentID}`; + const sourceUrl = `https://app.agilitycms.com/instance/${state.sourceGuid}/${locale}/content/listitem-${sourceEntity.contentID}`; + const targetUrl = `https://app.agilitycms.com/instance/${state.targetGuid}/${locale}/content/listitem-${targetEntity.contentID}`; if (overwrite) { return { diff --git a/src/lib/pushers/gallery-pusher.ts b/src/lib/pushers/gallery-pusher.ts index 512a4c4f..c792d4b0 100644 --- a/src/lib/pushers/gallery-pusher.ts +++ b/src/lib/pushers/gallery-pusher.ts @@ -44,7 +44,7 @@ export async function pushGalleries( const { sourceGuid, targetGuid, overwrite } = state; // Get the GUID logger from state instead of creating a new one - const logger = getLoggerForGuid(sourceGuid[0]) || new Logs("push", "gallery", sourceGuid[0]); + const logger = getLoggerForGuid(sourceGuid) || new Logs("push", "gallery", sourceGuid); if (!galleries || galleries.length === 0) { console.log("No galleries found to process."); @@ -54,7 +54,7 @@ export async function pushGalleries( // Get API client const apiClient = getApiClient(); - const referenceMapper = new GalleryMapper(sourceGuid[0], targetGuid[0]); + const referenceMapper = new GalleryMapper(sourceGuid, targetGuid); const totalGroupings = galleries.length; let successful = 0; @@ -76,7 +76,7 @@ export async function pushGalleries( if (!existingMapping && targetGalleryByName) { // Gallery exists in target by name but no mapping - add mapping and skip referenceMapper.addMapping(sourceGallery, targetGalleryByName); - logger.gallery.skipped(sourceGallery, "already exists in target by name", targetGuid[0]); + logger.gallery.skipped(sourceGallery, "already exists in target by name", targetGuid); preflightReport.record({ phase: "Galleries", action: "skip", @@ -94,7 +94,7 @@ export async function pushGalleries( if (state.preflight) { preflightReport.record({ phase: "Galleries", action: "create", name: sourceGallery.name }); } else { - await createGallery(sourceGallery, apiClient, targetGuid[0], referenceMapper, logger); + await createGallery(sourceGallery, apiClient, targetGuid, referenceMapper, logger); } successful++; } else if (existingMapping) { @@ -118,7 +118,7 @@ export async function pushGalleries( sourceGallery, existingMapping.targetMediaGroupingID, apiClient, - targetGuid[0], + targetGuid, referenceMapper, logger ); @@ -126,7 +126,7 @@ export async function pushGalleries( successful++; } else if (shouldSkip) { // Gallery exists and is up to date - skip - logger.gallery.skipped(sourceGallery, "up to date, skipping", targetGuid[0]); + logger.gallery.skipped(sourceGallery, "up to date, skipping", targetGuid); preflightReport.record({ phase: "Galleries", action: "skip", @@ -148,7 +148,7 @@ export async function pushGalleries( } } catch (error: any) { const errorMsg = extractErrorMessage(error); - logger.gallery.error(sourceGallery, errorMsg, targetGuid[0]); + logger.gallery.error(sourceGallery, errorMsg, targetGuid); failed++; currentStatus = "error"; overallStatus = "error"; diff --git a/src/lib/pushers/guid-data-loader.ts b/src/lib/pushers/guid-data-loader.ts index 3969decc..416bfb35 100644 --- a/src/lib/pushers/guid-data-loader.ts +++ b/src/lib/pushers/guid-data-loader.ts @@ -138,7 +138,7 @@ export class GuidDataLoader { // Apply model filtering if requested if (filterOptions) { - return await this.applyModelFiltering(guidEntities, filterOptions, locale, state.targetGuid[0], state.sourceGuid[0]); + return await this.applyModelFiltering(guidEntities, filterOptions, locale, state.targetGuid, state.sourceGuid); } return guidEntities; diff --git a/src/lib/pushers/model-pusher.ts b/src/lib/pushers/model-pusher.ts index 6bffe869..6a9f689e 100644 --- a/src/lib/pushers/model-pusher.ts +++ b/src/lib/pushers/model-pusher.ts @@ -73,7 +73,7 @@ async function findTargetModelAfterSave( export async function pushModels(sourceData: mgmtApi.Model[], targetData: mgmtApi.Model[]): Promise { const models: mgmtApi.Model[] = sourceData || []; const { sourceGuid, targetGuid } = state; - const logger = getLoggerForGuid(sourceGuid[0])!; + const logger = getLoggerForGuid(sourceGuid)!; const modelDefaults: string[] = [ "richtextarea", @@ -89,7 +89,7 @@ export async function pushModels(sourceData: mgmtApi.Model[], targetData: mgmtAp return { status: "success", successful: 0, failed: 0, skipped: 0 }; } - const referenceMapper = new ModelMapper(sourceGuid[0], targetGuid[0]); + const referenceMapper = new ModelMapper(sourceGuid, targetGuid); // PROD-1492: fail fast on stale duplicate model mappings. When a source model is deleted and // recreated (new ID, same reference name), the mapping ends up with two records sharing that name @@ -161,7 +161,7 @@ export async function pushModels(sourceData: mgmtApi.Model[], targetData: mgmtAp logger.model.skipped( sourceModel, "Model is missing required properties (id or referenceName), skipping", - targetGuid[0] + targetGuid ); skipped++; continue; @@ -211,7 +211,7 @@ export async function pushModels(sourceData: mgmtApi.Model[], targetData: mgmtAp new Error( `A target model named "${sourceModel.referenceName}" exists but is not mapped to source ID ${sourceModel.id} (likely a rename or reassignment of the source model).` ), - targetGuid[0] + targetGuid ); throw new Error( `Model validation failed: mapping inconsistency for model "${sourceModel.referenceName}" (ID: ${sourceModel.id}). ` + @@ -330,7 +330,7 @@ export async function pushModels(sourceData: mgmtApi.Model[], targetData: mgmtAp } for (const model of shouldCreateStub) { - const { result, error } = await createNewModel(model, referenceMapper, apiClient, targetGuid[0], logger); + const { result, error } = await createNewModel(model, referenceMapper, apiClient, targetGuid, logger); if (result === "created") { stubCreated.push(model); } else { @@ -342,7 +342,7 @@ export async function pushModels(sourceData: mgmtApi.Model[], targetData: mgmtAp error: error ? `Failed to create model "${model.referenceName}" (ID: ${model.id}): ${error}` : `Failed to create model "${model.referenceName}" (ID: ${model.id})`, - guid: sourceGuid[0], + guid: sourceGuid, }); } } @@ -355,7 +355,7 @@ export async function pushModels(sourceData: mgmtApi.Model[], targetData: mgmtAp sourceMapping.targetID, referenceMapper, apiClient, - targetGuid[0], + targetGuid, logger ); // PROD-2211: `updateExistingModel` returns the string "updated" | "failed". A bare `if (result)` @@ -369,13 +369,13 @@ export async function pushModels(sourceData: mgmtApi.Model[], targetData: mgmtAp failureDetails.push({ name: model.referenceName, error: `Failed to update model "${model.referenceName}" (target ID: ${sourceMapping.targetID})`, - guid: sourceGuid[0], + guid: sourceGuid, }); } } for (const model of shouldSkip) { - logger.model.skipped(model.model, model.reason, targetGuid[0]); + logger.model.skipped(model.model, model.reason, targetGuid); skipped++; } @@ -384,9 +384,9 @@ export async function pushModels(sourceData: mgmtApi.Model[], targetData: mgmtAp // SUMMARY, rather than attempting a create that is guaranteed to 409. for (const { model, target } of crossKindConflicts) { const message = crossKindCollisionMessage(model, target); - logger.model.error(model, new Error(message), targetGuid[0]); + logger.model.error(model, new Error(message), targetGuid); failed++; - failureDetails.push({ name: model.referenceName, error: message, guid: sourceGuid[0] }); + failureDetails.push({ name: model.referenceName, error: message, guid: sourceGuid }); } return { diff --git a/src/lib/pushers/orchestrate-pushers.ts b/src/lib/pushers/orchestrate-pushers.ts index 6475172b..a6a03ca3 100644 --- a/src/lib/pushers/orchestrate-pushers.ts +++ b/src/lib/pushers/orchestrate-pushers.ts @@ -40,8 +40,8 @@ export class Pushers { this.config = config; // Defer fileOps creation until we have a valid sourceGuid // This allows validation to provide a helpful error message first - if (state.sourceGuid && state.sourceGuid.length > 0 && state.sourceGuid[0]) { - this.fileOps = new fileOperations(state.sourceGuid[0], null); + if (state.sourceGuid) { + this.fileOps = new fileOperations(state.sourceGuid, null); } } @@ -125,17 +125,12 @@ export class Pushers { * Orchestrate push operations (MAIN METHOD) */ async instanceOrchestrator(): Promise { - const { sourceGuid: sourceGuids, targetGuid: targetGuids } = getState(); + const { sourceGuid, targetGuid } = getState(); - if (sourceGuids.length === 0 || targetGuids.length === 0) { - throw new Error("No source or target GUIDs available for push operation"); + if (!sourceGuid || !targetGuid) { + throw new Error("No source or target GUID available for push operation"); } - // For now, handle single source to single target (most common case) - // Future enhancement: handle multiple source/target combinations - const sourceGuid = sourceGuids[0]; - const targetGuid = targetGuids[0]; - console.log("--------------------------------"); // console.log(`Starting push operations from ${sourceGuid} to ${targetGuid}`); // console.log(`Elements: ${elements}`); @@ -350,7 +345,7 @@ export class Pushers { return { success: 0, failures: 0, skipped: 0, failureDetails: [] }; } - this.config.onOperationStart?.(config.name, state.sourceGuid[0], state.targetGuid[0]); + this.config.onOperationStart?.(config.name, state.sourceGuid, state.targetGuid); const pusherResult: PusherResult = await config.handler(sourceData, targetData, locale); @@ -377,8 +372,8 @@ export class Pushers { this.config.onOperationComplete?.( config.name, - state.sourceGuid[0], - state.targetGuid[0], + state.sourceGuid, + state.targetGuid, pusherResult.status === "success" ); diff --git a/src/lib/pushers/page-pusher/push-pages.ts b/src/lib/pushers/page-pusher/push-pages.ts index c5e5510a..6ffd68f0 100644 --- a/src/lib/pushers/page-pusher/push-pages.ts +++ b/src/lib/pushers/page-pusher/push-pages.ts @@ -11,8 +11,8 @@ export async function pushPages(sourceData: mgmtApi.PageItem[], locale: string): let pages: mgmtApi.PageItem[] = sourceData || []; const { sourceGuid, targetGuid } = state; - const logger = getLoggerForGuid(sourceGuid[0]); - const pageMapper = new PageMapper(sourceGuid[0], targetGuid[0], locale); + const logger = getLoggerForGuid(sourceGuid); + const pageMapper = new PageMapper(sourceGuid, targetGuid, locale); if (!pages || pages.length === 0) { console.log("No pages found to process."); @@ -24,7 +24,7 @@ export async function pushPages(sourceData: mgmtApi.PageItem[], locale: string): // Reset processed page IDs tracking for this locale resetProcessedPageIDs(); - const sitemaps = sitemapHierarchy.loadAllSitemaps(sourceGuid[0], locale); + const sitemaps = sitemapHierarchy.loadAllSitemaps(sourceGuid, locale); const channels = Object.keys(sitemaps); console.log(`Processing ${pages.length} pages across ${channels.length} channels in ${locale}...`); @@ -62,8 +62,8 @@ export async function pushPages(sourceData: mgmtApi.PageItem[], locale: string): channel, pageMapper, sitemapNodes: sitemap, - sourceGuid: sourceGuid[0], - targetGuid: targetGuid[0], + sourceGuid: sourceGuid, + targetGuid: targetGuid, locale: locale, apiClient, overwrite, @@ -94,7 +94,7 @@ export async function pushPages(sourceData: mgmtApi.PageItem[], locale: string): `⚠️ Error in page processing for channel: ${channel}: ${errorMessage}`, locale, channel, - targetGuid[0] + targetGuid ); status = "error"; // PROD-2310: a channel-level throw means every page under it failed to sync. @@ -105,7 +105,7 @@ export async function pushPages(sourceData: mgmtApi.PageItem[], locale: string): name: `Channel ${channel}`, error: errorMessage, type: "page", - guid: sourceGuid[0], + guid: sourceGuid, locale, }); } diff --git a/src/lib/pushers/template-pusher.ts b/src/lib/pushers/template-pusher.ts index 88b408e4..0823df21 100644 --- a/src/lib/pushers/template-pusher.ts +++ b/src/lib/pushers/template-pusher.ts @@ -15,7 +15,7 @@ export async function pushTemplates( ): Promise { const { sourceGuid, cachedApiClient: apiClient } = state; - const logger = getLoggerForGuid(sourceGuid[0]); + const logger = getLoggerForGuid(sourceGuid); if (!sourceTemplates || sourceTemplates.length === 0) { console.log("No sourceTemplates found to process."); @@ -33,8 +33,9 @@ export async function pushTemplates( let sourceTemplate = sourceTemplates[i]; const { sourceGuid, targetGuid } = state; - const templateMapper = new TemplateMapper(sourceGuid[0], targetGuid[0]); - const sectionMapper = new SectionMapper(sourceGuid[0], targetGuid[0]); + + const templateMapper = new TemplateMapper(sourceGuid, targetGuid); + const sectionMapper = new SectionMapper(sourceGuid, targetGuid); let existingMapping = templateMapper.getTemplateMapping(sourceTemplate, "source"); let targetTemplate: mgmtApi.PageModel | null = null; @@ -53,7 +54,7 @@ export async function pushTemplates( new Error( `A target template named "${targetTemplate.pageTemplateName}" with ID: ${targetTemplate.pageTemplateID} exists but is not mapped to source ID ${sourceTemplate.pageTemplateID} (likely a rename or reassignment of the source template).` ), - targetGuid[0] + targetGuid ); throw new Error( `Page template validation failed: mapping inconsistency for template "${sourceTemplate.pageTemplateName}" (ID: ${sourceTemplate.pageTemplateID}). ` + @@ -108,7 +109,7 @@ export async function pushTemplates( } } - logger.template.skipped(sourceTemplate, "Up to date, skipping", targetGuid[0]); + logger.template.skipped(sourceTemplate, "Up to date, skipping", targetGuid); preflightReport.record({ phase: "Templates", action: "skip", @@ -164,14 +165,14 @@ export async function pushTemplates( // should have the models by now if (sourceContentSecDef.contentDefinitionID) { - const modelMappers = new ModelMapper(sourceGuid[0], targetGuid[0]); + const modelMappers = new ModelMapper(sourceGuid, targetGuid); const modelMapping = modelMappers.getModelMappingByID(sourceContentSecDef.contentDefinitionID, "source"); if (modelMapping?.targetID) mappedDef.contentDefinitionID = modelMapping.targetID; } // should have the containers by now if (sourceContentSecDef.itemContainerID) { - const containerMappers = new ContainerMapper(sourceGuid[0], targetGuid[0]); + const containerMappers = new ContainerMapper(sourceGuid, targetGuid); const containerMapping = containerMappers.getContainerMappingByContentViewID(sourceContentSecDef.itemContainerID, "source"); if (containerMapping?.targetContentViewID) mappedDef.itemContainerID = containerMapping.targetContentViewID; } @@ -186,7 +187,7 @@ export async function pushTemplates( }; try { - const savedTemplate = await apiClient.pageMethods.savePageTemplate(targetGuid[0], locale, payload); + const savedTemplate = await apiClient.pageMethods.savePageTemplate(targetGuid, locale, payload); templateMapper.addMapping(sourceTemplate, savedTemplate); // Refresh section-level ID mappings from the confirmed response — this both seeds the @@ -202,16 +203,16 @@ export async function pushTemplates( } const action = shouldUpdate ? "updated" : "created"; - logger.template[action](sourceTemplate, action, targetGuid[0]); + logger.template[action](sourceTemplate, action, targetGuid); successful++; } catch (error: any) { - logger.template.error(sourceTemplate, error, targetGuid[0]); + logger.template.error(sourceTemplate, error, targetGuid); failed++; overallStatus = "error"; failureDetails.push({ name: sourceTemplate.pageTemplateName, error: error?.message || String(error), - guid: sourceGuid[0], + guid: sourceGuid, }); } } diff --git a/src/lib/pushers/tests/guid-data-loader.test.ts b/src/lib/pushers/tests/guid-data-loader.test.ts index 053ae2c8..ffbd10f0 100644 --- a/src/lib/pushers/tests/guid-data-loader.test.ts +++ b/src/lib/pushers/tests/guid-data-loader.test.ts @@ -18,8 +18,8 @@ beforeEach(() => { resetState(); setState({ rootPath: tmpDir }); // Model filtering builds a ModelDependencyTreeBuilder → AssetMapper, which needs guids. - state.sourceGuid = ["source-guid-u"]; - state.targetGuid = ["target-guid-u"]; + state.sourceGuid = "source-guid-u"; + state.targetGuid = "target-guid-u"; jest.spyOn(console, "log").mockImplementation(() => {}); jest.spyOn(console, "warn").mockImplementation(() => {}); jest.spyOn(console, "error").mockImplementation(() => {}); diff --git a/src/lib/pushers/tests/model-pusher.test.ts b/src/lib/pushers/tests/model-pusher.test.ts index c4f62261..94a22adb 100644 --- a/src/lib/pushers/tests/model-pusher.test.ts +++ b/src/lib/pushers/tests/model-pusher.test.ts @@ -163,7 +163,7 @@ describe("pushModels — source-side rename orphans a mapping and halts the sync // Seed the mapping exactly as it looked BEFORE the rename: // source model 248 ("ContactUsSendMessageForm") -> target model 118. - const seeder = new ModelMapper(state.sourceGuid[0], state.targetGuid[0]); + const seeder = new ModelMapper(state.sourceGuid, state.targetGuid); seeder.addMapping( { id: 248, @@ -217,7 +217,7 @@ describe("pushModels — deleted-and-recreated model leaves a duplicate mapping // Seed the mapping exactly as it looks after a deleted-and-recreated source model (the PROD-1492 // PromoBanner case): dead source 46 -> target 138 and live source 48 -> target 139, same name. - const seeder = new ModelMapper(state.sourceGuid[0], state.targetGuid[0]); + const seeder = new ModelMapper(state.sourceGuid, state.targetGuid); seeder.addMapping( { id: 46, referenceName: "PromoBanner", lastModifiedDate: new Date(2025, 0, 1).toISOString() } as any, { id: 138, referenceName: "PromoBanner", lastModifiedDate: new Date(2025, 0, 1).toISOString() } as any @@ -258,7 +258,7 @@ describe("pushModels — deleted-and-recreated model leaves a duplicate mapping // ChangeLog source 110 -> target 18 and Changelog source 117 -> target 24. // Neither source model exists in the current pull anymore (the model was fully deleted), so this // is stale mapping residue, not a delete-and-recreate. The gate must NOT halt the sync over it. - const seeder = new ModelMapper(state.sourceGuid[0], state.targetGuid[0]); + const seeder = new ModelMapper(state.sourceGuid, state.targetGuid); seeder.addMapping( { id: 110, referenceName: "ChangeLog", lastModifiedDate: new Date(2025, 0, 1).toISOString() } as any, { id: 18, referenceName: "ChangeLog", lastModifiedDate: new Date(2025, 0, 1).toISOString() } as any @@ -343,7 +343,7 @@ describe("pushModels — false-negative create recovery (PROD-2211)", () => { expect(getContentModules).toHaveBeenCalled(); expect(result.failed).toBe(0); expect(result.successful).toBe(1); - const mapper = new ModelMapper(state.sourceGuid[0], state.targetGuid[0]); + const mapper = new ModelMapper(state.sourceGuid, state.targetGuid); expect(mapper.getModelMappingByID(700, "source")?.targetID).toBe(8); }); @@ -366,7 +366,7 @@ describe("pushModels — false-negative create recovery (PROD-2211)", () => { it("recovers a failed UPDATE when the saved field set matches the source", async () => { // Mapping exists (model already on target); the update throws but the fields were persisted. const { ModelMapper } = await import("lib/mappers/model-mapper"); - const seeder = new ModelMapper(state.sourceGuid[0], state.targetGuid[0]); + const seeder = new ModelMapper(state.sourceGuid, state.targetGuid); seeder.addMapping( { id: 300, referenceName: "Header", lastModifiedDate: new Date(2024, 0, 1).toISOString() } as any, { id: 30, referenceName: "Header", lastModifiedDate: new Date(2024, 0, 1).toISOString() } as any @@ -419,7 +419,7 @@ describe("pushModels — exists in target without mapping, non-default (PROD-221 expect(result.skipped).toBe(1); expect(result.failed).toBe(0); - const mapper = new ModelMapper(state.sourceGuid[0], state.targetGuid[0]); + const mapper = new ModelMapper(state.sourceGuid, state.targetGuid); expect(mapper.getModelMappingByID(501, "source")?.targetID).toBe(10); }); @@ -482,7 +482,7 @@ describe("pushModels — mapped-before-unmapped ordering (PROD-2250)", () => { const fixedDateB = new Date(2024, 0, 1).toISOString(); const fixedDateD = new Date(2024, 5, 1).toISOString(); - const seeder = new ModelMapper(state.sourceGuid[0], state.targetGuid[0]); + const seeder = new ModelMapper(state.sourceGuid, state.targetGuid); const sourceB = makeModel({ id: 102, referenceName: "ModelB-Mapped", lastModifiedDate: fixedDateB }); const targetB = makeModel({ id: 1020, referenceName: "ModelB-Mapped", lastModifiedDate: fixedDateB }); seeder.addMapping(sourceB, targetB); @@ -513,7 +513,7 @@ describe("pushModels — mapped-before-unmapped ordering (PROD-2250)", () => { it("preserves each group's original relative order when multiple models share mapped/unmapped status", async () => { const { ModelMapper } = await import("lib/mappers/model-mapper"); - const seeder = new ModelMapper(state.sourceGuid[0], state.targetGuid[0]); + const seeder = new ModelMapper(state.sourceGuid, state.targetGuid); // Intentionally descending / non-sorted IDs so preserved order can't be mistaken for a sort. const mappedSources = [402, 401, 400].map((id) => makeModel({ id, referenceName: `Mapped-${id}`, lastModifiedDate: new Date(2024, 0, 1).toISOString() }) diff --git a/src/lib/pushers/tests/orchestrate-pushers.test.ts b/src/lib/pushers/tests/orchestrate-pushers.test.ts index 8ef03235..66ad176d 100644 --- a/src/lib/pushers/tests/orchestrate-pushers.test.ts +++ b/src/lib/pushers/tests/orchestrate-pushers.test.ts @@ -114,13 +114,13 @@ describe("Pushers.instanceOrchestrator — guard clause", () => { it("throws when no sourceGuid is set", async () => { const pushers = new Pushers(); // state has no sourceGuid after resetState - await expect(pushers.instanceOrchestrator()).rejects.toThrow(/No source or target GUIDs/); + await expect(pushers.instanceOrchestrator()).rejects.toThrow(/No source or target GUID/); }); it("throws when no targetGuid is set", async () => { setState({ sourceGuid: "src-guid-u" }); const pushers = new Pushers(); - await expect(pushers.instanceOrchestrator()).rejects.toThrow(/No source or target GUIDs/); + await expect(pushers.instanceOrchestrator()).rejects.toThrow(/No source or target GUID/); }); }); diff --git a/src/lib/pushers/url-redirection-pusher.ts b/src/lib/pushers/url-redirection-pusher.ts index 7ee49e92..ef8a7afc 100644 --- a/src/lib/pushers/url-redirection-pusher.ts +++ b/src/lib/pushers/url-redirection-pusher.ts @@ -57,14 +57,14 @@ export async function pushUrlRedirections( const { sourceGuid, targetGuid } = state; - const logger = getLoggerForGuid(sourceGuid[0]) || new Logs("push", "urlRedirection", sourceGuid[0]); + const logger = getLoggerForGuid(sourceGuid) || new Logs("push", "urlRedirection", sourceGuid); if (!redirections || redirections.length === 0) { console.log("No URL redirections found to process."); return { status: "success", successful: 0, failed: 0, skipped: 0 }; } - const mapper = new UrlRedirectionMapper(sourceGuid[0], targetGuid[0]); + const mapper = new UrlRedirectionMapper(sourceGuid, targetGuid); let successful = 0; let failed = 0; @@ -86,7 +86,7 @@ export async function pushUrlRedirections( // same self-heal behavior as galleries/models), then diff to decide update vs skip. mapper.addMapping(source.id, targetByOrigin.id, source.originUrl); if (areEquivalent(source, targetByOrigin)) { - logger.urlRedirection.skipped(source, "already exists in target by origin URL", targetGuid[0]); + logger.urlRedirection.skipped(source, "already exists in target by origin URL", targetGuid); preflightReport.record({ phase: "URL Redirections", action: "skip", @@ -99,7 +99,7 @@ export async function pushUrlRedirections( } } else if (mapping && targetById) { if (areEquivalent(source, targetById)) { - logger.urlRedirection.skipped(source, "up to date, skipping", targetGuid[0]); + logger.urlRedirection.skipped(source, "up to date, skipping", targetGuid); preflightReport.record({ phase: "URL Redirections", action: "skip", @@ -136,7 +136,7 @@ export async function pushUrlRedirections( try { const result = await saveUrlRedirections( - targetGuid[0], + targetGuid, batch.map((p) => p.payload) ); @@ -145,7 +145,7 @@ export async function pushUrlRedirections( const item = batch[created.index]; if (item && created.urlRedirectionID) { mapper.addMapping(item.source.id, created.urlRedirectionID, item.source.originUrl); - logger.urlRedirection.created(item.source, "created", targetGuid[0]); + logger.urlRedirection.created(item.source, "created", targetGuid); } successful++; } @@ -158,7 +158,7 @@ export async function pushUrlRedirections( updated.urlRedirectionID ?? item.payload.urlRedirectionID, item.source.originUrl ); - logger.urlRedirection.updated(item.source, "updated", targetGuid[0]); + logger.urlRedirection.updated(item.source, "updated", targetGuid); } successful++; } @@ -169,7 +169,7 @@ export async function pushUrlRedirections( logger.urlRedirection.skipped( item?.source ?? { originUrl: skippedItem.originUrl }, `skipped by API: ${skippedItem.reason || "no reason given"}`, - targetGuid[0] + targetGuid ); skipped++; } @@ -178,7 +178,7 @@ export async function pushUrlRedirections( failed += batch.length; overallStatus = "error"; for (const item of batch) { - logger.urlRedirection.error(item.source, error?.message || error, targetGuid[0]); + logger.urlRedirection.error(item.source, error?.message || error, targetGuid); } } } diff --git a/src/lib/ui/console/console-manager.ts b/src/lib/ui/console/console-manager.ts index 01080bd7..28e3f203 100644 --- a/src/lib/ui/console/console-manager.ts +++ b/src/lib/ui/console/console-manager.ts @@ -216,7 +216,7 @@ export class ConsoleManager { static createFileOps(guid?: string): fileOperations { const state = getState(); const targetGuid = guid || state.sourceGuid; - return new fileOperations(targetGuid[0], state.locale[0]); + return new fileOperations(targetGuid, state.locale[0]); } /** diff --git a/src/lib/ui/console/file-logger.ts b/src/lib/ui/console/file-logger.ts index b9258ffd..2e5408ce 100644 --- a/src/lib/ui/console/file-logger.ts +++ b/src/lib/ui/console/file-logger.ts @@ -36,7 +36,7 @@ export class FileLogger { return new FileLogger({ rootPath: state.rootPath, - guid: targetGuid[0], + guid: targetGuid, locale: state.locale[0], preview: state.preview, operationType, diff --git a/src/lib/ui/console/logging-modes.ts b/src/lib/ui/console/logging-modes.ts index e0f33a10..7b6214d6 100644 --- a/src/lib/ui/console/logging-modes.ts +++ b/src/lib/ui/console/logging-modes.ts @@ -298,7 +298,7 @@ export class LoggingModes { errors.push("rootPath is required for file logging"); } - if (!state.sourceGuid?.length) { + if (!state.sourceGuid) { errors.push("sourceGuid is required for logging operations"); } diff --git a/src/lib/ui/console/tests/console-setup-utils.test.ts b/src/lib/ui/console/tests/console-setup-utils.test.ts index 8eb1a16c..809e4e49 100644 --- a/src/lib/ui/console/tests/console-setup-utils.test.ts +++ b/src/lib/ui/console/tests/console-setup-utils.test.ts @@ -11,7 +11,7 @@ beforeEach(() => { resetState(); // Provide valid state so validateLoggingState passes by default state.rootPath = "agility-files"; - state.sourceGuid = ["test-guid"]; + state.sourceGuid = "test-guid"; state.locale = ["en-us"]; jest.spyOn(console, "log").mockImplementation(() => {}); jest.spyOn(console, "warn").mockImplementation(() => {}); @@ -56,8 +56,8 @@ describe("validateConsoleSetup", () => { expect(result.errors.some((e) => e.toLowerCase().includes("rootpath"))).toBe(true); }); - it("reports error when sourceGuid is an empty array", () => { - state.sourceGuid = []; + it("reports error when sourceGuid is empty", () => { + state.sourceGuid = ""; const result = validateConsoleSetup({ operationType: "pull" }); expect(result.errors.some((e) => e.toLowerCase().includes("sourceguid"))).toBe(true); }); diff --git a/src/lib/ui/console/tests/file-logger.test.ts b/src/lib/ui/console/tests/file-logger.test.ts index f2dfffb3..962c2d87 100644 --- a/src/lib/ui/console/tests/file-logger.test.ts +++ b/src/lib/ui/console/tests/file-logger.test.ts @@ -247,7 +247,7 @@ describe("FileLogger.finalize", () => { describe("FileLogger.fromState", () => { it("creates a logger using sourceGuid and locale from state", () => { - state.sourceGuid = ["from-state-guid"]; + state.sourceGuid = "from-state-guid"; state.locale = ["fr-ca"]; state.rootPath = "agility-files"; diff --git a/src/lib/ui/console/tests/logging-modes.test.ts b/src/lib/ui/console/tests/logging-modes.test.ts index 85be7d29..a2cdff9f 100644 --- a/src/lib/ui/console/tests/logging-modes.test.ts +++ b/src/lib/ui/console/tests/logging-modes.test.ts @@ -245,7 +245,7 @@ describe("LoggingModes.shouldShowContent", () => { describe("LoggingModes.validateLoggingState", () => { it("is valid with default state (rootPath, sourceGuid, locale populated)", () => { state.rootPath = "agility-files"; - state.sourceGuid = ["test-guid"]; + state.sourceGuid = "test-guid"; state.locale = ["en-us"]; const result = LoggingModes.validateLoggingState(); expect(result.isValid).toBe(true); @@ -254,16 +254,16 @@ describe("LoggingModes.validateLoggingState", () => { it("reports error when rootPath is missing", () => { state.rootPath = ""; - state.sourceGuid = ["test-guid"]; + state.sourceGuid = "test-guid"; state.locale = ["en-us"]; const result = LoggingModes.validateLoggingState(); expect(result.isValid).toBe(false); expect(result.errors.some((e) => e.includes("rootPath"))).toBe(true); }); - it("reports error when sourceGuid is empty array", () => { + it("reports error when sourceGuid is empty", () => { state.rootPath = "agility-files"; - state.sourceGuid = []; + state.sourceGuid = ""; state.locale = ["en-us"]; const result = LoggingModes.validateLoggingState(); expect(result.isValid).toBe(false); @@ -272,7 +272,7 @@ describe("LoggingModes.validateLoggingState", () => { it("reports error when locale is empty array", () => { state.rootPath = "agility-files"; - state.sourceGuid = ["test-guid"]; + state.sourceGuid = "test-guid"; state.locale = []; const result = LoggingModes.validateLoggingState(); expect(result.isValid).toBe(false); @@ -281,7 +281,7 @@ describe("LoggingModes.validateLoggingState", () => { it("warns when both headless and verbose are set", () => { state.rootPath = "agility-files"; - state.sourceGuid = ["test-guid"]; + state.sourceGuid = "test-guid"; state.locale = ["en-us"]; state.useHeadless = true; state.useVerbose = true; diff --git a/src/lib/workflows/process-batches.ts b/src/lib/workflows/process-batches.ts index e28c9000..b0d70da9 100644 --- a/src/lib/workflows/process-batches.ts +++ b/src/lib/workflows/process-batches.ts @@ -187,7 +187,7 @@ export async function processBatches( logLine(ansiColors.cyan(`\n${operationName}ing ${uniqueIds.length} ${label.toLowerCase()} items...`), logLines); // Get item display info and show breakdown (ALL items, no truncation) - const targetGuid = state.targetGuid?.[0]; + const targetGuid = state.targetGuid; if (targetGuid) { const displayMap = type === "content" diff --git a/src/lib/workflows/tests/refresh-mappings.test.ts b/src/lib/workflows/tests/refresh-mappings.test.ts index 291267b6..e9aab802 100644 --- a/src/lib/workflows/tests/refresh-mappings.test.ts +++ b/src/lib/workflows/tests/refresh-mappings.test.ts @@ -67,7 +67,8 @@ function stubPullFailure() { describe("refreshAndUpdateMappings", () => { describe("when no valid API keys exist for the target", () => { it("skips pull and mapping updates when target has no API keys", async () => { - state.apiKeys = []; // no keys + state.targetGuid = "tgt-guid"; + state.targetApiKeys = null; // no keys const pullSpy = stubPullSuccess(); const mappingSpy = stubMappingUpdate(); @@ -79,14 +80,16 @@ describe("refreshAndUpdateMappings", () => { }); it("does not throw when no API keys exist", async () => { - state.apiKeys = []; + state.targetGuid = "tgt-guid"; + state.targetApiKeys = null; await expect(refreshAndUpdateMappings([], [], "src-guid", "tgt-guid", "en-us")).resolves.not.toThrow(); }); }); describe("when valid API keys exist for the target", () => { beforeEach(() => { - state.apiKeys = [{ guid: "tgt-guid", previewKey: "pk", fetchKey: "fk" }]; + state.targetGuid = "tgt-guid"; + state.targetApiKeys = { previewKey: "pk", fetchKey: "fk" }; }); it("calls pull.pullInstances on a successful flow", async () => { diff --git a/src/lib/workflows/tests/workflow-operation.test.ts b/src/lib/workflows/tests/workflow-operation.test.ts index 50894b0e..e2b60108 100644 --- a/src/lib/workflows/tests/workflow-operation.test.ts +++ b/src/lib/workflows/tests/workflow-operation.test.ts @@ -50,7 +50,7 @@ function makeMappingSummary(totalContent = 0, totalPages = 0) { describe("WorkflowOperation.executeFromMappings", () => { describe("guard clauses", () => { it("returns success=false when sourceGuid is missing", async () => { - state.targetGuid = ["tgt-u"]; + state.targetGuid = "tgt-u"; state.locale = ["en-us"]; const op = new WorkflowOperation(); @@ -61,7 +61,7 @@ describe("WorkflowOperation.executeFromMappings", () => { }); it("returns success=false when targetGuid is missing", async () => { - state.sourceGuid = ["src-u"]; + state.sourceGuid = "src-u"; state.locale = ["en-us"]; const op = new WorkflowOperation(); @@ -72,8 +72,8 @@ describe("WorkflowOperation.executeFromMappings", () => { }); it("returns success=false when locale is missing", async () => { - state.sourceGuid = ["src-u"]; - state.targetGuid = ["tgt-u"]; + state.sourceGuid = "src-u"; + state.targetGuid = "tgt-u"; const op = new WorkflowOperation(); const result = await op.executeFromMappings(); @@ -85,8 +85,8 @@ describe("WorkflowOperation.executeFromMappings", () => { describe("standard mode — no mappings found", () => { it("returns early with success=true and zero counts when no mappings exist", async () => { - state.sourceGuid = ["src-u"]; - state.targetGuid = ["tgt-u"]; + state.sourceGuid = "src-u"; + state.targetGuid = "tgt-u"; state.locale = ["en-us"]; jest.spyOn(mappingReader, "getMappingSummary").mockReturnValue(makeMappingSummary(0, 0)); @@ -103,8 +103,8 @@ describe("WorkflowOperation.executeFromMappings", () => { describe("publish operation with source status check", () => { it("filters content to only published-in-source IDs", async () => { - state.sourceGuid = ["src-u"]; - state.targetGuid = ["tgt-u"]; + state.sourceGuid = "src-u"; + state.targetGuid = "tgt-u"; state.locale = ["en-us"]; state.operationType = "publish"; @@ -136,8 +136,8 @@ describe("WorkflowOperation.executeFromMappings", () => { describe("non-publish operation", () => { it("passes all mapped IDs to workflowOrchestrator without source status check", async () => { - state.sourceGuid = ["src-u"]; - state.targetGuid = ["tgt-u"]; + state.sourceGuid = "src-u"; + state.targetGuid = "tgt-u"; state.locale = ["en-us"]; state.operationType = "unpublish"; @@ -164,8 +164,8 @@ describe("WorkflowOperation.executeFromMappings", () => { describe("explicit IDs mode", () => { it("uses explicit contentIDs when provided", async () => { - state.sourceGuid = ["src-u"]; - state.targetGuid = ["tgt-u"]; + state.sourceGuid = "src-u"; + state.targetGuid = "tgt-u"; state.locale = ["en-us"]; state.operationType = "unpublish"; state.explicitContentIDs = [100, 200]; @@ -184,8 +184,8 @@ describe("WorkflowOperation.executeFromMappings", () => { }); it("returns early when all explicit IDs are empty", async () => { - state.sourceGuid = ["src-u"]; - state.targetGuid = ["tgt-u"]; + state.sourceGuid = "src-u"; + state.targetGuid = "tgt-u"; state.locale = ["en-us"]; state.operationType = "unpublish"; state.explicitContentIDs = []; @@ -206,8 +206,8 @@ describe("WorkflowOperation.executeFromMappings", () => { describe("result fields", () => { it("returns operation name in the result", async () => { - state.sourceGuid = ["src-u"]; - state.targetGuid = ["tgt-u"]; + state.sourceGuid = "src-u"; + state.targetGuid = "tgt-u"; state.locale = ["en-us"]; state.operationType = "approve"; @@ -220,8 +220,8 @@ describe("WorkflowOperation.executeFromMappings", () => { }); it("includes elapsedTime in the result", async () => { - state.sourceGuid = ["src-u"]; - state.targetGuid = ["tgt-u"]; + state.sourceGuid = "src-u"; + state.targetGuid = "tgt-u"; state.locale = ["en-us"]; jest.spyOn(mappingReader, "getMappingSummary").mockReturnValue(makeMappingSummary(0, 0)); diff --git a/src/lib/workflows/workflow-operation.ts b/src/lib/workflows/workflow-operation.ts index 000b2fc8..ef33f326 100644 --- a/src/lib/workflows/workflow-operation.ts +++ b/src/lib/workflows/workflow-operation.ts @@ -50,18 +50,18 @@ export class WorkflowOperation { const { sourceGuid, targetGuid, locale: locales } = state; // Validate required parameters - if (!sourceGuid || sourceGuid.length === 0) { + if (!sourceGuid) { throw new Error("Source GUID is required. Use --sourceGuid flag."); } - if (!targetGuid || targetGuid.length === 0) { + if (!targetGuid) { throw new Error("Target GUID is required. Use --targetGuid flag."); } if (!locales || locales.length === 0) { throw new Error("At least one locale is required. Use --locale flag."); } - const source = sourceGuid[0]; - const target = targetGuid[0]; + const source = sourceGuid; + const target = targetGuid; const primaryLocale = locales[0]; console.log(ansiColors.cyan("\n" + "═".repeat(50))); diff --git a/src/tests/workflows/batch-workflows.integration.test.ts b/src/tests/workflows/batch-workflows.integration.test.ts index c4d23696..1353e1e0 100644 --- a/src/tests/workflows/batch-workflows.integration.test.ts +++ b/src/tests/workflows/batch-workflows.integration.test.ts @@ -70,7 +70,7 @@ describe("Batch Workflow Operations - Integration Tests", () => { apiClient = new mgmtApi.ApiClient(options); // Set state for the workflow functions - state.targetGuid = [targetGuid]; + state.targetGuid = targetGuid; state.mgmtApiOptions = options; state.cachedApiClient = apiClient; });