sf-plugin-cms 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +276 -20
- package/lib/commands/cms/export/workspace.d.ts +7 -1
- package/lib/commands/cms/export/workspace.js +113 -2
- package/lib/commands/cms/export/workspace.js.map +1 -1
- package/lib/commands/cms/import/workspace.d.ts +12 -4
- package/lib/commands/cms/import/workspace.js +167 -20
- package/lib/commands/cms/import/workspace.js.map +1 -1
- package/lib/commands/cms/info.js +21 -2
- package/lib/commands/cms/info.js.map +1 -1
- package/lib/contracts/info.d.ts +7 -4
- package/lib/contracts/info.js +17 -8
- package/lib/contracts/info.js.map +1 -1
- package/lib/contracts/shared.d.ts +2 -1
- package/lib/contracts/shared.js +11 -3
- package/lib/contracts/shared.js.map +1 -1
- package/lib/contracts/workspace-export.d.ts +41 -3
- package/lib/contracts/workspace-export.js +171 -5
- package/lib/contracts/workspace-export.js.map +1 -1
- package/lib/contracts/workspace-import.d.ts +65 -0
- package/lib/contracts/workspace-import.js +132 -1
- package/lib/contracts/workspace-import.js.map +1 -1
- package/lib/index.d.ts +3 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/services/cms-prerequisite-lookup.d.ts +16 -0
- package/lib/services/cms-prerequisite-lookup.js +89 -0
- package/lib/services/cms-prerequisite-lookup.js.map +1 -0
- package/lib/services/content-key.d.ts +9 -0
- package/lib/services/content-key.js +15 -0
- package/lib/services/content-key.js.map +1 -0
- package/lib/services/email-fragment.d.ts +15 -0
- package/lib/services/email-fragment.js +203 -0
- package/lib/services/email-fragment.js.map +1 -0
- package/lib/services/export-workspace.d.ts +15 -1
- package/lib/services/export-workspace.js +284 -51
- package/lib/services/export-workspace.js.map +1 -1
- package/lib/services/image-import.d.ts +91 -0
- package/lib/services/image-import.js +575 -0
- package/lib/services/image-import.js.map +1 -0
- package/lib/services/import-identities.js +5 -1
- package/lib/services/import-identities.js.map +1 -1
- package/lib/services/import-workspace.d.ts +63 -3
- package/lib/services/import-workspace.js +277 -26
- package/lib/services/import-workspace.js.map +1 -1
- package/lib/services/landing-page-pair-export.d.ts +31 -0
- package/lib/services/landing-page-pair-export.js +122 -0
- package/lib/services/landing-page-pair-export.js.map +1 -0
- package/lib/services/landing-page-template.d.ts +62 -0
- package/lib/services/landing-page-template.js +372 -0
- package/lib/services/landing-page-template.js.map +1 -0
- package/lib/services/landing-page.d.ts +87 -0
- package/lib/services/landing-page.js +355 -0
- package/lib/services/landing-page.js.map +1 -0
- package/lib/services/variant-identity.d.ts +14 -0
- package/lib/services/variant-identity.js +24 -0
- package/lib/services/variant-identity.js.map +1 -1
- package/lib/services/web-fragment.d.ts +47 -0
- package/lib/services/web-fragment.js +286 -0
- package/lib/services/web-fragment.js.map +1 -0
- package/lib/transport/experimental-media.d.ts +30 -0
- package/lib/transport/experimental-media.js +224 -0
- package/lib/transport/experimental-media.js.map +1 -0
- package/lib/transport/json-request.d.ts +9 -1
- package/lib/transport/json-request.js +30 -3
- package/lib/transport/json-request.js.map +1 -1
- package/lib/transport/multipart-image-create.d.ts +28 -0
- package/lib/transport/multipart-image-create.js +189 -0
- package/lib/transport/multipart-image-create.js.map +1 -0
- package/messages/cms.export.workspace.md +20 -0
- package/messages/cms.import.workspace.md +12 -2
- package/package.json +1 -1
|
@@ -3,11 +3,15 @@ import { lstat, mkdir, open, readFile, readdir, realpath } from 'node:fs/promise
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { cleanupOwnedPath, replaceFileByAtomicRename, } from './atomic-publish.js';
|
|
5
5
|
import { CmsRequestError } from '../transport/json-request.js';
|
|
6
|
-
import { assertWorkspaceExportManifest, } from '../contracts/workspace-export.js';
|
|
6
|
+
import { assertWorkspaceExportManifest, UnsupportedWorkspacePackageVersionError, } from '../contracts/workspace-export.js';
|
|
7
7
|
import { getContent } from './read.js';
|
|
8
8
|
import { variantIdentity } from './variant-identity.js';
|
|
9
9
|
import { inventoryExportReferences } from './export-references.js';
|
|
10
|
-
import { planImportIdentities, planNativeCopies } from './import-identities.js';
|
|
10
|
+
import { planImportIdentities, planNativeCopies, } from './import-identities.js';
|
|
11
|
+
import { planEmailFragmentCopies } from './email-fragment.js';
|
|
12
|
+
import { planWebFragmentCopies, validateDataGraphPrerequisites, } from './web-fragment.js';
|
|
13
|
+
import { planLandingPageTemplateCopies, validateLandingPageTemplatePrerequisites, } from './landing-page-template.js';
|
|
14
|
+
import { planLandingPageCopies, resolveLandingPageTemplateDependencies, validateLandingPagePrerequisites, } from './landing-page.js';
|
|
11
15
|
import { loadEditableRawHtml } from './editable-raw-html-import.js';
|
|
12
16
|
import { isDeepStrictEqual } from 'node:util';
|
|
13
17
|
import { createContent, createVariant, } from './write.js';
|
|
@@ -233,6 +237,9 @@ export async function loadWorkspaceExport(sourceDirectory, options = {}) {
|
|
|
233
237
|
const manifestValue = parseJson(manifestBytes, 'manifest.json');
|
|
234
238
|
assertWorkspaceExportManifest(manifestValue);
|
|
235
239
|
const manifest = manifestValue;
|
|
240
|
+
if (options.profile === 'image' && manifest.schemaVersion !== 2) {
|
|
241
|
+
throw new UnsupportedWorkspacePackageVersionError('Image import profile requires workspace package manifest version 2');
|
|
242
|
+
}
|
|
236
243
|
const packageFiles = await enumeratePackageFiles(canonicalSource, canonicalSource);
|
|
237
244
|
const expectedFiles = [
|
|
238
245
|
'manifest.json',
|
|
@@ -242,6 +249,19 @@ export async function loadWorkspaceExport(sourceDirectory, options = {}) {
|
|
|
242
249
|
throw new TypeError('Workspace package contains missing, substituted, or unlisted regular files');
|
|
243
250
|
}
|
|
244
251
|
const manifestItems = new Map(manifest.items.map((item_) => [item_.path, item_]));
|
|
252
|
+
for (const [index, declared] of manifest.items.entries()) {
|
|
253
|
+
const segments = declared.path.split('/');
|
|
254
|
+
const file = path.join(canonicalSource, ...segments);
|
|
255
|
+
const normalized = path.normalize(file);
|
|
256
|
+
if (!normalized.startsWith(`${canonicalSource}${path.sep}`)) {
|
|
257
|
+
throw new TypeError(`manifest.items[${index}].path escapes the source directory`);
|
|
258
|
+
}
|
|
259
|
+
const bytes = await readStableRegularFile(file, `Manifest item ${declared.path}`);
|
|
260
|
+
const actualSha256 = createHash('sha256').update(bytes).digest('hex');
|
|
261
|
+
if (actualSha256 !== declared.sha256) {
|
|
262
|
+
throw new TypeError(`Manifest item ${declared.path} failed SHA-256 verification`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
245
265
|
const seenFiles = new Set();
|
|
246
266
|
const seenIds = new Set();
|
|
247
267
|
const items = [];
|
|
@@ -414,7 +434,13 @@ function variantPayload(item, contentKey) {
|
|
|
414
434
|
return payload;
|
|
415
435
|
}
|
|
416
436
|
function isProvenNotFound(error) {
|
|
417
|
-
return (error instanceof CmsRequestError &&
|
|
437
|
+
return (error instanceof CmsRequestError &&
|
|
438
|
+
error.operationKey === 'content.get' &&
|
|
439
|
+
(error.status === 404 ||
|
|
440
|
+
((error.status === 400 || error.status === undefined) &&
|
|
441
|
+
error.errorEntryCount === 1 &&
|
|
442
|
+
error.errorCode === 'INVALID_ID_FIELD' &&
|
|
443
|
+
error.responseMessage === 'Provide a valid content key, ID, or FQN.')));
|
|
418
444
|
}
|
|
419
445
|
function assertTransportableBody(value) {
|
|
420
446
|
if (Array.isArray(value)) {
|
|
@@ -432,6 +458,16 @@ function assertTransportableBody(value) {
|
|
|
432
458
|
for (const child of Object.values(value))
|
|
433
459
|
assertTransportableBody(child);
|
|
434
460
|
}
|
|
461
|
+
function selectedLandingPageTemplateReferences(source, selectedItems, resolvableRelationshipIds) {
|
|
462
|
+
const selectedIds = new Set(selectedItems.map(({ id }) => id));
|
|
463
|
+
return source.manifest.externalReferences.filter((reference) => {
|
|
464
|
+
if (reference.kind !== 'cms.relationship' || !selectedIds.has(reference.source.sourceId)) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
return (resolvableRelationshipIds !== undefined &&
|
|
468
|
+
!resolvableRelationshipIds.has(reference.referenceId));
|
|
469
|
+
});
|
|
470
|
+
}
|
|
435
471
|
function selectedNativeReferences(source, selectedItems) {
|
|
436
472
|
const selectedIds = new Set(selectedItems.map(({ id }) => id));
|
|
437
473
|
const inventory = inventoryExportReferences(source.manifest.workspaceId,
|
|
@@ -452,7 +488,7 @@ function selectedNativeReferences(source, selectedItems) {
|
|
|
452
488
|
source.manifest.items.some((item) => item.referenceId === reference.referenceId && item.path !== ownerEntry.file));
|
|
453
489
|
});
|
|
454
490
|
}
|
|
455
|
-
function preflightReferences(plan, references = plan.source.manifest.externalReferences) {
|
|
491
|
+
function preflightReferences(plan, references = plan.source.manifest.externalReferences, allowValidatedStructuredReferences = false) {
|
|
456
492
|
if (references.some((reference) => reference.required && reference.resolution !== 'included') ||
|
|
457
493
|
plan.source.manifest.dependencies.length > 0) {
|
|
458
494
|
throw new TypeError('Unresolved required package reference; import is blocked');
|
|
@@ -469,7 +505,8 @@ function preflightReferences(plan, references = plan.source.manifest.externalRef
|
|
|
469
505
|
item.contentType === 'sfdc_cms__doc') {
|
|
470
506
|
throw new TypeError('Unresolved content reference or media transport; import is blocked');
|
|
471
507
|
}
|
|
472
|
-
|
|
508
|
+
if (!allowValidatedStructuredReferences)
|
|
509
|
+
assertTransportableBody(item.contentBody);
|
|
473
510
|
if (item.externalSource !== undefined)
|
|
474
511
|
assertTransportableBody(item.externalSource);
|
|
475
512
|
const bodyUrl = item.contentBody['sfdc_cms:urlName'];
|
|
@@ -499,10 +536,24 @@ async function preflightConflicts(connection, plan, requestOptions) {
|
|
|
499
536
|
}
|
|
500
537
|
}
|
|
501
538
|
}
|
|
539
|
+
function soqlString(value) {
|
|
540
|
+
return `'${value.replaceAll('\\', '\\\\').replaceAll("'", String.raw `\'`)}'`;
|
|
541
|
+
}
|
|
542
|
+
async function preflightApiNames(connection, plan) {
|
|
543
|
+
if (connection.query === undefined) {
|
|
544
|
+
throw new TypeError('Destination API-name absence cannot be proven; email-fragment apply is blocked');
|
|
545
|
+
}
|
|
546
|
+
for (const apiName of new Set(plan.groups.flatMap(({ primary, variants }) => [primary, ...variants].flatMap((item) => (item.apiName === undefined ? [] : [item.apiName]))))) {
|
|
547
|
+
const result = await connection.query(`SELECT ApiName FROM ManagedContent WHERE ApiName = ${soqlString(apiName)} LIMIT 1`);
|
|
548
|
+
if (result.totalSize !== 0 || result.records.length > 0) {
|
|
549
|
+
throw new Error(`API name ${apiName} already exists`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
502
553
|
function compareMappingIdentity(left, right) {
|
|
503
554
|
return `${left.kind}\u0000${left.referenceId}`.localeCompare(`${right.kind}\u0000${right.referenceId}`);
|
|
504
555
|
}
|
|
505
|
-
function importContractResult(source, destinationOrgId, plan, mappings, references) {
|
|
556
|
+
function importContractResult(source, destinationOrgId, plan, mappings, references, templatePrerequisites) {
|
|
506
557
|
return {
|
|
507
558
|
sourcePackage: {
|
|
508
559
|
manifestSha256: source.manifestSha256,
|
|
@@ -515,11 +566,66 @@ function importContractResult(source, destinationOrgId, plan, mappings, referenc
|
|
|
515
566
|
integrity: source.integrity,
|
|
516
567
|
mappings: mappings.toSorted(compareMappingIdentity),
|
|
517
568
|
references: references.toSorted(compareMappingIdentity),
|
|
569
|
+
...(templatePrerequisites === undefined ? {} : { templatePrerequisites }),
|
|
518
570
|
};
|
|
519
571
|
}
|
|
520
572
|
function jsonBytes(value) {
|
|
521
573
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
522
574
|
}
|
|
575
|
+
function sha256Json(value) {
|
|
576
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
577
|
+
}
|
|
578
|
+
function landingPagePhaseEvidence(proposal, item) {
|
|
579
|
+
const dependencyIndex = proposal.compatibility.findIndex(({ pageVariantId }) => pageVariantId === item.id);
|
|
580
|
+
const dependency = proposal.templateDependencies[dependencyIndex];
|
|
581
|
+
if (dependency?.validationStatus !== 'passed' || dependency.target === undefined) {
|
|
582
|
+
throw new TypeError('Landing-page target template validation evidence is incomplete');
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
checkedAt: new Date().toISOString(),
|
|
586
|
+
status: 'passed',
|
|
587
|
+
template: structuredClone(dependency.target),
|
|
588
|
+
imageResolutionSha256: sha256Json(proposal.cmsPrerequisites),
|
|
589
|
+
dataGraphResolutionSha256: sha256Json(proposal.dataGraphs),
|
|
590
|
+
targetPageIdentitySha256: sha256Json({
|
|
591
|
+
apiName: item.apiName,
|
|
592
|
+
contentKey: item.contentKey,
|
|
593
|
+
contentType: item.contentType,
|
|
594
|
+
title: item.title,
|
|
595
|
+
urlName: item.urlName,
|
|
596
|
+
}),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function landingPageTemplateEvidence(source, proposal) {
|
|
600
|
+
return proposal.items.map((item, index) => {
|
|
601
|
+
const compatibility = proposal.compatibility[index];
|
|
602
|
+
const dependency = proposal.templateDependencies[index];
|
|
603
|
+
const pair = source.manifest.landingPageTemplatePairs?.find(({ page }) => page.variantId === compatibility.pageVariantId);
|
|
604
|
+
if (pair === undefined)
|
|
605
|
+
throw new TypeError('Landing-page template pair evidence is missing');
|
|
606
|
+
return {
|
|
607
|
+
sourcePage: pair.page,
|
|
608
|
+
sourceTemplate: pair.template,
|
|
609
|
+
requestedTargetSelector: {
|
|
610
|
+
...(dependency.targetContentKey === undefined
|
|
611
|
+
? {}
|
|
612
|
+
: { contentKey: dependency.targetContentKey }),
|
|
613
|
+
...(dependency.targetApiName === undefined ? {} : { apiName: dependency.targetApiName }),
|
|
614
|
+
...(dependency.targetTitle === undefined ? {} : { title: dependency.targetTitle }),
|
|
615
|
+
},
|
|
616
|
+
relationship: {
|
|
617
|
+
referenceId: compatibility.relationshipReferenceId,
|
|
618
|
+
descriptorSha256: compatibility.relationshipDescriptorSha256,
|
|
619
|
+
},
|
|
620
|
+
normalization: {
|
|
621
|
+
version: compatibility.normalizationVersion,
|
|
622
|
+
pageBodySha256: compatibility.normalizedPageBodySha256,
|
|
623
|
+
templateBodySha256: compatibility.normalizedTemplateBodySha256,
|
|
624
|
+
},
|
|
625
|
+
preflight: landingPagePhaseEvidence(proposal, item),
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
}
|
|
523
629
|
async function writeExclusive(file, value) {
|
|
524
630
|
const handle = await open(file, 'wx');
|
|
525
631
|
try {
|
|
@@ -675,15 +781,43 @@ export async function executeWorkspaceImport(options) {
|
|
|
675
781
|
}
|
|
676
782
|
validateRelationships(source.items);
|
|
677
783
|
const native = options.nativeCopyMappings !== undefined;
|
|
678
|
-
|
|
784
|
+
const emailFragment = options.emailFragmentMappings !== undefined;
|
|
785
|
+
const webFragment = options.webFragmentMappings !== undefined;
|
|
786
|
+
const landingPageTemplate = options.landingPageTemplateMappings !== undefined;
|
|
787
|
+
const landingPage = options.landingPageMappings !== undefined;
|
|
788
|
+
if (Number(native) +
|
|
789
|
+
Number(emailFragment) +
|
|
790
|
+
Number(webFragment) +
|
|
791
|
+
Number(landingPageTemplate) +
|
|
792
|
+
Number(landingPage) +
|
|
793
|
+
Number(options.identityMappings !== undefined) >
|
|
794
|
+
1)
|
|
679
795
|
throw new TypeError('Choose one identity profile');
|
|
680
796
|
if (options.editableDirectory !== undefined && !native)
|
|
681
797
|
throw new TypeError('editableDirectory requires nativeCopyMappings');
|
|
682
798
|
let proposal;
|
|
683
799
|
let editableSource;
|
|
800
|
+
let landingPageTemplateProposal;
|
|
801
|
+
let landingPageProposal;
|
|
802
|
+
let webFragmentProposal;
|
|
684
803
|
if (native) {
|
|
685
804
|
({ proposal, editableSource } = await planNativeWorkspaceImport(source, options.nativeCopyMappings, options.editableDirectory));
|
|
686
805
|
}
|
|
806
|
+
else if (emailFragment) {
|
|
807
|
+
proposal = planEmailFragmentCopies(source, options.emailFragmentMappings);
|
|
808
|
+
}
|
|
809
|
+
else if (webFragment) {
|
|
810
|
+
webFragmentProposal = planWebFragmentCopies(source, options.webFragmentMappings);
|
|
811
|
+
proposal = webFragmentProposal;
|
|
812
|
+
}
|
|
813
|
+
else if (landingPageTemplate) {
|
|
814
|
+
landingPageTemplateProposal = planLandingPageTemplateCopies(source, options.landingPageTemplateMappings);
|
|
815
|
+
proposal = landingPageTemplateProposal;
|
|
816
|
+
}
|
|
817
|
+
else if (landingPage) {
|
|
818
|
+
landingPageProposal = planLandingPageCopies(source, options.landingPageMappings);
|
|
819
|
+
proposal = landingPageProposal;
|
|
820
|
+
}
|
|
687
821
|
else if (options.identityMappings !== undefined)
|
|
688
822
|
proposal = planImportIdentities(source, options.identityMappings);
|
|
689
823
|
const selectedIds = new Set(proposal?.items.map((item) => item.id));
|
|
@@ -696,7 +830,9 @@ export async function executeWorkspaceImport(options) {
|
|
|
696
830
|
retryable: false,
|
|
697
831
|
},
|
|
698
832
|
];
|
|
699
|
-
const selectedSource = native
|
|
833
|
+
const selectedSource = native || emailFragment || webFragment || landingPageTemplate || landingPage
|
|
834
|
+
? { ...source, items: proposal.items }
|
|
835
|
+
: source;
|
|
700
836
|
const sourcePlan = planWorkspaceImport(selectedSource, options.destinationWorkspace, options.workspaceId);
|
|
701
837
|
// Keep original variant IDs and source manifest associations; never substitute the raw package.
|
|
702
838
|
const proposedById = new Map(proposal?.items.map((item) => [item.id, item]));
|
|
@@ -711,23 +847,56 @@ export async function executeWorkspaceImport(options) {
|
|
|
711
847
|
variants: group.variants.map((variant) => proposedById.get(variant.id)),
|
|
712
848
|
})),
|
|
713
849
|
};
|
|
714
|
-
|
|
850
|
+
let selectedReferences;
|
|
851
|
+
if (native)
|
|
852
|
+
selectedReferences = selectedNativeReferences(source, proposal.items);
|
|
853
|
+
else if (landingPageTemplate) {
|
|
854
|
+
selectedReferences = selectedLandingPageTemplateReferences(source, proposal.items);
|
|
855
|
+
}
|
|
856
|
+
else if (landingPage) {
|
|
857
|
+
selectedReferences = selectedLandingPageTemplateReferences(source, proposal.items, new Set(landingPageProposal.compatibility.map(({ relationshipReferenceId }) => relationshipReferenceId)));
|
|
858
|
+
}
|
|
859
|
+
preflightReferences(plan, selectedReferences, landingPageTemplate || landingPage);
|
|
715
860
|
const requestOptions = options.requestOptions ?? {};
|
|
716
861
|
if (!native)
|
|
717
862
|
await preflightConflicts(options.connection, plan, requestOptions);
|
|
863
|
+
if (emailFragment || webFragment || landingPageTemplate || landingPage)
|
|
864
|
+
await preflightApiNames(options.connection, plan);
|
|
865
|
+
if (webFragment) {
|
|
866
|
+
await validateDataGraphPrerequisites(options.connection, webFragmentProposal.dataGraphs);
|
|
867
|
+
}
|
|
868
|
+
if (landingPageTemplate) {
|
|
869
|
+
await validateLandingPageTemplatePrerequisites(options.connection, plan.destinationWorkspaceId, landingPageTemplateProposal, requestOptions);
|
|
870
|
+
}
|
|
871
|
+
let templatePrerequisites;
|
|
872
|
+
if (landingPage) {
|
|
873
|
+
await resolveLandingPageTemplateDependencies(options.connection, plan.destinationWorkspaceId, landingPageProposal, requestOptions);
|
|
874
|
+
await validateLandingPagePrerequisites(options.connection, plan.destinationWorkspaceId, landingPageProposal, requestOptions);
|
|
875
|
+
templatePrerequisites = landingPageTemplateEvidence(source, landingPageProposal);
|
|
876
|
+
}
|
|
718
877
|
const hasNamedIdentities = plan.groups.some((group) => [group.primary, ...group.variants].some((item) => item.apiName !== undefined ||
|
|
719
878
|
item.urlName !== undefined ||
|
|
720
879
|
item.contentBody['sfdc_cms:urlName'] !== undefined));
|
|
721
|
-
if (!native &&
|
|
880
|
+
if (!native &&
|
|
881
|
+
!emailFragment &&
|
|
882
|
+
!webFragment &&
|
|
883
|
+
!landingPageTemplate &&
|
|
884
|
+
!landingPage &&
|
|
885
|
+
hasNamedIdentities &&
|
|
886
|
+
options.dryRun !== true) {
|
|
722
887
|
throw new TypeError('Destination API-name/URL-name uniqueness cannot be verified by the supported CMS APIs; import is blocked');
|
|
723
888
|
}
|
|
889
|
+
const pairedRelationshipIds = new Set(landingPageProposal?.compatibility.map(({ relationshipReferenceId }) => relationshipReferenceId));
|
|
724
890
|
const references = source.manifest.externalReferences
|
|
725
891
|
.filter(({ resolution }) => resolution !== 'included')
|
|
726
|
-
.map(({ kind, referenceId, resolution }) =>
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
892
|
+
.map(({ kind, referenceId, resolution }) => {
|
|
893
|
+
let status = 'unresolved';
|
|
894
|
+
if (pairedRelationshipIds.has(referenceId))
|
|
895
|
+
status = 'resolved';
|
|
896
|
+
else if (resolution === 'unsupported')
|
|
897
|
+
status = 'unsupported';
|
|
898
|
+
return { referenceId, kind, status };
|
|
899
|
+
})
|
|
731
900
|
.toSorted(compareMappingIdentity);
|
|
732
901
|
if (options.dryRun === true) {
|
|
733
902
|
const diagnostics = [
|
|
@@ -748,15 +917,18 @@ export async function executeWorkspaceImport(options) {
|
|
|
748
917
|
if (hasNamedIdentities && !native) {
|
|
749
918
|
diagnostics.unshift({
|
|
750
919
|
code: 'NAME_AVAILABILITY_UNVERIFIED',
|
|
751
|
-
message:
|
|
920
|
+
message: emailFragment || webFragment || landingPageTemplate || landingPage
|
|
921
|
+
? `Destination API-name absence was proven by exact ManagedContent.ApiName equality lookup;${webFragment || landingPageTemplate || landingPage ? ' every named prerequisite was proven in exact type/workspace scope;' : ''} other server conflict behavior remains unverified.`
|
|
922
|
+
: 'Destination API-name/URL-name availability is unverified; named apply remains blocked until collision evidence is available.',
|
|
752
923
|
retryable: false,
|
|
753
924
|
});
|
|
754
925
|
}
|
|
755
926
|
return {
|
|
756
927
|
diagnostics,
|
|
757
|
-
contractResult: importContractResult(source, options.destinationOrgId, plan, [], references),
|
|
928
|
+
contractResult: importContractResult(source, options.destinationOrgId, plan, [], references, templatePrerequisites),
|
|
758
929
|
dryRun: true,
|
|
759
930
|
plan,
|
|
931
|
+
...(templatePrerequisites === undefined ? {} : { templatePrerequisites }),
|
|
760
932
|
};
|
|
761
933
|
}
|
|
762
934
|
if (!nonemptyString(options.reportDirectory)) {
|
|
@@ -767,10 +939,27 @@ export async function executeWorkspaceImport(options) {
|
|
|
767
939
|
const reportFile = path.join(reportDirectory, 'workspace-import-run.json');
|
|
768
940
|
const report = {
|
|
769
941
|
...(editableSource === undefined ? {} : { editableSource }),
|
|
942
|
+
...(landingPageTemplateProposal === undefined && landingPageProposal === undefined
|
|
943
|
+
? {}
|
|
944
|
+
: {
|
|
945
|
+
cmsPrerequisites: landingPageTemplateProposal?.cmsPrerequisites ?? landingPageProposal.cmsPrerequisites,
|
|
946
|
+
}),
|
|
947
|
+
...(webFragmentProposal === undefined &&
|
|
948
|
+
landingPageTemplateProposal === undefined &&
|
|
949
|
+
landingPageProposal === undefined
|
|
950
|
+
? {}
|
|
951
|
+
: {
|
|
952
|
+
dataGraphPrerequisites: webFragmentProposal?.dataGraphs ??
|
|
953
|
+
landingPageTemplateProposal?.dataGraphs ??
|
|
954
|
+
landingPageProposal.dataGraphs,
|
|
955
|
+
}),
|
|
770
956
|
createdParents: [],
|
|
771
957
|
destinationOrgId: options.destinationOrgId,
|
|
772
958
|
destinationWorkspaceId: plan.destinationWorkspaceId,
|
|
773
959
|
operations: [],
|
|
960
|
+
...(templatePrerequisites === undefined
|
|
961
|
+
? {}
|
|
962
|
+
: { templatePrerequisites: structuredClone(templatePrerequisites) }),
|
|
774
963
|
runId: randomUUID(),
|
|
775
964
|
sourceDirectory: source.sourceDirectory,
|
|
776
965
|
sourceManifestSha256: source.manifestSha256,
|
|
@@ -808,8 +997,14 @@ export async function executeWorkspaceImport(options) {
|
|
|
808
997
|
await writeExclusive(reportFile, report);
|
|
809
998
|
try {
|
|
810
999
|
for (const group of plan.groups) {
|
|
1000
|
+
if (landingPageTemplate) {
|
|
1001
|
+
await validateLandingPageTemplatePrerequisites(options.connection, plan.destinationWorkspaceId, landingPageTemplateProposal, requestOptions);
|
|
1002
|
+
}
|
|
1003
|
+
const finalPrimary = proposal?.items.find((item) => item.id === group.primary.id) ?? group.primary;
|
|
1004
|
+
const finalVariants = group.variants.map((variant) => proposal?.items.find((item) => item.id === variant.id) ?? variant);
|
|
1005
|
+
const finalGroup = { ...group, primary: finalPrimary, variants: finalVariants };
|
|
811
1006
|
const groupReferenceKeys = new Set();
|
|
812
|
-
for (const item_ of [
|
|
1007
|
+
for (const item_ of [finalGroup.primary, ...finalGroup.variants]) {
|
|
813
1008
|
for (const key of cmsContentBodyReferenceKeys(item_.contentBody, sourceReferenceValues)) {
|
|
814
1009
|
groupReferenceKeys.add(key);
|
|
815
1010
|
}
|
|
@@ -826,11 +1021,11 @@ export async function executeWorkspaceImport(options) {
|
|
|
826
1021
|
}
|
|
827
1022
|
}
|
|
828
1023
|
const rewrittenGroup = hasUnknownRequiredMapping
|
|
829
|
-
?
|
|
1024
|
+
? finalGroup
|
|
830
1025
|
: {
|
|
831
|
-
...
|
|
832
|
-
primary: rewriteItem(
|
|
833
|
-
variants:
|
|
1026
|
+
...finalGroup,
|
|
1027
|
+
primary: rewriteItem(finalGroup.primary, replacements),
|
|
1028
|
+
variants: finalGroup.variants.map((variant) => rewriteItem(variant, replacements)),
|
|
834
1029
|
};
|
|
835
1030
|
const parentPayload = createPayload(rewrittenGroup, native ? plan.destinationWorkspaceId : plan.rootFolderId);
|
|
836
1031
|
if (native) {
|
|
@@ -838,18 +1033,60 @@ export async function executeWorkspaceImport(options) {
|
|
|
838
1033
|
delete parentPayload.externalId;
|
|
839
1034
|
delete parentPayload.externalSource;
|
|
840
1035
|
}
|
|
1036
|
+
if (emailFragment || webFragment || landingPageTemplate || landingPage) {
|
|
1037
|
+
const currentPlan = {
|
|
1038
|
+
...plan,
|
|
1039
|
+
groups: [rewrittenGroup],
|
|
1040
|
+
};
|
|
1041
|
+
await preflightConflicts(options.connection, currentPlan, requestOptions);
|
|
1042
|
+
await preflightApiNames(options.connection, currentPlan);
|
|
1043
|
+
if (webFragment) {
|
|
1044
|
+
await validateDataGraphPrerequisites(options.connection, webFragmentProposal.dataGraphs);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
let finalPayload = parentPayload;
|
|
1048
|
+
if (landingPage) {
|
|
1049
|
+
const freshProposal = planLandingPageCopies(source, options.landingPageMappings);
|
|
1050
|
+
await resolveLandingPageTemplateDependencies(options.connection, plan.destinationWorkspaceId, freshProposal, requestOptions);
|
|
1051
|
+
await validateLandingPagePrerequisites(options.connection, plan.destinationWorkspaceId, freshProposal, requestOptions);
|
|
1052
|
+
const freshItem = freshProposal.items.find(({ id }) => id === group.primary.id);
|
|
1053
|
+
if (freshItem === undefined)
|
|
1054
|
+
throw new TypeError('Landing-page pre-create item is missing');
|
|
1055
|
+
const evidenceIndex = report.templatePrerequisites?.findIndex(({ sourcePage }) => sourcePage.variantId === group.primary.id);
|
|
1056
|
+
if (evidenceIndex === undefined || evidenceIndex < 0) {
|
|
1057
|
+
throw new TypeError('Landing-page durable template evidence is missing');
|
|
1058
|
+
}
|
|
1059
|
+
const preflight = report.templatePrerequisites[evidenceIndex].preflight;
|
|
1060
|
+
const preCreate = landingPagePhaseEvidence(freshProposal, freshItem);
|
|
1061
|
+
if (!isDeepStrictEqual(preflight.template, preCreate.template) ||
|
|
1062
|
+
preflight.imageResolutionSha256 !== preCreate.imageResolutionSha256 ||
|
|
1063
|
+
preflight.dataGraphResolutionSha256 !== preCreate.dataGraphResolutionSha256 ||
|
|
1064
|
+
preflight.targetPageIdentitySha256 !== preCreate.targetPageIdentitySha256) {
|
|
1065
|
+
throw new TypeError('Landing-page template, image, Data Graph, or target page identity drifted before CREATE');
|
|
1066
|
+
}
|
|
1067
|
+
report.templatePrerequisites[evidenceIndex].preCreate = preCreate;
|
|
1068
|
+
finalPayload = createPayload({ ...group, primary: freshItem, variants: [] }, plan.rootFolderId);
|
|
1069
|
+
}
|
|
841
1070
|
const parentOperation = editableSource === undefined
|
|
842
|
-
? pendingOperation(report, 'create-parent', group.contentKey, group.primary.language,
|
|
1071
|
+
? pendingOperation(report, 'create-parent', group.contentKey, group.primary.language, finalPayload)
|
|
843
1072
|
: report.operations.find((operation) => operation.contentKey === group.contentKey);
|
|
844
1073
|
if (editableSource === undefined)
|
|
845
1074
|
report.operations.push(parentOperation);
|
|
1075
|
+
if (landingPage && report.templatePrerequisites !== undefined) {
|
|
1076
|
+
const evidence = report.templatePrerequisites.find(({ sourcePage }) => sourcePage.variantId === group.primary.id);
|
|
1077
|
+
if (evidence === undefined)
|
|
1078
|
+
throw new TypeError('Landing-page journal evidence is missing');
|
|
1079
|
+
evidence.finalRequestSha256 = createHash('sha256')
|
|
1080
|
+
.update(JSON.stringify(finalPayload))
|
|
1081
|
+
.digest('hex');
|
|
1082
|
+
}
|
|
846
1083
|
else if (parentOperation.requestSha256 !==
|
|
847
|
-
operationIdentity(report.runId, 'create-parent', group.contentKey, group.primary.language,
|
|
1084
|
+
operationIdentity(report.runId, 'create-parent', group.contentKey, group.primary.language, finalPayload).requestSha256)
|
|
848
1085
|
throw new TypeError('Editable CREATE payload changed after initial journal');
|
|
849
1086
|
await rewrite(reportFile, report);
|
|
850
1087
|
let parentResponse;
|
|
851
1088
|
try {
|
|
852
|
-
parentResponse = await createContent(options.connection,
|
|
1089
|
+
parentResponse = await createContent(options.connection, finalPayload, requestOptions);
|
|
853
1090
|
}
|
|
854
1091
|
catch (error) {
|
|
855
1092
|
parentOperation.state = native ? 'pending' : 'failed';
|
|
@@ -887,6 +1124,17 @@ export async function executeWorkspaceImport(options) {
|
|
|
887
1124
|
contentKey: created.contentKey,
|
|
888
1125
|
primaryVariantId: created.primaryVariantId,
|
|
889
1126
|
};
|
|
1127
|
+
if (landingPage && report.templatePrerequisites !== undefined) {
|
|
1128
|
+
const evidence = report.templatePrerequisites.find(({ sourcePage }) => sourcePage.variantId === group.primary.id);
|
|
1129
|
+
if (evidence === undefined)
|
|
1130
|
+
throw new TypeError('Landing-page result evidence is missing');
|
|
1131
|
+
evidence.returned = {
|
|
1132
|
+
contentId: created.contentId,
|
|
1133
|
+
contentKey: created.contentKey,
|
|
1134
|
+
primaryVariantId: created.primaryVariantId,
|
|
1135
|
+
state: 'unverified',
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
890
1138
|
await persistOperationResult(reportFile, report, parentOperation, rewrite);
|
|
891
1139
|
if (native) {
|
|
892
1140
|
if (source.items.some((item) => item.contentKey === created.contentKey ||
|
|
@@ -984,9 +1232,12 @@ export async function executeWorkspaceImport(options) {
|
|
|
984
1232
|
}
|
|
985
1233
|
return {
|
|
986
1234
|
diagnostics: editableDiagnostics,
|
|
987
|
-
contractResult: importContractResult(source, options.destinationOrgId, plan, mappings, references),
|
|
1235
|
+
contractResult: importContractResult(source, options.destinationOrgId, plan, mappings, references, report.templatePrerequisites),
|
|
988
1236
|
dryRun: false,
|
|
989
1237
|
plan,
|
|
1238
|
+
...(report.templatePrerequisites === undefined
|
|
1239
|
+
? {}
|
|
1240
|
+
: { templatePrerequisites: report.templatePrerequisites }),
|
|
990
1241
|
report,
|
|
991
1242
|
reportFile,
|
|
992
1243
|
};
|