sf-plugin-cms 0.3.1 → 0.5.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 +334 -28
- package/lib/commands/cms/export/workspace.d.ts +6 -1
- package/lib/commands/cms/export/workspace.js +80 -3
- package/lib/commands/cms/export/workspace.js.map +1 -1
- package/lib/commands/cms/import/workspace.d.ts +14 -4
- package/lib/commands/cms/import/workspace.js +199 -24
- 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 +23 -3
- package/lib/contracts/workspace-export.js +118 -5
- package/lib/contracts/workspace-export.js.map +1 -1
- package/lib/contracts/workspace-import.d.ts +64 -0
- package/lib/contracts/workspace-import.js +119 -0
- 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/bulk-export-workspaces.js +10 -4
- package/lib/services/bulk-export-workspaces.js.map +1 -1
- package/lib/services/editable-raw-html-export.d.ts +26 -0
- package/lib/services/editable-raw-html-export.js +100 -0
- package/lib/services/editable-raw-html-export.js.map +1 -0
- package/lib/services/editable-raw-html-import.d.ts +25 -0
- package/lib/services/editable-raw-html-import.js +107 -0
- package/lib/services/editable-raw-html-import.js.map +1 -0
- package/lib/services/editable-raw-html.d.ts +34 -0
- package/lib/services/editable-raw-html.js +83 -0
- package/lib/services/editable-raw-html.js.map +1 -0
- package/lib/services/email-fragment.d.ts +15 -0
- package/lib/services/email-fragment.js +199 -0
- package/lib/services/email-fragment.js.map +1 -0
- package/lib/services/export-references.js +3 -1
- package/lib/services/export-references.js.map +1 -1
- package/lib/services/export-workspace.d.ts +16 -1
- package/lib/services/export-workspace.js +222 -15
- package/lib/services/export-workspace.js.map +1 -1
- package/lib/services/image-import.d.ts +91 -0
- package/lib/services/image-import.js +579 -0
- package/lib/services/image-import.js.map +1 -0
- package/lib/services/import-identities.d.ts +33 -0
- package/lib/services/import-identities.js +287 -0
- package/lib/services/import-identities.js.map +1 -0
- package/lib/services/import-workspace.d.ts +50 -1
- package/lib/services/import-workspace.js +445 -21
- package/lib/services/import-workspace.js.map +1 -1
- package/lib/services/landing-page-template.d.ts +52 -0
- package/lib/services/landing-page-template.js +421 -0
- package/lib/services/landing-page-template.js.map +1 -0
- package/lib/services/landing-page.d.ts +43 -0
- package/lib/services/landing-page.js +117 -0
- package/lib/services/landing-page.js.map +1 -0
- package/lib/services/resolve-workspace.d.ts +6 -1
- package/lib/services/resolve-workspace.js +51 -4
- package/lib/services/resolve-workspace.js.map +1 -1
- package/lib/services/variant-identity.d.ts +13 -0
- package/lib/services/variant-identity.js +34 -0
- package/lib/services/variant-identity.js.map +1 -0
- package/lib/services/web-fragment.d.ts +41 -0
- package/lib/services/web-fragment.js +245 -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 +12 -2
- package/messages/cms.import.workspace.md +29 -3
- package/package.json +1 -1
|
@@ -3,8 +3,17 @@ 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
|
+
import { variantIdentity } from './variant-identity.js';
|
|
9
|
+
import { inventoryExportReferences } from './export-references.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, validateLandingPagePrerequisites } from './landing-page.js';
|
|
15
|
+
import { loadEditableRawHtml } from './editable-raw-html-import.js';
|
|
16
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
8
17
|
import { createContent, createVariant, } from './write.js';
|
|
9
18
|
export class WorkspaceImportOwnershipUncertainError extends Error {
|
|
10
19
|
operation;
|
|
@@ -93,7 +102,13 @@ async function assertRegularFile(file, label) {
|
|
|
93
102
|
throw new TypeError(`${label} must be a regular file and not a symlink or reparse point`);
|
|
94
103
|
}
|
|
95
104
|
}
|
|
96
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Read a regular file while detecting observed replacement or modification.
|
|
107
|
+
* @param {string} file - Absolute file path.
|
|
108
|
+
* @param {string} label - Error context.
|
|
109
|
+
* @returns {Promise<Buffer>} Original file bytes.
|
|
110
|
+
*/
|
|
111
|
+
export async function readStableRegularFile(file, label) {
|
|
97
112
|
await assertRegularFile(file, label);
|
|
98
113
|
const before = await lstat(file);
|
|
99
114
|
const bytes = await readFile(file);
|
|
@@ -103,7 +118,13 @@ async function readStableRegularFile(file, label) {
|
|
|
103
118
|
}
|
|
104
119
|
return bytes;
|
|
105
120
|
}
|
|
106
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Enumerate regular package files without following symlinks.
|
|
123
|
+
* @param {string} directory - Directory to inspect.
|
|
124
|
+
* @param {string} packageRoot - Root for relative paths.
|
|
125
|
+
* @returns {Promise<string[]>} Sorted portable relative filenames.
|
|
126
|
+
*/
|
|
127
|
+
export async function enumeratePackageFiles(directory, packageRoot) {
|
|
107
128
|
const files = [];
|
|
108
129
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
109
130
|
const absolute = path.join(directory, entry.name);
|
|
@@ -123,12 +144,24 @@ async function enumeratePackageFiles(directory, packageRoot) {
|
|
|
123
144
|
}
|
|
124
145
|
return files.toSorted();
|
|
125
146
|
}
|
|
126
|
-
function validateItem(
|
|
127
|
-
if (!isRecord(
|
|
147
|
+
function validateItem(raw, variantId, workspaceId) {
|
|
148
|
+
if (!isRecord(raw))
|
|
128
149
|
throw new TypeError(`Item ${variantId} must contain an object`);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
150
|
+
const identity = variantIdentity(raw, variantId);
|
|
151
|
+
assertIdentifier(identity.variantId, `Item ${variantId}.id`);
|
|
152
|
+
// Normalize only the in-memory import view after raw-byte integrity verification.
|
|
153
|
+
const normalized = { ...raw, id: identity.variantId };
|
|
154
|
+
if (isRecord(normalized.contentType)) {
|
|
155
|
+
if (Object.keys(normalized.contentType).some((key) => key !== 'fullyQualifiedName' && key !== 'name')) {
|
|
156
|
+
throw new TypeError(`Item ${variantId}.contentType contains unsupported summary fields`);
|
|
157
|
+
}
|
|
158
|
+
normalized.contentType = normalized.contentType.fullyQualifiedName;
|
|
159
|
+
}
|
|
160
|
+
if (normalized.externalId === null)
|
|
161
|
+
delete normalized.externalId;
|
|
162
|
+
if (normalized.externalSource === null)
|
|
163
|
+
delete normalized.externalSource;
|
|
164
|
+
const value = normalized;
|
|
132
165
|
for (const key of ['contentKey', 'language', 'contentType', 'title']) {
|
|
133
166
|
if (!nonemptyString(value[key]))
|
|
134
167
|
throw new TypeError(`Item ${variantId}.${key} is required`);
|
|
@@ -158,7 +191,20 @@ function validateRelationships(items) {
|
|
|
158
191
|
const pairIds = new Map();
|
|
159
192
|
const keyToApiName = new Map();
|
|
160
193
|
const apiNameToKey = new Map();
|
|
194
|
+
const contentIdToKey = new Map();
|
|
195
|
+
const keyToContentId = new Map();
|
|
161
196
|
for (const item of items) {
|
|
197
|
+
const { contentId } = variantIdentity(item, item.id);
|
|
198
|
+
if (contentId !== undefined) {
|
|
199
|
+
const priorKey = contentIdToKey.get(contentId);
|
|
200
|
+
const priorId = keyToContentId.get(item.contentKey);
|
|
201
|
+
if ((priorKey !== undefined && priorKey !== item.contentKey) ||
|
|
202
|
+
(priorId !== undefined && priorId !== contentId)) {
|
|
203
|
+
throw new TypeError('Content keys have conflicting parent content identities');
|
|
204
|
+
}
|
|
205
|
+
contentIdToKey.set(contentId, item.contentKey);
|
|
206
|
+
keyToContentId.set(item.contentKey, contentId);
|
|
207
|
+
}
|
|
162
208
|
const pair = `${item.contentKey}\u0000${item.language}`;
|
|
163
209
|
if (pairIds.has(pair)) {
|
|
164
210
|
throw new TypeError(`Duplicate contentKey/language mapping: ${item.contentKey}/${item.language}`);
|
|
@@ -191,6 +237,9 @@ export async function loadWorkspaceExport(sourceDirectory, options = {}) {
|
|
|
191
237
|
const manifestValue = parseJson(manifestBytes, 'manifest.json');
|
|
192
238
|
assertWorkspaceExportManifest(manifestValue);
|
|
193
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
|
+
}
|
|
194
243
|
const packageFiles = await enumeratePackageFiles(canonicalSource, canonicalSource);
|
|
195
244
|
const expectedFiles = [
|
|
196
245
|
'manifest.json',
|
|
@@ -200,6 +249,19 @@ export async function loadWorkspaceExport(sourceDirectory, options = {}) {
|
|
|
200
249
|
throw new TypeError('Workspace package contains missing, substituted, or unlisted regular files');
|
|
201
250
|
}
|
|
202
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
|
+
}
|
|
203
265
|
const seenFiles = new Set();
|
|
204
266
|
const seenIds = new Set();
|
|
205
267
|
const items = [];
|
|
@@ -374,6 +436,81 @@ function variantPayload(item, contentKey) {
|
|
|
374
436
|
function isProvenNotFound(error) {
|
|
375
437
|
return (error instanceof CmsRequestError && error.operationKey === 'content.get' && error.status === 404);
|
|
376
438
|
}
|
|
439
|
+
function assertTransportableBody(value) {
|
|
440
|
+
if (Array.isArray(value)) {
|
|
441
|
+
for (const child of value)
|
|
442
|
+
assertTransportableBody(child);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (!isRecord(value))
|
|
446
|
+
return;
|
|
447
|
+
if ('ref' in value ||
|
|
448
|
+
value.type === 'file' ||
|
|
449
|
+
(typeof value.type === 'string' && value.type.endsWith('Reference'))) {
|
|
450
|
+
throw new TypeError('Unresolved content reference or media transport; import is blocked');
|
|
451
|
+
}
|
|
452
|
+
for (const child of Object.values(value))
|
|
453
|
+
assertTransportableBody(child);
|
|
454
|
+
}
|
|
455
|
+
function selectedLandingPageTemplateReferences(source, selectedItems) {
|
|
456
|
+
const selectedIds = new Set(selectedItems.map(({ id }) => id));
|
|
457
|
+
return source.manifest.externalReferences.filter((reference) => reference.kind !== 'cms.relationship' || !selectedIds.has(reference.source.sourceId));
|
|
458
|
+
}
|
|
459
|
+
function selectedNativeReferences(source, selectedItems) {
|
|
460
|
+
const selectedIds = new Set(selectedItems.map(({ id }) => id));
|
|
461
|
+
const inventory = inventoryExportReferences(source.manifest.workspaceId,
|
|
462
|
+
// The inventory only reads JSON; its transport type uses mutable array annotations.
|
|
463
|
+
new Map(source.items.map((item) => [item.id, item])));
|
|
464
|
+
const evidenced = new Map(inventory.externalReferences
|
|
465
|
+
.filter(({ kind }) => kind === 'cms.relationship')
|
|
466
|
+
.map((reference) => [reference.referenceId, reference]));
|
|
467
|
+
return source.manifest.externalReferences.filter((reference) => {
|
|
468
|
+
// Only the exporter-proven variant-owned relationship shape can be excluded.
|
|
469
|
+
// Unknown kinds, mismatched identities and contradictory associations fail closed.
|
|
470
|
+
const ownerId = reference.source.sourceId;
|
|
471
|
+
if (selectedIds.has(ownerId) ||
|
|
472
|
+
!isDeepStrictEqual(reference, evidenced.get(reference.referenceId)))
|
|
473
|
+
return true;
|
|
474
|
+
const ownerEntry = source.manifest.entries.find(({ variantId }) => variantId === ownerId);
|
|
475
|
+
return (ownerEntry === undefined ||
|
|
476
|
+
source.manifest.items.some((item) => item.referenceId === reference.referenceId && item.path !== ownerEntry.file));
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function preflightReferences(plan, references = plan.source.manifest.externalReferences, allowValidatedStructuredReferences = false) {
|
|
480
|
+
if (references.some((reference) => reference.required && reference.resolution !== 'included') ||
|
|
481
|
+
plan.source.manifest.dependencies.length > 0) {
|
|
482
|
+
throw new TypeError('Unresolved required package reference; import is blocked');
|
|
483
|
+
}
|
|
484
|
+
const urls = new Set();
|
|
485
|
+
for (const group of plan.groups) {
|
|
486
|
+
if (new Set([group.primary, ...group.variants].map((item) => item.contentType)).size !== 1) {
|
|
487
|
+
throw new TypeError('Parent variants have conflicting content types');
|
|
488
|
+
}
|
|
489
|
+
for (const item of [group.primary, ...group.variants]) {
|
|
490
|
+
if ('references' in item ||
|
|
491
|
+
'referencesList' in item ||
|
|
492
|
+
item.contentType === 'sfdc_cms__image' ||
|
|
493
|
+
item.contentType === 'sfdc_cms__doc') {
|
|
494
|
+
throw new TypeError('Unresolved content reference or media transport; import is blocked');
|
|
495
|
+
}
|
|
496
|
+
if (!allowValidatedStructuredReferences)
|
|
497
|
+
assertTransportableBody(item.contentBody);
|
|
498
|
+
if (item.externalSource !== undefined)
|
|
499
|
+
assertTransportableBody(item.externalSource);
|
|
500
|
+
const bodyUrl = item.contentBody['sfdc_cms:urlName'];
|
|
501
|
+
if (bodyUrl !== undefined &&
|
|
502
|
+
(typeof bodyUrl !== 'string' || (item.urlName !== undefined && item.urlName !== bodyUrl))) {
|
|
503
|
+
throw new TypeError('Conflicting body and variant URL names');
|
|
504
|
+
}
|
|
505
|
+
const url = item.urlName ?? bodyUrl;
|
|
506
|
+
if (typeof url === 'string') {
|
|
507
|
+
if (urls.has(url))
|
|
508
|
+
throw new TypeError('Target URL names must be unique across variants');
|
|
509
|
+
urls.add(url);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
377
514
|
async function preflightConflicts(connection, plan, requestOptions) {
|
|
378
515
|
for (const group of plan.groups) {
|
|
379
516
|
try {
|
|
@@ -387,6 +524,20 @@ async function preflightConflicts(connection, plan, requestOptions) {
|
|
|
387
524
|
}
|
|
388
525
|
}
|
|
389
526
|
}
|
|
527
|
+
function soqlString(value) {
|
|
528
|
+
return `'${value.replaceAll('\\', '\\\\').replaceAll("'", String.raw `\'`)}'`;
|
|
529
|
+
}
|
|
530
|
+
async function preflightApiNames(connection, plan) {
|
|
531
|
+
if (connection.query === undefined) {
|
|
532
|
+
throw new TypeError('Destination API-name absence cannot be proven; email-fragment apply is blocked');
|
|
533
|
+
}
|
|
534
|
+
for (const apiName of new Set(plan.groups.flatMap(({ primary, variants }) => [primary, ...variants].flatMap((item) => (item.apiName === undefined ? [] : [item.apiName]))))) {
|
|
535
|
+
const result = await connection.query(`SELECT ApiName FROM ManagedContent WHERE ApiName = ${soqlString(apiName)} LIMIT 1`);
|
|
536
|
+
if (result.totalSize !== 0 || result.records.length > 0) {
|
|
537
|
+
throw new Error(`API name ${apiName} already exists`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
390
541
|
function compareMappingIdentity(left, right) {
|
|
391
542
|
return `${left.kind}\u0000${left.referenceId}`.localeCompare(`${right.kind}\u0000${right.referenceId}`);
|
|
392
543
|
}
|
|
@@ -446,6 +597,39 @@ function validateParentResponse(response, expectedContentKey) {
|
|
|
446
597
|
primaryVariantId: responseIdentifier(response, ['primaryVariantId', 'managedContentVariantId', 'variantId'], 'primary variant ID'),
|
|
447
598
|
};
|
|
448
599
|
}
|
|
600
|
+
function verifyNativeCopy(response, payload, created, language) {
|
|
601
|
+
const type = isRecord(response.contentType)
|
|
602
|
+
? response.contentType.fullyQualifiedName
|
|
603
|
+
: response.contentType;
|
|
604
|
+
if (response.contentKey !== created.contentKey ||
|
|
605
|
+
responseIdentifier(response, ['managedContentId', 'id'], 'content ID') !== created.contentId ||
|
|
606
|
+
response.apiName !== payload.apiName ||
|
|
607
|
+
response.urlName !== payload.urlName ||
|
|
608
|
+
response.language !== language ||
|
|
609
|
+
response.title !== payload.title ||
|
|
610
|
+
type !== payload.contentType ||
|
|
611
|
+
!isRecord(response.contentSpace) ||
|
|
612
|
+
response.contentSpace.id !== payload.contentSpaceOrFolderId ||
|
|
613
|
+
response.isPublished !== false ||
|
|
614
|
+
!isRecord(response.status) ||
|
|
615
|
+
response.status.status !== 'Draft' ||
|
|
616
|
+
!isRecord(response.contentBody))
|
|
617
|
+
throw new TypeError('Native copy readback identity or draft verification failed');
|
|
618
|
+
for (const [key, value] of Object.entries(payload.contentBody)) {
|
|
619
|
+
// Salesforce entity-encodes raw HTML on retrieval; compare one encoding layer only.
|
|
620
|
+
const encoded = key === 'rawHtml' && typeof value === 'string'
|
|
621
|
+
? value
|
|
622
|
+
.replaceAll('&', '&')
|
|
623
|
+
.replaceAll('<', '<')
|
|
624
|
+
.replaceAll('>', '>')
|
|
625
|
+
.replaceAll('"', '"')
|
|
626
|
+
.replaceAll("'", ''')
|
|
627
|
+
: value;
|
|
628
|
+
if (!isDeepStrictEqual(response.contentBody[key], value) &&
|
|
629
|
+
!isDeepStrictEqual(response.contentBody[key], encoded))
|
|
630
|
+
throw new TypeError(`Native copy readback body differs: ${key}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
449
633
|
function validateChildResponse(response) {
|
|
450
634
|
return responseIdentifier(response, ['id', 'variantId', 'managedContentVariantId'], 'variant ID');
|
|
451
635
|
}
|
|
@@ -488,6 +672,33 @@ async function persistOperationResult(reportFile, report, operation, rewrite) {
|
|
|
488
672
|
throw new WorkspaceImportOwnershipUncertainError(operation, error);
|
|
489
673
|
}
|
|
490
674
|
}
|
|
675
|
+
/**
|
|
676
|
+
* Reconstruct editable HTML before native validation, retaining all original identity inventory.
|
|
677
|
+
* @param {LoadedWorkspaceExport} source - Complete verified original package.
|
|
678
|
+
* @param {unknown} mappings - Native copy selections and fresh identities.
|
|
679
|
+
* @param {string} editableDirectory - Optional verified HTML companion.
|
|
680
|
+
* @returns {Promise<object>} Native proposal and optional all-companion evidence.
|
|
681
|
+
*/
|
|
682
|
+
export async function planNativeWorkspaceImport(source, mappings, editableDirectory) {
|
|
683
|
+
if (editableDirectory === undefined)
|
|
684
|
+
return { proposal: planNativeCopies(source, mappings) };
|
|
685
|
+
const editable = await loadEditableRawHtml(source, editableDirectory);
|
|
686
|
+
const editedById = new Map(editable.items.map((item) => [item.id, item]));
|
|
687
|
+
const proposal = planNativeCopies({ ...source, items: source.items.map((item) => editedById.get(item.id) ?? item) }, mappings, editable.literalHtmlVariantIds);
|
|
688
|
+
for (const item of proposal.items) {
|
|
689
|
+
if (!editedById.has(item.id))
|
|
690
|
+
throw new TypeError(`Selected variant ${item.id} is missing from the editable companion`);
|
|
691
|
+
}
|
|
692
|
+
preflightReferences({
|
|
693
|
+
source,
|
|
694
|
+
groups: proposal.items.map((primary) => ({
|
|
695
|
+
contentKey: primary.contentKey,
|
|
696
|
+
primary,
|
|
697
|
+
variants: [],
|
|
698
|
+
})),
|
|
699
|
+
}, selectedNativeReferences(source, proposal.items));
|
|
700
|
+
return { proposal, editableSource: editable.editableSource };
|
|
701
|
+
}
|
|
491
702
|
export async function executeWorkspaceImport(options) {
|
|
492
703
|
if (!nonemptyString(options.destinationOrgId)) {
|
|
493
704
|
throw new TypeError('destinationOrgId must be the nonempty destination org ID');
|
|
@@ -496,9 +707,112 @@ export async function executeWorkspaceImport(options) {
|
|
|
496
707
|
(await loadWorkspaceExport(options.sourceDirectory, {
|
|
497
708
|
allowPartial: options.allowPartial,
|
|
498
709
|
}));
|
|
499
|
-
|
|
710
|
+
if (!source.integrity.verified)
|
|
711
|
+
throw new TypeError('Source integrity must be verified first');
|
|
712
|
+
if (source.isPartial && options.allowPartial !== true) {
|
|
713
|
+
throw new TypeError('Workspace export is partial; explicit allowPartial is required');
|
|
714
|
+
}
|
|
715
|
+
validateRelationships(source.items);
|
|
716
|
+
const native = options.nativeCopyMappings !== undefined;
|
|
717
|
+
const emailFragment = options.emailFragmentMappings !== undefined;
|
|
718
|
+
const webFragment = options.webFragmentMappings !== undefined;
|
|
719
|
+
const landingPageTemplate = options.landingPageTemplateMappings !== undefined;
|
|
720
|
+
const landingPage = options.landingPageMappings !== undefined;
|
|
721
|
+
if (Number(native) +
|
|
722
|
+
Number(emailFragment) +
|
|
723
|
+
Number(webFragment) +
|
|
724
|
+
Number(landingPageTemplate) +
|
|
725
|
+
Number(landingPage) +
|
|
726
|
+
Number(options.identityMappings !== undefined) >
|
|
727
|
+
1)
|
|
728
|
+
throw new TypeError('Choose one identity profile');
|
|
729
|
+
if (options.editableDirectory !== undefined && !native)
|
|
730
|
+
throw new TypeError('editableDirectory requires nativeCopyMappings');
|
|
731
|
+
let proposal;
|
|
732
|
+
let editableSource;
|
|
733
|
+
let landingPageTemplateProposal;
|
|
734
|
+
let landingPageProposal;
|
|
735
|
+
let webFragmentProposal;
|
|
736
|
+
if (native) {
|
|
737
|
+
({ proposal, editableSource } = await planNativeWorkspaceImport(source, options.nativeCopyMappings, options.editableDirectory));
|
|
738
|
+
}
|
|
739
|
+
else if (emailFragment) {
|
|
740
|
+
proposal = planEmailFragmentCopies(source, options.emailFragmentMappings);
|
|
741
|
+
}
|
|
742
|
+
else if (webFragment) {
|
|
743
|
+
webFragmentProposal = planWebFragmentCopies(source, options.webFragmentMappings);
|
|
744
|
+
proposal = webFragmentProposal;
|
|
745
|
+
}
|
|
746
|
+
else if (landingPageTemplate) {
|
|
747
|
+
landingPageTemplateProposal = planLandingPageTemplateCopies(source, options.landingPageTemplateMappings);
|
|
748
|
+
proposal = landingPageTemplateProposal;
|
|
749
|
+
}
|
|
750
|
+
else if (landingPage) {
|
|
751
|
+
landingPageProposal = planLandingPageCopies(source, options.landingPageMappings);
|
|
752
|
+
proposal = landingPageProposal;
|
|
753
|
+
}
|
|
754
|
+
else if (options.identityMappings !== undefined)
|
|
755
|
+
proposal = planImportIdentities(source, options.identityMappings);
|
|
756
|
+
const selectedIds = new Set(proposal?.items.map((item) => item.id));
|
|
757
|
+
const editableDiagnostics = editableSource === undefined
|
|
758
|
+
? []
|
|
759
|
+
: [
|
|
760
|
+
{
|
|
761
|
+
code: 'EDITABLE_HTML_INPUT',
|
|
762
|
+
message: `Editable HTML input: original package integrity and references describe only the unchanged baseline, not the edited payload. ${editableSource.entries.filter((entry) => entry.changed && selectedIds.has(entry.variantId)).length} selected HTML modification(s) ${options.dryRun === true ? 'planned' : 'applied'}; ${editableSource.entries.filter((entry) => entry.changed).length} changed entry/entries across all ${editableSource.entries.length} companion entries. No CMS reference rewriting is established by HTML edits.`,
|
|
763
|
+
retryable: false,
|
|
764
|
+
},
|
|
765
|
+
];
|
|
766
|
+
const selectedSource = native || emailFragment || webFragment || landingPageTemplate || landingPage
|
|
767
|
+
? { ...source, items: proposal.items }
|
|
768
|
+
: source;
|
|
769
|
+
const sourcePlan = planWorkspaceImport(selectedSource, options.destinationWorkspace, options.workspaceId);
|
|
770
|
+
// Keep original variant IDs and source manifest associations; never substitute the raw package.
|
|
771
|
+
const proposedById = new Map(proposal?.items.map((item) => [item.id, item]));
|
|
772
|
+
const plan = proposal === undefined
|
|
773
|
+
? sourcePlan
|
|
774
|
+
: {
|
|
775
|
+
...sourcePlan,
|
|
776
|
+
source,
|
|
777
|
+
groups: sourcePlan.groups.map((group) => ({
|
|
778
|
+
contentKey: proposedById.get(group.primary.id).contentKey,
|
|
779
|
+
primary: proposedById.get(group.primary.id),
|
|
780
|
+
variants: group.variants.map((variant) => proposedById.get(variant.id)),
|
|
781
|
+
})),
|
|
782
|
+
};
|
|
783
|
+
let selectedReferences;
|
|
784
|
+
if (native)
|
|
785
|
+
selectedReferences = selectedNativeReferences(source, proposal.items);
|
|
786
|
+
else if (landingPageTemplate || landingPage) {
|
|
787
|
+
selectedReferences = selectedLandingPageTemplateReferences(source, proposal.items);
|
|
788
|
+
}
|
|
789
|
+
preflightReferences(plan, selectedReferences, landingPageTemplate || landingPage);
|
|
500
790
|
const requestOptions = options.requestOptions ?? {};
|
|
501
|
-
|
|
791
|
+
if (!native)
|
|
792
|
+
await preflightConflicts(options.connection, plan, requestOptions);
|
|
793
|
+
if (emailFragment || webFragment || landingPageTemplate || landingPage)
|
|
794
|
+
await preflightApiNames(options.connection, plan);
|
|
795
|
+
if (webFragment) {
|
|
796
|
+
await validateDataGraphPrerequisites(options.connection, webFragmentProposal.dataGraphs);
|
|
797
|
+
}
|
|
798
|
+
if (landingPageTemplate) {
|
|
799
|
+
await validateLandingPageTemplatePrerequisites(options.connection, plan.destinationWorkspaceId, landingPageTemplateProposal, requestOptions);
|
|
800
|
+
}
|
|
801
|
+
if (landingPage) {
|
|
802
|
+
await validateLandingPagePrerequisites(options.connection, plan.destinationWorkspaceId, landingPageProposal, requestOptions);
|
|
803
|
+
}
|
|
804
|
+
const hasNamedIdentities = plan.groups.some((group) => [group.primary, ...group.variants].some((item) => item.apiName !== undefined ||
|
|
805
|
+
item.urlName !== undefined ||
|
|
806
|
+
item.contentBody['sfdc_cms:urlName'] !== undefined));
|
|
807
|
+
if (!native &&
|
|
808
|
+
!emailFragment &&
|
|
809
|
+
!webFragment &&
|
|
810
|
+
!landingPageTemplate &&
|
|
811
|
+
!landingPage &&
|
|
812
|
+
hasNamedIdentities &&
|
|
813
|
+
options.dryRun !== true) {
|
|
814
|
+
throw new TypeError('Destination API-name/URL-name uniqueness cannot be verified by the supported CMS APIs; import is blocked');
|
|
815
|
+
}
|
|
502
816
|
const references = source.manifest.externalReferences
|
|
503
817
|
.filter(({ resolution }) => resolution !== 'included')
|
|
504
818
|
.map(({ kind, referenceId, resolution }) => ({
|
|
@@ -508,7 +822,32 @@ export async function executeWorkspaceImport(options) {
|
|
|
508
822
|
}))
|
|
509
823
|
.toSorted(compareMappingIdentity);
|
|
510
824
|
if (options.dryRun === true) {
|
|
825
|
+
const diagnostics = [
|
|
826
|
+
...editableDiagnostics,
|
|
827
|
+
{
|
|
828
|
+
code: 'SERVER_CONFLICT_CHECK_UNVERIFIED',
|
|
829
|
+
message: native
|
|
830
|
+
? 'Native copy omits contentKey. API-name conflicts can reject; duplicate URLs can create distinct objects. Destination name availability is not prevalidated.'
|
|
831
|
+
: 'Content-key absence was checked, but complete server conflict validation is unverified; this is a read-only proposal.',
|
|
832
|
+
retryable: false,
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
code: 'APPLY_READINESS_UNVERIFIED',
|
|
836
|
+
message: 'Dry-run success is not deploy readiness and does not authorize apply.',
|
|
837
|
+
retryable: false,
|
|
838
|
+
},
|
|
839
|
+
];
|
|
840
|
+
if (hasNamedIdentities && !native) {
|
|
841
|
+
diagnostics.unshift({
|
|
842
|
+
code: 'NAME_AVAILABILITY_UNVERIFIED',
|
|
843
|
+
message: emailFragment || webFragment || landingPageTemplate || landingPage
|
|
844
|
+
? `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.`
|
|
845
|
+
: 'Destination API-name/URL-name availability is unverified; named apply remains blocked until collision evidence is available.',
|
|
846
|
+
retryable: false,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
511
849
|
return {
|
|
850
|
+
diagnostics,
|
|
512
851
|
contractResult: importContractResult(source, options.destinationOrgId, plan, [], references),
|
|
513
852
|
dryRun: true,
|
|
514
853
|
plan,
|
|
@@ -521,6 +860,21 @@ export async function executeWorkspaceImport(options) {
|
|
|
521
860
|
await mkdir(reportDirectory);
|
|
522
861
|
const reportFile = path.join(reportDirectory, 'workspace-import-run.json');
|
|
523
862
|
const report = {
|
|
863
|
+
...(editableSource === undefined ? {} : { editableSource }),
|
|
864
|
+
...(landingPageTemplateProposal === undefined && landingPageProposal === undefined
|
|
865
|
+
? {}
|
|
866
|
+
: {
|
|
867
|
+
cmsPrerequisites: landingPageTemplateProposal?.cmsPrerequisites ?? landingPageProposal.cmsPrerequisites,
|
|
868
|
+
}),
|
|
869
|
+
...(webFragmentProposal === undefined &&
|
|
870
|
+
landingPageTemplateProposal === undefined &&
|
|
871
|
+
landingPageProposal === undefined
|
|
872
|
+
? {}
|
|
873
|
+
: {
|
|
874
|
+
dataGraphPrerequisites: webFragmentProposal?.dataGraphs ??
|
|
875
|
+
landingPageTemplateProposal?.dataGraphs ??
|
|
876
|
+
landingPageProposal.dataGraphs,
|
|
877
|
+
}),
|
|
524
878
|
createdParents: [],
|
|
525
879
|
destinationOrgId: options.destinationOrgId,
|
|
526
880
|
destinationWorkspaceId: plan.destinationWorkspaceId,
|
|
@@ -548,11 +902,31 @@ export async function executeWorkspaceImport(options) {
|
|
|
548
902
|
]));
|
|
549
903
|
const sourceReferenceValues = new Set(sourceReferencesByValue.keys());
|
|
550
904
|
const unresolvedMappingIds = new Set();
|
|
905
|
+
// Editable native copies have no reference-bearing body or child variants. Bind every
|
|
906
|
+
// exact CREATE payload in the initial durable journal, before any mutation.
|
|
907
|
+
if (editableSource !== undefined) {
|
|
908
|
+
for (const group of plan.groups) {
|
|
909
|
+
const payload = createPayload(group, plan.destinationWorkspaceId);
|
|
910
|
+
delete payload.contentKey;
|
|
911
|
+
delete payload.externalId;
|
|
912
|
+
delete payload.externalSource;
|
|
913
|
+
report.operations.push(pendingOperation(report, 'create-parent', group.contentKey, group.primary.language, payload));
|
|
914
|
+
}
|
|
915
|
+
}
|
|
551
916
|
await writeExclusive(reportFile, report);
|
|
552
917
|
try {
|
|
553
918
|
for (const group of plan.groups) {
|
|
919
|
+
if (landingPageTemplate) {
|
|
920
|
+
await validateLandingPageTemplatePrerequisites(options.connection, plan.destinationWorkspaceId, landingPageTemplateProposal, requestOptions);
|
|
921
|
+
}
|
|
922
|
+
else if (landingPage) {
|
|
923
|
+
await validateLandingPagePrerequisites(options.connection, plan.destinationWorkspaceId, landingPageProposal, requestOptions);
|
|
924
|
+
}
|
|
925
|
+
const finalPrimary = proposal?.items.find((item) => item.id === group.primary.id) ?? group.primary;
|
|
926
|
+
const finalVariants = group.variants.map((variant) => proposal?.items.find((item) => item.id === variant.id) ?? variant);
|
|
927
|
+
const finalGroup = { ...group, primary: finalPrimary, variants: finalVariants };
|
|
554
928
|
const groupReferenceKeys = new Set();
|
|
555
|
-
for (const item_ of [
|
|
929
|
+
for (const item_ of [finalGroup.primary, ...finalGroup.variants]) {
|
|
556
930
|
for (const key of cmsContentBodyReferenceKeys(item_.contentBody, sourceReferenceValues)) {
|
|
557
931
|
groupReferenceKeys.add(key);
|
|
558
932
|
}
|
|
@@ -569,22 +943,46 @@ export async function executeWorkspaceImport(options) {
|
|
|
569
943
|
}
|
|
570
944
|
}
|
|
571
945
|
const rewrittenGroup = hasUnknownRequiredMapping
|
|
572
|
-
?
|
|
946
|
+
? finalGroup
|
|
573
947
|
: {
|
|
574
|
-
...
|
|
575
|
-
primary: rewriteItem(
|
|
576
|
-
variants:
|
|
948
|
+
...finalGroup,
|
|
949
|
+
primary: rewriteItem(finalGroup.primary, replacements),
|
|
950
|
+
variants: finalGroup.variants.map((variant) => rewriteItem(variant, replacements)),
|
|
951
|
+
};
|
|
952
|
+
const parentPayload = createPayload(rewrittenGroup, native ? plan.destinationWorkspaceId : plan.rootFolderId);
|
|
953
|
+
if (native) {
|
|
954
|
+
delete parentPayload.contentKey;
|
|
955
|
+
delete parentPayload.externalId;
|
|
956
|
+
delete parentPayload.externalSource;
|
|
957
|
+
}
|
|
958
|
+
if (emailFragment || webFragment || landingPageTemplate || landingPage) {
|
|
959
|
+
const currentPlan = {
|
|
960
|
+
...plan,
|
|
961
|
+
groups: [rewrittenGroup],
|
|
577
962
|
};
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
963
|
+
await preflightConflicts(options.connection, currentPlan, requestOptions);
|
|
964
|
+
await preflightApiNames(options.connection, currentPlan);
|
|
965
|
+
if (webFragment) {
|
|
966
|
+
await validateDataGraphPrerequisites(options.connection, webFragmentProposal.dataGraphs);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const parentOperation = editableSource === undefined
|
|
970
|
+
? pendingOperation(report, 'create-parent', group.contentKey, group.primary.language, parentPayload)
|
|
971
|
+
: report.operations.find((operation) => operation.contentKey === group.contentKey);
|
|
972
|
+
if (editableSource === undefined)
|
|
973
|
+
report.operations.push(parentOperation);
|
|
974
|
+
else if (parentOperation.requestSha256 !==
|
|
975
|
+
operationIdentity(report.runId, 'create-parent', group.contentKey, group.primary.language, parentPayload).requestSha256)
|
|
976
|
+
throw new TypeError('Editable CREATE payload changed after initial journal');
|
|
581
977
|
await rewrite(reportFile, report);
|
|
582
978
|
let parentResponse;
|
|
583
979
|
try {
|
|
584
980
|
parentResponse = await createContent(options.connection, parentPayload, requestOptions);
|
|
585
981
|
}
|
|
586
982
|
catch (error) {
|
|
587
|
-
parentOperation.state = 'failed';
|
|
983
|
+
parentOperation.state = native ? 'pending' : 'failed';
|
|
984
|
+
if (native)
|
|
985
|
+
report.state = 'ownership-uncertain';
|
|
588
986
|
parentOperation.error = error instanceof Error ? error.message : String(error);
|
|
589
987
|
try {
|
|
590
988
|
await rewrite(reportFile, report);
|
|
@@ -594,14 +992,38 @@ export async function executeWorkspaceImport(options) {
|
|
|
594
992
|
}
|
|
595
993
|
throw error;
|
|
596
994
|
}
|
|
597
|
-
|
|
995
|
+
// Persist returned identity before any semantic verification or further requests.
|
|
996
|
+
if (native) {
|
|
997
|
+
parentOperation.result = {
|
|
998
|
+
contentKey: typeof parentResponse.contentKey === 'string' ? parentResponse.contentKey : undefined,
|
|
999
|
+
contentId: [parentResponse.managedContentId, parentResponse.id].find(nonemptyString),
|
|
1000
|
+
primaryVariantId: [
|
|
1001
|
+
parentResponse.managedContentVariantId,
|
|
1002
|
+
parentResponse.primaryVariantId,
|
|
1003
|
+
].find(nonemptyString),
|
|
1004
|
+
};
|
|
1005
|
+
report.state = 'ownership-uncertain';
|
|
1006
|
+
await persistOperationResult(reportFile, report, parentOperation, rewrite);
|
|
1007
|
+
}
|
|
1008
|
+
const created = validateParentResponse(parentResponse, native
|
|
1009
|
+
? responseIdentifier(parentResponse, ['contentKey'], 'generated key')
|
|
1010
|
+
: group.contentKey);
|
|
598
1011
|
report.createdParents.push(created);
|
|
599
1012
|
parentOperation.state = 'succeeded';
|
|
600
1013
|
parentOperation.result = {
|
|
601
1014
|
contentId: created.contentId,
|
|
1015
|
+
contentKey: created.contentKey,
|
|
602
1016
|
primaryVariantId: created.primaryVariantId,
|
|
603
1017
|
};
|
|
604
1018
|
await persistOperationResult(reportFile, report, parentOperation, rewrite);
|
|
1019
|
+
if (native) {
|
|
1020
|
+
if (source.items.some((item) => item.contentKey === created.contentKey ||
|
|
1021
|
+
variantIdentity(item, item.id).contentId === created.contentId))
|
|
1022
|
+
throw new TypeError('Native response reused a source identity');
|
|
1023
|
+
const readback = await getContent(options.connection, created.contentKey, {}, requestOptions);
|
|
1024
|
+
verifyNativeCopy(readback, parentPayload, created, group.primary.language);
|
|
1025
|
+
report.state = 'applying';
|
|
1026
|
+
}
|
|
605
1027
|
const reference = referenceByVariantId.get(group.primary.id);
|
|
606
1028
|
if (reference !== undefined) {
|
|
607
1029
|
replacements.set(reference.source.sourceId, created.contentId);
|
|
@@ -667,7 +1089,8 @@ export async function executeWorkspaceImport(options) {
|
|
|
667
1089
|
await rewrite(reportFile, report);
|
|
668
1090
|
}
|
|
669
1091
|
catch (error) {
|
|
670
|
-
if (!(error instanceof WorkspaceImportOwnershipUncertainError)
|
|
1092
|
+
if (!(error instanceof WorkspaceImportOwnershipUncertainError) &&
|
|
1093
|
+
report.state !== 'ownership-uncertain')
|
|
671
1094
|
report.state = 'failed';
|
|
672
1095
|
try {
|
|
673
1096
|
await rewrite(reportFile, report);
|
|
@@ -688,6 +1111,7 @@ export async function executeWorkspaceImport(options) {
|
|
|
688
1111
|
}
|
|
689
1112
|
}
|
|
690
1113
|
return {
|
|
1114
|
+
diagnostics: editableDiagnostics,
|
|
691
1115
|
contractResult: importContractResult(source, options.destinationOrgId, plan, mappings, references),
|
|
692
1116
|
dryRun: false,
|
|
693
1117
|
plan,
|