sb-mig 6.2.0 → 6.3.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 +15 -4
- package/dist/api/assets/asset-folders.d.ts +3 -0
- package/dist/api/assets/asset-folders.js +56 -0
- package/dist/api/assets/asset-folders.types.d.ts +29 -0
- package/dist/api/assets/asset-folders.types.js +1 -0
- package/dist/api/assets/assets.d.ts +4 -1
- package/dist/api/assets/assets.js +61 -16
- package/dist/api/assets/assets.types.d.ts +12 -1
- package/dist/api/assets/index.d.ts +4 -2
- package/dist/api/assets/index.js +2 -1
- package/dist/api/copy/assets.d.ts +12 -0
- package/dist/api/copy/assets.js +84 -0
- package/dist/api/copy/graph.d.ts +8 -0
- package/dist/api/copy/graph.js +27 -0
- package/dist/api/copy/index.d.ts +6 -0
- package/dist/api/copy/index.js +6 -0
- package/dist/api/copy/manifest.d.ts +23 -0
- package/dist/api/copy/manifest.js +115 -0
- package/dist/api/copy/reference-rewriter.d.ts +6 -0
- package/dist/api/copy/reference-rewriter.js +153 -0
- package/dist/api/copy/reference-scanner.d.ts +23 -0
- package/dist/api/copy/reference-scanner.js +322 -0
- package/dist/api/copy/types.d.ts +196 -0
- package/dist/api/copy/types.js +1 -0
- package/dist/api/managementApi.d.ts +6 -0
- package/dist/api/stories/stories.js +3 -3
- package/dist/api/stories/stories.types.d.ts +3 -1
- package/dist/api/testApi.d.ts +6 -0
- package/dist/api-v2/requestConfig.d.ts +17 -1
- package/dist/api-v2/requestConfig.js +28 -0
- package/dist/cli/cli-descriptions.d.ts +2 -2
- package/dist/cli/cli-descriptions.js +83 -18
- package/dist/cli/commands/copy.js +2091 -145
- package/dist/utils/files.d.ts +1 -0
- package/dist/utils/files.js +77 -3
- package/dist-cjs/api/stories/stories.js +3 -3
- package/dist-cjs/api-v2/requestConfig.js +29 -0
- package/package.json +2 -2
|
@@ -1,195 +1,2141 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { appendManifestEntry, buildCopyAssetsGraph, buildCopyMaps, createCopyGraph, dedupeManifestFile, getDefaultCopyManifestPaths, loadManifest, normalizeAssetFolderParentId, rewriteCopyReferences, scanStoriesReferences, summarizeCopyGraph, } from "../../api/copy/index.js";
|
|
4
|
+
import { buildPublishedLayerContext, resolveStoryLayerState, } from "../../api/data-migration/published-layer.js";
|
|
1
5
|
import { managementApi } from "../../api/managementApi.js";
|
|
2
|
-
import {
|
|
6
|
+
import { parsePublishLanguagesOption, resolvePublishLanguageCodes, } from "../../api/stories/stories.js";
|
|
7
|
+
import { createTree } from "../../api/stories/tree.js";
|
|
8
|
+
import { mapWithConcurrency } from "../../utils/async-utils.js";
|
|
3
9
|
import Logger from "../../utils/logger.js";
|
|
10
|
+
import { getFileName } from "../../utils/string-utils.js";
|
|
4
11
|
import { apiConfig } from "../api-config.js";
|
|
5
|
-
import { getSourceSpace, getTargetSpace, getWhat, getWhere, } from "../utils/cli-utils.js";
|
|
6
12
|
const COPY_COMMANDS = {
|
|
7
13
|
stories: "stories",
|
|
14
|
+
assets: "assets",
|
|
8
15
|
};
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
const COPY_MODES = ["subtree", "children", "self"];
|
|
17
|
+
const TARGET_CONFLICT_CHECK_CONCURRENCY = 10;
|
|
18
|
+
const COPY_DRY_RUN_BASE_LIMITATIONS = [
|
|
19
|
+
"create_or_match",
|
|
20
|
+
"target_state_may_change",
|
|
21
|
+
];
|
|
22
|
+
const COPY_DRY_RUN_STORY_ONLY_LIMITATIONS = [
|
|
23
|
+
"assets_not_copied_by_story_command",
|
|
24
|
+
"asset_rewrite_requires_existing_asset_manifest",
|
|
25
|
+
];
|
|
26
|
+
const COPY_DRY_RUN_WITH_ASSETS_LIMITATIONS = [
|
|
27
|
+
"target_asset_identity_not_resolved_until_apply",
|
|
28
|
+
];
|
|
29
|
+
const isCopyMode = (value) => COPY_MODES.includes(value);
|
|
30
|
+
const readStringFlag = (flags, names) => {
|
|
31
|
+
for (const name of names) {
|
|
32
|
+
const value = flags[name];
|
|
33
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
34
|
+
return String(value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
};
|
|
39
|
+
const readStringListFlag = (flags, names) => {
|
|
40
|
+
const values = [];
|
|
41
|
+
for (const name of names) {
|
|
42
|
+
const value = flags[name];
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
values.push(...value.map(String));
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
48
|
+
values.push(String(value));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return values
|
|
52
|
+
.map((value) => value.trim())
|
|
53
|
+
.filter((value) => value.length > 0);
|
|
54
|
+
};
|
|
55
|
+
const getCopySpace = (flags, names, fallback) => readStringFlag(flags, names) ?? fallback;
|
|
56
|
+
const parseCopyPublicationMode = (publicationModeFlag) => {
|
|
57
|
+
if (!publicationModeFlag) {
|
|
58
|
+
return "preserve-layers";
|
|
59
|
+
}
|
|
60
|
+
if (publicationModeFlag === "preserve-layers" ||
|
|
61
|
+
publicationModeFlag === "collapse-draft" ||
|
|
62
|
+
publicationModeFlag === "save-only") {
|
|
63
|
+
return publicationModeFlag;
|
|
64
|
+
}
|
|
65
|
+
throw new Error("--publicationMode must be one of: preserve-layers, collapse-draft, save-only.");
|
|
66
|
+
};
|
|
67
|
+
const resolveCopyPublicationOptions = async ({ flags, targetSpace, dryRun, }) => {
|
|
68
|
+
const mode = parseCopyPublicationMode(readStringFlag(flags, ["publicationMode", "publication-mode"]));
|
|
69
|
+
const publicationLanguagesFlag = readStringFlag(flags, [
|
|
70
|
+
"publicationLanguages",
|
|
71
|
+
"publication-languages",
|
|
72
|
+
]);
|
|
73
|
+
if (mode === "save-only" && publicationLanguagesFlag) {
|
|
74
|
+
throw new Error("--publicationLanguages cannot be used with --publicationMode save-only.");
|
|
75
|
+
}
|
|
76
|
+
if (mode === "save-only") {
|
|
77
|
+
return { mode };
|
|
78
|
+
}
|
|
79
|
+
const publishLanguages = publicationLanguagesFlag
|
|
80
|
+
? parsePublishLanguagesOption(publicationLanguagesFlag)
|
|
81
|
+
: "all";
|
|
82
|
+
return {
|
|
83
|
+
mode,
|
|
84
|
+
publishLanguages,
|
|
85
|
+
resolvedPublishLanguages: dryRun
|
|
86
|
+
? undefined
|
|
87
|
+
: await resolvePublishLanguageCodes(publishLanguages, {
|
|
88
|
+
...apiConfig,
|
|
89
|
+
spaceId: targetSpace,
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
const normalizeDestination = (destination) => !destination || destination === "/" || destination === "root"
|
|
94
|
+
? ""
|
|
95
|
+
: destination.replace(/^\/+|\/+$/g, "");
|
|
96
|
+
const joinSlugs = (...parts) => parts
|
|
97
|
+
.filter((part) => Boolean(part))
|
|
98
|
+
.map((part) => part.replace(/^\/+|\/+$/g, ""))
|
|
99
|
+
.filter((part) => part.length > 0)
|
|
100
|
+
.join("/");
|
|
101
|
+
const normalizeAssetFolderPath = (folderPath) => folderPath.replace(/^\/+|\/+$/g, "");
|
|
102
|
+
const toSelectionReport = (selection) => selection.type === "all"
|
|
103
|
+
? "all"
|
|
104
|
+
: selection.type === "referenced_by_stories"
|
|
105
|
+
? {
|
|
106
|
+
type: "referenced_by_stories",
|
|
107
|
+
source: selection.storySelection.source,
|
|
108
|
+
mode: selection.storySelection.mode,
|
|
109
|
+
}
|
|
110
|
+
: {
|
|
111
|
+
type: selection.type,
|
|
112
|
+
values: selection.values,
|
|
113
|
+
};
|
|
114
|
+
const resolveCopyAssetsSelection = (flags) => {
|
|
115
|
+
const assetValues = readStringListFlag(flags, ["asset", "assetId"]);
|
|
116
|
+
const assetFolderValues = readStringListFlag(flags, [
|
|
117
|
+
"assetFolder",
|
|
118
|
+
"asset-folder",
|
|
119
|
+
"assetFolderId",
|
|
120
|
+
"asset-folder-id",
|
|
121
|
+
]);
|
|
122
|
+
const hasAll = Boolean(flags["all"]);
|
|
123
|
+
const hasReferencedByStories = Boolean(flags["referencedByStories"] ?? flags["referenced-by-stories"]);
|
|
124
|
+
const selectorCount = (hasAll ? 1 : 0) +
|
|
125
|
+
(assetValues.length > 0 ? 1 : 0) +
|
|
126
|
+
(assetFolderValues.length > 0 ? 1 : 0) +
|
|
127
|
+
(hasReferencedByStories ? 1 : 0);
|
|
128
|
+
if (selectorCount === 0) {
|
|
129
|
+
throw new Error("copy assets requires one selector: --all, --asset <id|url|unique-name>, --assetFolder <id|path>, or --referenced-by-stories --source <full_slug>.");
|
|
130
|
+
}
|
|
131
|
+
if (selectorCount > 1) {
|
|
132
|
+
throw new Error("copy assets accepts only one selector family at a time. Use --all, --asset, --assetFolder, or --referenced-by-stories.");
|
|
133
|
+
}
|
|
134
|
+
if (hasReferencedByStories) {
|
|
135
|
+
return {
|
|
136
|
+
type: "referenced_by_stories",
|
|
137
|
+
storySelection: resolveCopySelection(flags),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (assetValues.length > 0) {
|
|
141
|
+
return {
|
|
142
|
+
type: "asset",
|
|
143
|
+
values: assetValues,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (assetFolderValues.length > 0) {
|
|
147
|
+
return {
|
|
148
|
+
type: "asset_folder",
|
|
149
|
+
values: assetFolderValues.map(normalizeAssetFolderPath),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
type: "all",
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
const resolveCopySelection = (flags) => {
|
|
157
|
+
const rawSource = readStringFlag(flags, ["source", "what"]);
|
|
158
|
+
if (!rawSource) {
|
|
159
|
+
throw new Error("Missing source. Pass --source <full_slug> (or legacy --what <full_slug>).");
|
|
160
|
+
}
|
|
161
|
+
const rawMode = readStringFlag(flags, ["mode"]);
|
|
162
|
+
if (rawMode && !isCopyMode(rawMode)) {
|
|
163
|
+
throw new Error(`Unsupported copy mode '${rawMode}'. Use one of: ${COPY_MODES.join(", ")}.`);
|
|
164
|
+
}
|
|
165
|
+
const explicitMode = rawMode;
|
|
166
|
+
if (rawSource.endsWith("/*")) {
|
|
167
|
+
return {
|
|
168
|
+
source: rawSource.slice(0, -2),
|
|
169
|
+
mode: explicitMode ?? "children",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
source: rawSource,
|
|
174
|
+
mode: explicitMode ?? "subtree",
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
const resolveDestinationParentId = async (destination, targetSpace) => {
|
|
178
|
+
if (!destination || destination === "/" || destination === "root") {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const targetEntryStory = await managementApi.stories.getStoryBySlug(destination, {
|
|
12
182
|
...apiConfig,
|
|
13
183
|
spaceId: targetSpace,
|
|
14
184
|
});
|
|
15
|
-
|
|
185
|
+
if (!targetEntryStory) {
|
|
186
|
+
throw new Error(`Destination story or folder not found in target space: ${destination}`);
|
|
187
|
+
}
|
|
188
|
+
if (!targetEntryStory.story.is_folder) {
|
|
189
|
+
throw new Error(`Destination must be a folder or root. '${destination}' is not a folder.`);
|
|
190
|
+
}
|
|
191
|
+
return targetEntryStory.story.id;
|
|
192
|
+
};
|
|
193
|
+
const getStoryBySlugOrThrow = async (slug, sourceSpace) => {
|
|
194
|
+
const entryStory = await managementApi.stories.getStoryBySlug(slug, {
|
|
195
|
+
...apiConfig,
|
|
196
|
+
spaceId: sourceSpace,
|
|
197
|
+
});
|
|
198
|
+
if (!entryStory) {
|
|
199
|
+
throw new Error(`Source story or folder not found: ${slug}`);
|
|
200
|
+
}
|
|
16
201
|
return entryStory;
|
|
17
202
|
};
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
203
|
+
const getStoriesForSelection = async (selection, sourceSpace) => {
|
|
204
|
+
const rootStory = await getStoryBySlugOrThrow(selection.source, sourceSpace);
|
|
205
|
+
const root = rootStory.story;
|
|
206
|
+
if (selection.mode === "self") {
|
|
207
|
+
return [rootStory];
|
|
208
|
+
}
|
|
209
|
+
if (!root.is_folder) {
|
|
210
|
+
if (selection.mode === "children") {
|
|
211
|
+
throw new Error(`Copy mode 'children' requires a folder source. '${selection.source}' is not a folder.`);
|
|
212
|
+
}
|
|
213
|
+
return [rootStory];
|
|
214
|
+
}
|
|
215
|
+
const children = await managementApi.stories.getAllStories({
|
|
216
|
+
options: {
|
|
217
|
+
starts_with: `${selection.source}/`,
|
|
218
|
+
},
|
|
219
|
+
}, {
|
|
220
|
+
...apiConfig,
|
|
221
|
+
spaceId: sourceSpace,
|
|
222
|
+
});
|
|
223
|
+
return [rootStory, ...children];
|
|
224
|
+
};
|
|
225
|
+
const stripGeneratedStoryFields = (story) => {
|
|
226
|
+
const { id, uuid, created_at, updated_at, published_at, first_published_at, last_author, alternates, parent, ...copyableStory } = story;
|
|
227
|
+
return copyableStory;
|
|
228
|
+
};
|
|
229
|
+
const normalizeStoriesForTree = (stories, selection) => stories.map((item) => {
|
|
230
|
+
const story = item.story;
|
|
231
|
+
const copyableStory = stripGeneratedStoryFields(story);
|
|
232
|
+
if (story.full_slug === selection.source) {
|
|
233
|
+
return {
|
|
234
|
+
...copyableStory,
|
|
235
|
+
id: story.id,
|
|
236
|
+
parent_id: null,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
...copyableStory,
|
|
241
|
+
id: story.id,
|
|
242
|
+
parent_id: story.parent_id === 0 ? null : story.parent_id,
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
const selectTreeRoots = (tree, selection) => {
|
|
246
|
+
if (selection.mode === "children") {
|
|
247
|
+
return tree[0]?.children ?? [];
|
|
248
|
+
}
|
|
249
|
+
return tree;
|
|
250
|
+
};
|
|
251
|
+
const prepareTreeForCreate = (tree) => tree.map((node) => ({
|
|
252
|
+
...node,
|
|
253
|
+
story: stripGeneratedStoryFields(node.story),
|
|
254
|
+
children: node.children ? prepareTreeForCreate(node.children) : [],
|
|
255
|
+
}));
|
|
256
|
+
const resolveStorySlug = (story) => {
|
|
257
|
+
if (typeof story.slug === "string" && story.slug.length > 0) {
|
|
258
|
+
return story.slug;
|
|
259
|
+
}
|
|
260
|
+
const fullSlug = String(story.full_slug ?? "");
|
|
261
|
+
return fullSlug.split("/").filter(Boolean).at(-1) ?? "";
|
|
262
|
+
};
|
|
263
|
+
const buildStoryShellPayload = (story, parentId) => {
|
|
264
|
+
const component = story.content?.component;
|
|
265
|
+
return {
|
|
266
|
+
name: story.name,
|
|
267
|
+
slug: resolveStorySlug(story),
|
|
268
|
+
is_folder: story.is_folder === true,
|
|
269
|
+
...(parentId !== null ? { parent_id: parentId } : {}),
|
|
270
|
+
...(story.is_startpage === true && parentId !== null
|
|
271
|
+
? { is_startpage: true }
|
|
272
|
+
: {}),
|
|
273
|
+
...(component
|
|
274
|
+
? {
|
|
275
|
+
content: {
|
|
276
|
+
_uid: "",
|
|
277
|
+
component,
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
: {}),
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
const buildFinalStoryPayload = ({ sourceStory, targetParentId, rewrittenContent, }) => {
|
|
284
|
+
const payload = stripGeneratedStoryFields(sourceStory);
|
|
285
|
+
delete payload.full_slug;
|
|
286
|
+
delete payload.parent_id;
|
|
287
|
+
payload.slug = resolveStorySlug(sourceStory);
|
|
288
|
+
payload.content = rewrittenContent;
|
|
289
|
+
if (targetParentId !== null) {
|
|
290
|
+
payload.parent_id = targetParentId;
|
|
291
|
+
}
|
|
292
|
+
if (sourceStory.is_startpage === true && targetParentId !== null) {
|
|
293
|
+
payload.is_startpage = true;
|
|
294
|
+
}
|
|
295
|
+
return payload;
|
|
296
|
+
};
|
|
297
|
+
const getValidMappedTargetStory = async ({ sourceStory, targetStoryId, targetFullSlug, targetSpace, }) => {
|
|
298
|
+
const targetStory = await managementApi.stories.getStoryById(String(targetStoryId), {
|
|
299
|
+
...apiConfig,
|
|
300
|
+
spaceId: targetSpace,
|
|
301
|
+
});
|
|
302
|
+
if (targetStory?.story?.id) {
|
|
303
|
+
if (!targetFullSlug) {
|
|
304
|
+
return targetStory.story;
|
|
305
|
+
}
|
|
306
|
+
const targetStoryBySlug = await managementApi.stories.getStoryBySlug(targetFullSlug, {
|
|
307
|
+
...apiConfig,
|
|
308
|
+
spaceId: targetSpace,
|
|
309
|
+
});
|
|
310
|
+
if (Number(targetStoryBySlug?.story?.id) === targetStoryId) {
|
|
311
|
+
return targetStory.story;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
Logger.warning(`Ignoring stale story manifest mapping for '${sourceStory.full_slug ?? sourceStory.slug}' because target story '${targetStoryId}' was not found at the expected target path in space '${targetSpace}'.`);
|
|
315
|
+
return undefined;
|
|
316
|
+
};
|
|
317
|
+
const assertStoryUpdateSucceeded = ({ result, sourceStory, targetStoryId, targetSpace, }) => {
|
|
318
|
+
if (result?.ok !== false) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const statusLabel = result.status
|
|
322
|
+
? `status ${result.status}`
|
|
323
|
+
: "unknown status";
|
|
324
|
+
const responseLabel = result.response
|
|
325
|
+
? ` Response: ${result.response}`
|
|
326
|
+
: "";
|
|
327
|
+
throw new Error(`Failed to update copied story '${sourceStory.full_slug ?? sourceStory.slug}' in target space '${targetSpace}' (target story id ${targetStoryId}, ${statusLabel}).${responseLabel}`);
|
|
328
|
+
};
|
|
329
|
+
const isStoryUpdateNotFound = (result) => result?.ok === false && Number(result.status) === 404;
|
|
330
|
+
const buildCopyPlan = (tree, destination) => {
|
|
331
|
+
const destinationRoot = normalizeDestination(destination);
|
|
332
|
+
const plan = [];
|
|
333
|
+
const walk = (nodes, parentTargetSlug) => {
|
|
334
|
+
for (const node of nodes) {
|
|
335
|
+
const story = node.story;
|
|
336
|
+
const targetFullSlug = joinSlugs(parentTargetSlug, resolveStorySlug(story));
|
|
337
|
+
plan.push({
|
|
338
|
+
type: story.is_folder ? "folder" : "story",
|
|
339
|
+
sourceFullSlug: String(story.full_slug ?? story.slug ?? ""),
|
|
340
|
+
targetFullSlug,
|
|
341
|
+
name: String(story.name ?? story.slug ?? "unknown"),
|
|
342
|
+
action: "create",
|
|
343
|
+
});
|
|
344
|
+
if (node.children?.length) {
|
|
345
|
+
walk(node.children, targetFullSlug);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
walk(tree, destinationRoot);
|
|
350
|
+
return plan;
|
|
351
|
+
};
|
|
352
|
+
const findTargetConflicts = async (plan, targetSpace) => {
|
|
353
|
+
let checked = 0;
|
|
354
|
+
if (plan.length === 0) {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
Logger.warning(`Checking ${plan.length} planned target path(s) for existing stories/folders.`);
|
|
358
|
+
const results = await mapWithConcurrency(plan, TARGET_CONFLICT_CHECK_CONCURRENCY, async (item) => {
|
|
359
|
+
const existingStory = await managementApi.stories.getStoryBySlug(item.targetFullSlug, {
|
|
24
360
|
...apiConfig,
|
|
25
|
-
spaceId:
|
|
361
|
+
spaceId: targetSpace,
|
|
26
362
|
});
|
|
27
|
-
|
|
28
|
-
|
|
363
|
+
checked += 1;
|
|
364
|
+
if (checked === plan.length ||
|
|
365
|
+
checked % 25 === 0 ||
|
|
366
|
+
plan.length <= 25) {
|
|
367
|
+
Logger.success(`Checked ${checked} of ${plan.length} target path(s) for conflicts.`);
|
|
29
368
|
}
|
|
30
|
-
|
|
31
|
-
|
|
369
|
+
return existingStory ? item : null;
|
|
370
|
+
});
|
|
371
|
+
const conflicts = results.filter((item) => item !== null);
|
|
372
|
+
Logger.success(`Target conflict check complete. Found ${conflicts.length} existing target path(s).`);
|
|
373
|
+
return conflicts;
|
|
374
|
+
};
|
|
375
|
+
const withConflictFlags = (plan, conflicts) => {
|
|
376
|
+
const conflictSlugs = new Set(conflicts.map((conflict) => conflict.targetFullSlug));
|
|
377
|
+
return plan.map((item) => ({
|
|
378
|
+
...item,
|
|
379
|
+
...(conflictSlugs.has(item.targetFullSlug) ? { conflict: true } : {}),
|
|
380
|
+
}));
|
|
381
|
+
};
|
|
382
|
+
const buildDryRunWarnings = ({ conflicts, withAssets, }) => [
|
|
383
|
+
...conflicts.map((conflict) => ({
|
|
384
|
+
code: "target_exists",
|
|
385
|
+
message: `Target ${conflict.type} already exists at '${conflict.targetFullSlug}'. Current copy is create-only, so the real copy may fail.`,
|
|
386
|
+
targetFullSlug: conflict.targetFullSlug,
|
|
387
|
+
})),
|
|
388
|
+
...(withAssets
|
|
389
|
+
? []
|
|
390
|
+
: [
|
|
391
|
+
{
|
|
392
|
+
code: "assets_not_copied_by_story_command",
|
|
393
|
+
message: "Assets are not copied by this command unless --with-assets is passed.",
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
code: "asset_rewrite_requires_existing_asset_manifest",
|
|
397
|
+
message: "Asset fields are rewritten only when matching asset manifest entries already exist.",
|
|
398
|
+
},
|
|
399
|
+
]),
|
|
400
|
+
];
|
|
401
|
+
const quoteCommandArg = (value) => /^[a-zA-Z0-9_./:-]+$/.test(value) ? value : JSON.stringify(value);
|
|
402
|
+
const buildCopyCommand = ({ sourceSpace, targetSpace, selection, destination, withAssets, dryRun, outputPath, }) => {
|
|
403
|
+
const args = [
|
|
404
|
+
"sb-mig",
|
|
405
|
+
"copy",
|
|
406
|
+
"stories",
|
|
407
|
+
"--from",
|
|
408
|
+
sourceSpace,
|
|
409
|
+
"--to",
|
|
410
|
+
targetSpace,
|
|
411
|
+
"--source",
|
|
412
|
+
selection.source,
|
|
413
|
+
"--mode",
|
|
414
|
+
selection.mode,
|
|
415
|
+
];
|
|
416
|
+
if (destination) {
|
|
417
|
+
args.push("--destination", destination);
|
|
418
|
+
}
|
|
419
|
+
if (withAssets) {
|
|
420
|
+
args.push("--with-assets");
|
|
421
|
+
}
|
|
422
|
+
if (dryRun) {
|
|
423
|
+
args.push("--dry-run");
|
|
424
|
+
}
|
|
425
|
+
if (outputPath) {
|
|
426
|
+
args.push("--outputPath", outputPath);
|
|
427
|
+
}
|
|
428
|
+
return args.map(quoteCommandArg).join(" ");
|
|
429
|
+
};
|
|
430
|
+
const buildCopyAssetsCommand = ({ sourceSpace, targetSpace, selection, dryRun, outputPath, }) => {
|
|
431
|
+
const args = [
|
|
432
|
+
"sb-mig",
|
|
433
|
+
"copy",
|
|
434
|
+
"assets",
|
|
435
|
+
"--from",
|
|
436
|
+
sourceSpace,
|
|
437
|
+
"--to",
|
|
438
|
+
targetSpace,
|
|
439
|
+
];
|
|
440
|
+
if (selection.type === "all") {
|
|
441
|
+
args.push("--all");
|
|
442
|
+
}
|
|
443
|
+
else if (selection.type === "asset") {
|
|
444
|
+
for (const value of selection.values) {
|
|
445
|
+
args.push("--asset", value);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else if (selection.type === "asset_folder") {
|
|
449
|
+
for (const value of selection.values) {
|
|
450
|
+
args.push("--assetFolder", value);
|
|
32
451
|
}
|
|
33
|
-
return { strategy: "folder_recursive" };
|
|
34
452
|
}
|
|
35
|
-
|
|
36
|
-
|
|
453
|
+
else {
|
|
454
|
+
args.push("--referenced-by-stories", "--source", selection.storySelection.source, "--mode", selection.storySelection.mode);
|
|
455
|
+
}
|
|
456
|
+
if (dryRun) {
|
|
457
|
+
args.push("--dry-run");
|
|
458
|
+
}
|
|
459
|
+
if (outputPath) {
|
|
460
|
+
args.push("--outputPath", outputPath);
|
|
461
|
+
}
|
|
462
|
+
return args.map(quoteCommandArg).join(" ");
|
|
463
|
+
};
|
|
464
|
+
const parseStoryblokAssetUrl = (filename) => {
|
|
465
|
+
if (!filename) {
|
|
466
|
+
return {};
|
|
467
|
+
}
|
|
468
|
+
let url;
|
|
469
|
+
try {
|
|
470
|
+
url = new URL(filename);
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
return {
|
|
474
|
+
uniqueKey: filename,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (url.hostname !== "a.storyblok.com") {
|
|
478
|
+
return {
|
|
479
|
+
uniqueKey: filename,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
483
|
+
const fIndex = parts.indexOf("f");
|
|
484
|
+
const spaceId = fIndex >= 0 ? parts[fIndex + 1] : undefined;
|
|
485
|
+
const assetHash = fIndex >= 0 ? parts[fIndex + 3] : undefined;
|
|
486
|
+
const assetName = fIndex >= 0 ? parts.slice(fIndex + 4).join("/") : "";
|
|
487
|
+
if (!spaceId || !assetHash || !assetName) {
|
|
488
|
+
return {
|
|
489
|
+
spaceId,
|
|
490
|
+
uniqueKey: filename,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
spaceId,
|
|
495
|
+
uniqueKey: `storyblok:${spaceId}:${assetHash}:${assetName}`,
|
|
496
|
+
};
|
|
497
|
+
};
|
|
498
|
+
const getAssetReferenceUniqueKey = (reference) => {
|
|
499
|
+
const parsed = parseStoryblokAssetUrl(reference.filename);
|
|
500
|
+
return (parsed.uniqueKey ??
|
|
501
|
+
(reference.assetId === undefined
|
|
502
|
+
? `unknown:${reference.path}`
|
|
503
|
+
: `asset-id:${reference.assetId}`));
|
|
504
|
+
};
|
|
505
|
+
const buildAssetReferenceSummary = ({ graph, sourceSpace, }) => {
|
|
506
|
+
if (!graph) {
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
const statuses = [
|
|
510
|
+
"mapped",
|
|
511
|
+
"planned",
|
|
512
|
+
"unresolved",
|
|
513
|
+
"unsupported",
|
|
514
|
+
];
|
|
515
|
+
const uniqueByStatus = new Map(statuses.map((status) => [status, new Set()]));
|
|
516
|
+
const occurrencesByStatus = new Map(statuses.map((status) => [status, 0]));
|
|
517
|
+
const foreignSpaces = new Map();
|
|
518
|
+
for (const reference of graph.assetReferences) {
|
|
519
|
+
const status = reference.status;
|
|
520
|
+
const uniqueKey = getAssetReferenceUniqueKey(reference);
|
|
521
|
+
occurrencesByStatus.set(status, (occurrencesByStatus.get(status) ?? 0) + 1);
|
|
522
|
+
uniqueByStatus.get(status)?.add(uniqueKey);
|
|
523
|
+
const parsed = parseStoryblokAssetUrl(reference.filename);
|
|
524
|
+
if (parsed.spaceId && parsed.spaceId !== sourceSpace) {
|
|
525
|
+
const existing = foreignSpaces.get(parsed.spaceId) ?? {
|
|
526
|
+
occurrences: 0,
|
|
527
|
+
uniqueAssets: new Set(),
|
|
528
|
+
};
|
|
529
|
+
existing.occurrences += 1;
|
|
530
|
+
existing.uniqueAssets.add(uniqueKey);
|
|
531
|
+
foreignSpaces.set(parsed.spaceId, existing);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const bucket = (status) => ({
|
|
535
|
+
occurrences: occurrencesByStatus.get(status) ?? 0,
|
|
536
|
+
uniqueAssets: uniqueByStatus.get(status)?.size ?? 0,
|
|
537
|
+
});
|
|
538
|
+
return {
|
|
539
|
+
mapped: bucket("mapped"),
|
|
540
|
+
planned: bucket("planned"),
|
|
541
|
+
unresolved: bucket("unresolved"),
|
|
542
|
+
unsupported: bucket("unsupported"),
|
|
543
|
+
foreignAssetSpaces: Array.from(foreignSpaces.entries())
|
|
544
|
+
.map(([spaceId, summary]) => ({
|
|
545
|
+
spaceId,
|
|
546
|
+
occurrences: summary.occurrences,
|
|
547
|
+
uniqueAssets: summary.uniqueAssets.size,
|
|
548
|
+
}))
|
|
549
|
+
.sort((left, right) => left.spaceId.localeCompare(right.spaceId)),
|
|
550
|
+
};
|
|
551
|
+
};
|
|
552
|
+
const buildCopyDryRunReport = ({ sourceSpace, targetSpace, selection, destination, withAssets, input, plan, conflicts, graph, outputPath, }) => {
|
|
553
|
+
const items = withConflictFlags(plan, conflicts);
|
|
554
|
+
const warnings = buildDryRunWarnings({ conflicts, withAssets });
|
|
555
|
+
const graphSummary = graph ? summarizeCopyGraph(graph) : undefined;
|
|
556
|
+
const assetReferencesMapped = graph?.assetReferences.filter((reference) => reference.status === "mapped").length ?? 0;
|
|
557
|
+
const assetReferencesPlanned = graph?.assetReferences.filter((reference) => reference.status === "planned").length ?? 0;
|
|
558
|
+
const assetReferencesUnresolved = graph?.assetReferences.filter((reference) => reference.status === "unresolved").length ?? 0;
|
|
559
|
+
const storyReferencesMapped = graph?.storyReferences.filter((reference) => reference.status === "mapped").length ?? 0;
|
|
560
|
+
const storyReferencesPreserved = graph?.storyReferences.filter((reference) => reference.status === "preserved_external").length ?? 0;
|
|
561
|
+
const storyReferencesUnresolved = graph?.storyReferences.filter((reference) => reference.status === "unresolved").length ?? 0;
|
|
562
|
+
const assetsMapped = graph?.assets.filter((asset) => asset.action === "match").length ?? 0;
|
|
563
|
+
const assetsToCopy = graph?.assets.filter((asset) => asset.action === "create").length ?? 0;
|
|
564
|
+
const assetReferenceSummary = buildAssetReferenceSummary({
|
|
565
|
+
graph,
|
|
566
|
+
sourceSpace,
|
|
567
|
+
});
|
|
568
|
+
const limitations = [
|
|
569
|
+
...COPY_DRY_RUN_BASE_LIMITATIONS,
|
|
570
|
+
...(withAssets
|
|
571
|
+
? COPY_DRY_RUN_WITH_ASSETS_LIMITATIONS
|
|
572
|
+
: COPY_DRY_RUN_STORY_ONLY_LIMITATIONS),
|
|
573
|
+
];
|
|
574
|
+
return {
|
|
575
|
+
schemaVersion: 1,
|
|
576
|
+
command: "copy stories",
|
|
577
|
+
dryRun: true,
|
|
578
|
+
generatedAt: new Date().toISOString(),
|
|
579
|
+
input,
|
|
580
|
+
normalized: {
|
|
581
|
+
sourceSpaceId: sourceSpace,
|
|
582
|
+
targetSpaceId: targetSpace,
|
|
583
|
+
source: selection.source,
|
|
584
|
+
destination: normalizeDestination(destination) || "root",
|
|
585
|
+
mode: selection.mode,
|
|
586
|
+
withAssets,
|
|
587
|
+
},
|
|
588
|
+
summary: {
|
|
589
|
+
plannedCreates: items.length,
|
|
590
|
+
folders: items.filter((item) => item.type === "folder").length,
|
|
591
|
+
stories: items.filter((item) => item.type === "story").length,
|
|
592
|
+
assetFolders: graphSummary?.assetFolders ?? 0,
|
|
593
|
+
assets: graphSummary?.assets ?? 0,
|
|
594
|
+
assetReferences: graphSummary?.assetReferences ?? 0,
|
|
595
|
+
assetReferencesMapped,
|
|
596
|
+
assetReferencesPlanned,
|
|
597
|
+
assetReferencesUnresolved,
|
|
598
|
+
assetsMapped,
|
|
599
|
+
assetsToCopy,
|
|
600
|
+
storyReferences: graphSummary?.storyReferences ?? 0,
|
|
601
|
+
storyReferencesMapped,
|
|
602
|
+
storyReferencesPreserved,
|
|
603
|
+
storyReferencesUnresolved,
|
|
604
|
+
conflicts: conflicts.length,
|
|
605
|
+
warnings: warnings.length + (graphSummary?.warnings ?? 0),
|
|
606
|
+
errors: graphSummary?.errors ?? 0,
|
|
607
|
+
},
|
|
608
|
+
items,
|
|
609
|
+
...(graph ? { graph } : {}),
|
|
610
|
+
...(assetReferenceSummary ? { assetReferenceSummary } : {}),
|
|
611
|
+
warnings,
|
|
612
|
+
errors: graph?.errors ?? [],
|
|
613
|
+
limitations,
|
|
614
|
+
commands: {
|
|
615
|
+
dryRun: buildCopyCommand({
|
|
616
|
+
sourceSpace,
|
|
617
|
+
targetSpace,
|
|
618
|
+
selection,
|
|
619
|
+
destination,
|
|
620
|
+
withAssets,
|
|
621
|
+
dryRun: true,
|
|
622
|
+
outputPath,
|
|
623
|
+
}),
|
|
624
|
+
apply: buildCopyCommand({
|
|
625
|
+
sourceSpace,
|
|
626
|
+
targetSpace,
|
|
627
|
+
selection,
|
|
628
|
+
destination,
|
|
629
|
+
withAssets,
|
|
630
|
+
dryRun: false,
|
|
631
|
+
}),
|
|
632
|
+
},
|
|
633
|
+
};
|
|
634
|
+
};
|
|
635
|
+
const buildCopyAssetsDryRunReport = ({ sourceSpace, targetSpace, selection, input, outputPath, graph, }) => {
|
|
636
|
+
const graphSummary = summarizeCopyGraph(graph);
|
|
637
|
+
return {
|
|
638
|
+
schemaVersion: 1,
|
|
639
|
+
command: "copy assets",
|
|
640
|
+
dryRun: true,
|
|
641
|
+
generatedAt: graph.generatedAt,
|
|
642
|
+
input,
|
|
643
|
+
normalized: {
|
|
644
|
+
sourceSpaceId: sourceSpace,
|
|
645
|
+
targetSpaceId: targetSpace,
|
|
646
|
+
selection: toSelectionReport(selection),
|
|
647
|
+
},
|
|
648
|
+
summary: {
|
|
649
|
+
plannedCreates: graphSummary.assetFolders + graphSummary.assets,
|
|
650
|
+
assetFolders: graphSummary.assetFolders,
|
|
651
|
+
assets: graphSummary.assets,
|
|
652
|
+
warnings: graphSummary.warnings,
|
|
653
|
+
errors: graphSummary.errors,
|
|
654
|
+
},
|
|
655
|
+
graph,
|
|
656
|
+
limitations: graph.limitations,
|
|
657
|
+
commands: {
|
|
658
|
+
dryRun: buildCopyAssetsCommand({
|
|
659
|
+
sourceSpace,
|
|
660
|
+
targetSpace,
|
|
661
|
+
selection,
|
|
662
|
+
dryRun: true,
|
|
663
|
+
outputPath,
|
|
664
|
+
}),
|
|
665
|
+
apply: buildCopyAssetsCommand({
|
|
666
|
+
sourceSpace,
|
|
667
|
+
targetSpace,
|
|
668
|
+
selection,
|
|
669
|
+
dryRun: false,
|
|
670
|
+
}),
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
};
|
|
674
|
+
const buildCopyStoriesApplyReport = ({ sourceSpace, targetSpace, selection, destination, withAssets, input, plan, storySummary, graph, assetCopyReport, manifestRoot, }) => {
|
|
675
|
+
const manifestPaths = getDefaultCopyManifestPaths({
|
|
676
|
+
sourceSpaceId: sourceSpace,
|
|
677
|
+
targetSpaceId: targetSpace,
|
|
678
|
+
rootDir: manifestRoot,
|
|
679
|
+
});
|
|
680
|
+
const graphSummary = graph ? summarizeCopyGraph(graph) : undefined;
|
|
681
|
+
return {
|
|
682
|
+
schemaVersion: 1,
|
|
683
|
+
command: "copy stories",
|
|
684
|
+
dryRun: false,
|
|
685
|
+
generatedAt: new Date().toISOString(),
|
|
686
|
+
input,
|
|
687
|
+
normalized: {
|
|
688
|
+
sourceSpaceId: sourceSpace,
|
|
689
|
+
targetSpaceId: targetSpace,
|
|
690
|
+
source: selection.source,
|
|
691
|
+
destination: normalizeDestination(destination) || "root",
|
|
692
|
+
mode: selection.mode,
|
|
693
|
+
withAssets,
|
|
694
|
+
},
|
|
695
|
+
summary: {
|
|
696
|
+
...storySummary,
|
|
697
|
+
...(assetCopyReport
|
|
698
|
+
? {
|
|
699
|
+
assetFoldersCreated: assetCopyReport.summary.assetFoldersCreated,
|
|
700
|
+
assetFoldersMatched: assetCopyReport.summary.assetFoldersMatched,
|
|
701
|
+
assetsCreated: assetCopyReport.summary.assetsCreated,
|
|
702
|
+
assetsMatched: assetCopyReport.summary.assetsMatched,
|
|
703
|
+
}
|
|
704
|
+
: {}),
|
|
705
|
+
warnings: (graphSummary?.warnings ?? 0) +
|
|
706
|
+
(assetCopyReport?.summary.warnings ?? 0),
|
|
707
|
+
errors: (graphSummary?.errors ?? 0) +
|
|
708
|
+
(assetCopyReport?.summary.errors ?? 0),
|
|
709
|
+
},
|
|
710
|
+
items: plan,
|
|
711
|
+
...(graph ? { graph } : {}),
|
|
712
|
+
...(assetCopyReport ? { assetCopy: assetCopyReport } : {}),
|
|
713
|
+
manifestPaths: {
|
|
714
|
+
stories: manifestPaths.stories,
|
|
715
|
+
assets: manifestPaths.assets,
|
|
716
|
+
assetFolders: manifestPaths.assetFolders,
|
|
717
|
+
combined: manifestPaths.combined,
|
|
718
|
+
},
|
|
719
|
+
warnings: [
|
|
720
|
+
...(graph?.warnings ?? []),
|
|
721
|
+
...(assetCopyReport?.warnings ?? []),
|
|
722
|
+
],
|
|
723
|
+
errors: [...(graph?.errors ?? []), ...(assetCopyReport?.errors ?? [])],
|
|
724
|
+
};
|
|
725
|
+
};
|
|
726
|
+
const writeDryRunReport = async (outputPath, report) => {
|
|
727
|
+
const outputDirectory = path.dirname(outputPath);
|
|
728
|
+
await fs.mkdir(outputDirectory, { recursive: true });
|
|
729
|
+
await fs.writeFile(outputPath, JSON.stringify(report, null, 2), "utf8");
|
|
730
|
+
Logger.success(`[dry-run] Copy plan written to ${outputPath}`);
|
|
731
|
+
};
|
|
732
|
+
const writeJsonReport = async (outputPath, report) => {
|
|
733
|
+
const outputDirectory = path.dirname(outputPath);
|
|
734
|
+
await fs.mkdir(outputDirectory, { recursive: true });
|
|
735
|
+
await fs.writeFile(outputPath, JSON.stringify(report, null, 2), "utf8");
|
|
736
|
+
Logger.success(`Copy report written to ${outputPath}`);
|
|
737
|
+
};
|
|
738
|
+
const appendCopyManifestEntry = async ({ combinedPath, resourcePath, entry, }) => {
|
|
739
|
+
await appendManifestEntry(resourcePath, entry);
|
|
740
|
+
await appendManifestEntry(combinedPath, entry);
|
|
741
|
+
};
|
|
742
|
+
const getAssetFolderPath = (folder, folderById) => {
|
|
743
|
+
const names = [String(folder.name ?? folder.id)];
|
|
744
|
+
const visited = new Set([Number(folder.id)]);
|
|
745
|
+
let parentId = folder.parent_id;
|
|
746
|
+
while (parentId !== null && parentId !== undefined) {
|
|
747
|
+
const parent = folderById.get(Number(parentId));
|
|
748
|
+
if (!parent || visited.has(Number(parent.id))) {
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
names.unshift(String(parent.name ?? parent.id));
|
|
752
|
+
visited.add(Number(parent.id));
|
|
753
|
+
parentId = parent.parent_id;
|
|
754
|
+
}
|
|
755
|
+
return names.join("/");
|
|
756
|
+
};
|
|
757
|
+
const buildAssetFolderPathMap = (folders) => {
|
|
758
|
+
const folderById = new Map(folders.map((folder) => [Number(folder.id), folder]));
|
|
759
|
+
const folderByPath = new Map();
|
|
760
|
+
for (const folder of folders) {
|
|
761
|
+
folderByPath.set(getAssetFolderPath(folder, folderById), folder);
|
|
762
|
+
}
|
|
763
|
+
return folderByPath;
|
|
764
|
+
};
|
|
765
|
+
const isNumericSelector = (value) => /^\d+$/.test(value);
|
|
766
|
+
const findSourceAssetBySelector = (assets, selector) => {
|
|
767
|
+
if (isNumericSelector(selector)) {
|
|
768
|
+
const byId = assets.find((asset) => Number(asset.id) === Number(selector));
|
|
769
|
+
if (byId) {
|
|
770
|
+
return byId;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const exactFilenameMatches = assets.filter((asset) => asset.filename === selector);
|
|
774
|
+
if (exactFilenameMatches.length === 1) {
|
|
775
|
+
return exactFilenameMatches[0];
|
|
776
|
+
}
|
|
777
|
+
if (exactFilenameMatches.length > 1) {
|
|
778
|
+
throw new Error(`Asset selector '${selector}' matched multiple source assets by filename. Use --asset <asset-id>.`);
|
|
779
|
+
}
|
|
780
|
+
const fileNameMatches = assets.filter((asset) => getFileName(asset.filename) === selector);
|
|
781
|
+
if (fileNameMatches.length === 1) {
|
|
782
|
+
return fileNameMatches[0];
|
|
783
|
+
}
|
|
784
|
+
if (fileNameMatches.length > 1) {
|
|
785
|
+
throw new Error(`Asset selector '${selector}' matched multiple source assets by file name. Use --asset <asset-id>.`);
|
|
786
|
+
}
|
|
787
|
+
throw new Error(`Asset selector '${selector}' did not match a source asset by id, filename, or unique file name.`);
|
|
788
|
+
};
|
|
789
|
+
const findSourceAssetFolderBySelector = (folderById, folderByPath, selector) => {
|
|
790
|
+
if (isNumericSelector(selector)) {
|
|
791
|
+
const byId = folderById.get(Number(selector));
|
|
792
|
+
if (byId) {
|
|
793
|
+
return byId;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const normalizedPath = normalizeAssetFolderPath(selector);
|
|
797
|
+
const byPath = folderByPath.get(normalizedPath);
|
|
798
|
+
if (byPath) {
|
|
799
|
+
return byPath;
|
|
800
|
+
}
|
|
801
|
+
throw new Error(`Asset folder selector '${selector}' did not match a source asset folder by id or path.`);
|
|
802
|
+
};
|
|
803
|
+
const addAssetFolderAncestors = ({ folderId, folderById, selectedFolderIds, }) => {
|
|
804
|
+
const visited = new Set();
|
|
805
|
+
let currentFolderId = folderId;
|
|
806
|
+
while (currentFolderId !== null && currentFolderId !== undefined) {
|
|
807
|
+
const currentFolder = folderById.get(Number(currentFolderId));
|
|
808
|
+
if (!currentFolder || visited.has(Number(currentFolder.id))) {
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
selectedFolderIds.add(Number(currentFolder.id));
|
|
812
|
+
visited.add(Number(currentFolder.id));
|
|
813
|
+
currentFolderId = currentFolder.parent_id;
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
const collectAssetFolderSubtreeIds = (rootFolderId, folders) => {
|
|
817
|
+
const selected = new Set([rootFolderId]);
|
|
818
|
+
let changed = true;
|
|
819
|
+
while (changed) {
|
|
820
|
+
changed = false;
|
|
821
|
+
for (const folder of folders) {
|
|
822
|
+
const folderId = Number(folder.id);
|
|
823
|
+
const parentId = folder.parent_id;
|
|
824
|
+
if (parentId !== null &&
|
|
825
|
+
parentId !== undefined &&
|
|
826
|
+
selected.has(Number(parentId)) &&
|
|
827
|
+
!selected.has(folderId)) {
|
|
828
|
+
selected.add(folderId);
|
|
829
|
+
changed = true;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return selected;
|
|
834
|
+
};
|
|
835
|
+
const selectSourceAssetsForCopy = ({ selection, sourceAssets, sourceAssetFolders, }) => {
|
|
836
|
+
if (selection.type === "all") {
|
|
837
|
+
return {
|
|
838
|
+
assets: sourceAssets,
|
|
839
|
+
assetFolders: sourceAssetFolders,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
const folderById = new Map(sourceAssetFolders.map((folder) => [Number(folder.id), folder]));
|
|
843
|
+
const folderByPath = buildAssetFolderPathMap(sourceAssetFolders);
|
|
844
|
+
const selectedAssetIds = new Set();
|
|
845
|
+
const selectedFolderIds = new Set();
|
|
846
|
+
if (selection.type === "asset") {
|
|
847
|
+
for (const selector of selection.values) {
|
|
848
|
+
const asset = findSourceAssetBySelector(sourceAssets, selector);
|
|
849
|
+
selectedAssetIds.add(Number(asset.id));
|
|
850
|
+
addAssetFolderAncestors({
|
|
851
|
+
folderId: asset.asset_folder_id,
|
|
852
|
+
folderById,
|
|
853
|
+
selectedFolderIds,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (selection.type === "asset_folder") {
|
|
858
|
+
for (const selector of selection.values) {
|
|
859
|
+
const folder = findSourceAssetFolderBySelector(folderById, folderByPath, selector);
|
|
860
|
+
const subtreeFolderIds = collectAssetFolderSubtreeIds(Number(folder.id), sourceAssetFolders);
|
|
861
|
+
for (const folderId of subtreeFolderIds) {
|
|
862
|
+
selectedFolderIds.add(folderId);
|
|
863
|
+
}
|
|
864
|
+
addAssetFolderAncestors({
|
|
865
|
+
folderId: folder.parent_id,
|
|
866
|
+
folderById,
|
|
867
|
+
selectedFolderIds,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
for (const asset of sourceAssets) {
|
|
871
|
+
if (asset.asset_folder_id !== null &&
|
|
872
|
+
asset.asset_folder_id !== undefined &&
|
|
873
|
+
selectedFolderIds.has(Number(asset.asset_folder_id))) {
|
|
874
|
+
selectedAssetIds.add(Number(asset.id));
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
assets: sourceAssets.filter((asset) => selectedAssetIds.has(Number(asset.id))),
|
|
880
|
+
assetFolders: sourceAssetFolders.filter((folder) => selectedFolderIds.has(Number(folder.id))),
|
|
881
|
+
};
|
|
882
|
+
};
|
|
883
|
+
const selectSourceAssetsFromGraph = ({ graph, sourceAssets, sourceAssetFolders, }) => {
|
|
884
|
+
const graphAssetIds = new Set(graph.assets.map((asset) => Number(asset.sourceId)));
|
|
885
|
+
const graphAssetFolderIds = new Set(graph.assetFolders.map((folder) => Number(folder.sourceId)));
|
|
886
|
+
return {
|
|
887
|
+
assets: sourceAssets.filter((asset) => graphAssetIds.has(Number(asset.id))),
|
|
888
|
+
assetFolders: sourceAssetFolders.filter((folder) => graphAssetFolderIds.has(Number(folder.id))),
|
|
889
|
+
};
|
|
890
|
+
};
|
|
891
|
+
const getAssetMetadataPayload = (asset) => ({
|
|
892
|
+
...(asset.alt ? { alt: asset.alt } : {}),
|
|
893
|
+
...(asset.title ? { title: asset.title } : {}),
|
|
894
|
+
...(asset.copyright ? { copyright: asset.copyright } : {}),
|
|
895
|
+
...(asset.source ? { source: asset.source } : {}),
|
|
896
|
+
...(asset.focus ? { focus: asset.focus } : {}),
|
|
897
|
+
...(asset.meta_data ? { meta_data: asset.meta_data } : {}),
|
|
898
|
+
...(asset.is_private === undefined ? {} : { is_private: asset.is_private }),
|
|
899
|
+
...(asset.locked === undefined ? {} : { locked: asset.locked }),
|
|
900
|
+
...(asset.publish_at === undefined ? {} : { publish_at: asset.publish_at }),
|
|
901
|
+
...(asset.internal_tag_ids?.length
|
|
902
|
+
? { internal_tag_ids: asset.internal_tag_ids }
|
|
903
|
+
: {}),
|
|
904
|
+
});
|
|
905
|
+
const findUniqueTargetAssetByFileName = (targetAssets, fileName) => {
|
|
906
|
+
const matches = targetAssets.filter((asset) => getFileName(asset.filename) === fileName);
|
|
907
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
908
|
+
};
|
|
909
|
+
const buildCopyAssetsApplyReport = ({ sourceSpace, targetSpace, selection, input, graph, manifestPaths, assetFoldersCreated, assetFoldersMatched, assetsCreated, assetsMatched, }) => ({
|
|
910
|
+
schemaVersion: 1,
|
|
911
|
+
command: "copy assets",
|
|
912
|
+
dryRun: false,
|
|
913
|
+
generatedAt: graph.generatedAt,
|
|
914
|
+
input,
|
|
915
|
+
normalized: {
|
|
916
|
+
sourceSpaceId: sourceSpace,
|
|
917
|
+
targetSpaceId: targetSpace,
|
|
918
|
+
selection: toSelectionReport(selection),
|
|
919
|
+
},
|
|
920
|
+
summary: {
|
|
921
|
+
assetFoldersCreated,
|
|
922
|
+
assetFoldersMatched,
|
|
923
|
+
assetsCreated,
|
|
924
|
+
assetsMatched,
|
|
925
|
+
warnings: graph.warnings.length,
|
|
926
|
+
errors: graph.errors.length,
|
|
927
|
+
},
|
|
928
|
+
graph,
|
|
929
|
+
manifestPaths: {
|
|
930
|
+
assets: manifestPaths.assets,
|
|
931
|
+
assetFolders: manifestPaths.assetFolders,
|
|
932
|
+
combined: manifestPaths.combined,
|
|
933
|
+
},
|
|
934
|
+
warnings: graph.warnings,
|
|
935
|
+
errors: graph.errors,
|
|
936
|
+
});
|
|
937
|
+
const buildSourceStoryById = (sourceStories) => new Map(sourceStories
|
|
938
|
+
.map((item) => item?.story)
|
|
939
|
+
.filter(Boolean)
|
|
940
|
+
.map((story) => [Number(story.id), story]));
|
|
941
|
+
const buildTargetSlugBySourceSlug = (plan) => new Map(plan.map((item) => [item.sourceFullSlug, item.targetFullSlug]));
|
|
942
|
+
const countTreeStories = (nodes) => nodes.reduce((counts, node) => {
|
|
943
|
+
const childCounts = countTreeStories(node.children ?? []);
|
|
944
|
+
return {
|
|
945
|
+
folders: counts.folders +
|
|
946
|
+
childCounts.folders +
|
|
947
|
+
(node.story?.is_folder ? 1 : 0),
|
|
948
|
+
stories: counts.stories +
|
|
949
|
+
childCounts.stories +
|
|
950
|
+
(node.story?.is_folder ? 0 : 1),
|
|
951
|
+
};
|
|
952
|
+
}, { folders: 0, stories: 0 });
|
|
953
|
+
const buildComponentSchemaRegistry = async (sourceSpace) => {
|
|
954
|
+
const components = await managementApi.components.getAllComponents({
|
|
37
955
|
...apiConfig,
|
|
38
956
|
spaceId: sourceSpace,
|
|
39
957
|
});
|
|
40
|
-
if (!
|
|
41
|
-
|
|
958
|
+
if (!Array.isArray(components)) {
|
|
959
|
+
return {};
|
|
42
960
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
961
|
+
return Object.fromEntries(components
|
|
962
|
+
.filter((component) => component?.name && component?.schema)
|
|
963
|
+
.map((component) => [component.name, component.schema]));
|
|
964
|
+
};
|
|
965
|
+
const collectAssetFolderAncestors = ({ assets, assetFolders, }) => {
|
|
966
|
+
const folderById = new Map(assetFolders.map((folder) => [Number(folder.id), folder]));
|
|
967
|
+
const selectedFolderIds = new Set();
|
|
968
|
+
for (const asset of assets) {
|
|
969
|
+
let folderId = asset.asset_folder_id;
|
|
970
|
+
const visited = new Set();
|
|
971
|
+
while (folderId !== null && folderId !== undefined) {
|
|
972
|
+
const numericFolderId = Number(folderId);
|
|
973
|
+
if (visited.has(numericFolderId)) {
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
visited.add(numericFolderId);
|
|
977
|
+
selectedFolderIds.add(numericFolderId);
|
|
978
|
+
const folder = folderById.get(numericFolderId);
|
|
979
|
+
if (!folder) {
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
folderId = folder.parent_id;
|
|
983
|
+
}
|
|
46
984
|
}
|
|
47
|
-
return
|
|
985
|
+
return assetFolders.filter((folder) => selectedFolderIds.has(Number(folder.id)));
|
|
48
986
|
};
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
987
|
+
const hasMappedAssetReference = ({ assetId, filename, copyMaps, }) => (assetId !== undefined && copyMaps.assetIds.has(assetId)) ||
|
|
988
|
+
(filename !== undefined && copyMaps.assetFilenames.has(filename));
|
|
989
|
+
const annotateReferencesWithManifestMaps = ({ graph, copyMaps, withAssets, }) => {
|
|
990
|
+
for (const reference of graph.assetReferences) {
|
|
991
|
+
if (hasMappedAssetReference({ ...reference, copyMaps })) {
|
|
992
|
+
reference.status = "mapped";
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
if (!withAssets && reference.status === "planned") {
|
|
996
|
+
reference.status = "unresolved";
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
for (const reference of graph.storyReferences) {
|
|
1000
|
+
if ((reference.referencedStoryId !== undefined &&
|
|
1001
|
+
copyMaps.storyIds.has(reference.referencedStoryId)) ||
|
|
1002
|
+
(reference.referencedStoryUuid !== undefined &&
|
|
1003
|
+
copyMaps.storyUuids.has(reference.referencedStoryUuid))) {
|
|
1004
|
+
reference.status = "mapped";
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
const annotateAssetsWithManifestMaps = ({ graph, copyMaps, }) => {
|
|
1009
|
+
for (const folder of graph.assetFolders) {
|
|
1010
|
+
const targetFolderId = copyMaps.assetFolderIds.get(folder.sourceId);
|
|
1011
|
+
if (targetFolderId === undefined) {
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
const sourceParentId = normalizeAssetFolderParentId(folder.sourceParentId);
|
|
1015
|
+
folder.targetParentId =
|
|
1016
|
+
sourceParentId === null
|
|
1017
|
+
? null
|
|
1018
|
+
: (copyMaps.assetFolderIds.get(sourceParentId) ?? null);
|
|
1019
|
+
folder.action = "match";
|
|
1020
|
+
}
|
|
1021
|
+
for (const asset of graph.assets) {
|
|
1022
|
+
const mappedAsset = copyMaps.assetIds.get(asset.sourceId);
|
|
1023
|
+
const targetFilename = mappedAsset?.filename ??
|
|
1024
|
+
copyMaps.assetFilenames.get(asset.sourceFilename);
|
|
1025
|
+
if (!mappedAsset && !targetFilename) {
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
asset.action = "match";
|
|
1029
|
+
asset.targetFilename = targetFilename ?? asset.targetFilename;
|
|
1030
|
+
asset.targetAssetFolderId =
|
|
1031
|
+
asset.sourceAssetFolderId === null ||
|
|
1032
|
+
asset.sourceAssetFolderId === undefined
|
|
1033
|
+
? null
|
|
1034
|
+
: (copyMaps.assetFolderIds.get(asset.sourceAssetFolderId) ??
|
|
1035
|
+
null);
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
const buildStoryReferenceDryRunGraph = ({ sourceSpace, targetSpace, selection, destination, plan, sourceStories, schemas, copyMaps, onScanProgress, }) => {
|
|
1039
|
+
const sourceStoryByFullSlug = new Map(sourceStories
|
|
1040
|
+
.map((item) => item?.story)
|
|
1041
|
+
.filter(Boolean)
|
|
1042
|
+
.map((story) => [String(story.full_slug ?? ""), story]));
|
|
1043
|
+
const scanResult = scanStoriesReferences({
|
|
1044
|
+
stories: sourceStories.map((item) => item?.story).filter(Boolean),
|
|
1045
|
+
schemas,
|
|
1046
|
+
options: {
|
|
1047
|
+
referencePolicy: "preserve",
|
|
1048
|
+
onProgress: onScanProgress,
|
|
1049
|
+
},
|
|
1050
|
+
});
|
|
1051
|
+
const graph = createCopyGraph({
|
|
1052
|
+
sourceSpaceId: sourceSpace,
|
|
1053
|
+
targetSpaceId: targetSpace,
|
|
1054
|
+
scope: {
|
|
1055
|
+
command: "copy stories",
|
|
1056
|
+
source: selection.source,
|
|
1057
|
+
destination: normalizeDestination(destination) || "root",
|
|
1058
|
+
mode: selection.mode,
|
|
1059
|
+
withAssets: false,
|
|
1060
|
+
referencePolicy: "preserve",
|
|
1061
|
+
},
|
|
1062
|
+
});
|
|
1063
|
+
graph.stories = plan.map((item) => ({
|
|
1064
|
+
type: "story",
|
|
1065
|
+
sourceId: Number(sourceStoryByFullSlug.get(item.sourceFullSlug)?.id ?? 0),
|
|
1066
|
+
sourceUuid: sourceStoryByFullSlug.get(item.sourceFullSlug)?.uuid,
|
|
1067
|
+
sourceFullSlug: item.sourceFullSlug,
|
|
1068
|
+
targetFullSlug: item.targetFullSlug,
|
|
1069
|
+
isFolder: item.type === "folder",
|
|
1070
|
+
action: item.action,
|
|
1071
|
+
}));
|
|
1072
|
+
graph.assetReferences.push(...scanResult.assetReferences);
|
|
1073
|
+
graph.storyReferences.push(...scanResult.storyReferences);
|
|
1074
|
+
graph.opaqueFields.push(...scanResult.opaqueFields);
|
|
1075
|
+
graph.warnings.push(...scanResult.warnings);
|
|
1076
|
+
graph.errors.push(...scanResult.errors);
|
|
1077
|
+
annotateReferencesWithManifestMaps({
|
|
1078
|
+
graph,
|
|
1079
|
+
copyMaps,
|
|
1080
|
+
withAssets: false,
|
|
1081
|
+
});
|
|
1082
|
+
return graph;
|
|
1083
|
+
};
|
|
1084
|
+
const buildReferencedAssetsGraph = ({ sourceSpace, targetSpace, selection, destination, plan, sourceStories, sourceAssets, sourceAssetFolders, schemas, copyMaps, onScanProgress, }) => {
|
|
1085
|
+
const scanResult = scanStoriesReferences({
|
|
1086
|
+
stories: sourceStories.map((item) => item?.story).filter(Boolean),
|
|
1087
|
+
schemas,
|
|
1088
|
+
options: {
|
|
1089
|
+
referencePolicy: "preserve",
|
|
1090
|
+
onProgress: onScanProgress,
|
|
1091
|
+
},
|
|
1092
|
+
});
|
|
1093
|
+
const referencedAssetIds = new Set(scanResult.assetReferences
|
|
1094
|
+
.map((reference) => reference.assetId)
|
|
1095
|
+
.filter((assetId) => assetId !== undefined));
|
|
1096
|
+
const referencedFilenames = new Set(scanResult.assetReferences
|
|
1097
|
+
.map((reference) => reference.filename)
|
|
1098
|
+
.filter((filename) => filename !== undefined));
|
|
1099
|
+
const selectedAssets = sourceAssets.filter((asset) => referencedAssetIds.has(Number(asset.id)) ||
|
|
1100
|
+
referencedFilenames.has(String(asset.filename)));
|
|
1101
|
+
const selectedAssetIds = new Set(selectedAssets.map((asset) => Number(asset.id)));
|
|
1102
|
+
const selectedFilenames = new Set(selectedAssets.map((asset) => String(asset.filename)));
|
|
1103
|
+
const selectedAssetFolders = collectAssetFolderAncestors({
|
|
1104
|
+
assets: selectedAssets,
|
|
1105
|
+
assetFolders: sourceAssetFolders,
|
|
1106
|
+
});
|
|
1107
|
+
const sourceStoryByFullSlug = new Map(sourceStories
|
|
1108
|
+
.map((item) => item?.story)
|
|
1109
|
+
.filter(Boolean)
|
|
1110
|
+
.map((story) => [String(story.full_slug ?? ""), story]));
|
|
1111
|
+
const graph = buildCopyAssetsGraph({
|
|
1112
|
+
sourceSpaceId: sourceSpace,
|
|
1113
|
+
targetSpaceId: targetSpace,
|
|
1114
|
+
assets: selectedAssets,
|
|
1115
|
+
assetFolders: selectedAssetFolders,
|
|
1116
|
+
});
|
|
1117
|
+
graph.scope = {
|
|
1118
|
+
command: "copy stories",
|
|
1119
|
+
source: selection.source,
|
|
1120
|
+
destination: normalizeDestination(destination) || "root",
|
|
1121
|
+
mode: selection.mode,
|
|
1122
|
+
withAssets: true,
|
|
1123
|
+
referencePolicy: "preserve",
|
|
1124
|
+
};
|
|
1125
|
+
graph.stories = plan.map((item) => ({
|
|
1126
|
+
type: "story",
|
|
1127
|
+
sourceId: Number(sourceStoryByFullSlug.get(item.sourceFullSlug)?.id ?? 0),
|
|
1128
|
+
sourceUuid: sourceStoryByFullSlug.get(item.sourceFullSlug)?.uuid,
|
|
1129
|
+
sourceFullSlug: item.sourceFullSlug,
|
|
1130
|
+
targetFullSlug: item.targetFullSlug,
|
|
1131
|
+
isFolder: item.type === "folder",
|
|
1132
|
+
action: item.action,
|
|
1133
|
+
}));
|
|
1134
|
+
graph.assetReferences.push(...scanResult.assetReferences.map((reference) => {
|
|
1135
|
+
const isPlanned = (reference.assetId !== undefined &&
|
|
1136
|
+
selectedAssetIds.has(reference.assetId)) ||
|
|
1137
|
+
(reference.filename !== undefined &&
|
|
1138
|
+
selectedFilenames.has(reference.filename));
|
|
1139
|
+
return {
|
|
1140
|
+
...reference,
|
|
1141
|
+
status: isPlanned ? "planned" : "unresolved",
|
|
1142
|
+
};
|
|
1143
|
+
}));
|
|
1144
|
+
graph.storyReferences.push(...scanResult.storyReferences);
|
|
1145
|
+
graph.opaqueFields.push(...scanResult.opaqueFields);
|
|
1146
|
+
graph.warnings.push(...scanResult.warnings);
|
|
1147
|
+
graph.errors.push(...scanResult.errors);
|
|
1148
|
+
annotateReferencesWithManifestMaps({
|
|
1149
|
+
graph,
|
|
1150
|
+
copyMaps,
|
|
1151
|
+
withAssets: true,
|
|
1152
|
+
});
|
|
1153
|
+
annotateAssetsWithManifestMaps({
|
|
1154
|
+
graph,
|
|
1155
|
+
copyMaps,
|
|
1156
|
+
});
|
|
1157
|
+
for (const reference of graph.assetReferences) {
|
|
1158
|
+
if (reference.status !== "unresolved") {
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
graph.warnings.push({
|
|
1162
|
+
code: "referenced_asset_not_found",
|
|
1163
|
+
message: "A story references an asset that was not returned by the source space asset list.",
|
|
1164
|
+
path: reference.path,
|
|
1165
|
+
sourceValue: reference.assetId ?? reference.filename,
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
return graph;
|
|
1169
|
+
};
|
|
1170
|
+
const countStoryItems = (sourceStories) => sourceStories.map((item) => item?.story).filter(Boolean).length;
|
|
1171
|
+
const logReferenceScanProgress = ({ scanned, total, }) => {
|
|
1172
|
+
Logger.success(`Scanned ${scanned} of ${total} stories for references.`);
|
|
1173
|
+
};
|
|
1174
|
+
const shouldUsePublishedLayerCopy = (publication, sourceStory, publishedLayerRecord) => publication.mode === "preserve-layers" &&
|
|
1175
|
+
resolveStoryLayerState(sourceStory) === "dirty-published" &&
|
|
1176
|
+
Boolean(publishedLayerRecord?.publishedLayerItem?.story);
|
|
1177
|
+
const shouldPublishCopiedCurrentStory = (publication, sourceStory) => {
|
|
1178
|
+
if (publication.mode === "save-only") {
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
const layerState = resolveStoryLayerState(sourceStory);
|
|
1182
|
+
if (layerState === "clean-published") {
|
|
1183
|
+
return true;
|
|
1184
|
+
}
|
|
1185
|
+
if (layerState === "dirty-published") {
|
|
1186
|
+
return publication.mode === "collapse-draft";
|
|
1187
|
+
}
|
|
1188
|
+
return false;
|
|
1189
|
+
};
|
|
1190
|
+
const buildRewrittenStoryPayload = ({ sourceStory, content, targetParentId, maps, schemas, }) => {
|
|
1191
|
+
const rewritten = rewriteCopyReferences({
|
|
1192
|
+
value: content ?? {},
|
|
1193
|
+
maps,
|
|
1194
|
+
schemas,
|
|
1195
|
+
});
|
|
1196
|
+
return {
|
|
1197
|
+
payload: buildFinalStoryPayload({
|
|
1198
|
+
sourceStory,
|
|
1199
|
+
targetParentId,
|
|
1200
|
+
rewrittenContent: rewritten.value,
|
|
1201
|
+
}),
|
|
1202
|
+
rewrittenReferences: rewritten.records.length,
|
|
1203
|
+
};
|
|
1204
|
+
};
|
|
1205
|
+
const publishCopiedStory = async ({ storyId, story, publication, targetSpace, }) => {
|
|
1206
|
+
const languages = publication.resolvedPublishLanguages;
|
|
1207
|
+
if (!languages || languages.length === 0) {
|
|
1208
|
+
return { ok: true, stage: "publish_skipped" };
|
|
1209
|
+
}
|
|
1210
|
+
return managementApi.stories.publishStoryLanguages({
|
|
1211
|
+
storyId,
|
|
1212
|
+
story,
|
|
1213
|
+
languages,
|
|
1214
|
+
}, {
|
|
1215
|
+
...apiConfig,
|
|
1216
|
+
spaceId: targetSpace,
|
|
1217
|
+
});
|
|
1218
|
+
};
|
|
1219
|
+
const rewriteCopiedStoryContents = async ({ tree, realParentId, sourceStoryById, targetSlugBySourceSlug, publication, publishedLayerRecordBySourceId, sourceSpace, targetSpace, manifestRoot, }) => {
|
|
1220
|
+
const manifestPaths = getDefaultCopyManifestPaths({
|
|
1221
|
+
sourceSpaceId: sourceSpace,
|
|
1222
|
+
targetSpaceId: targetSpace,
|
|
1223
|
+
rootDir: manifestRoot,
|
|
1224
|
+
});
|
|
1225
|
+
const manifestEntries = await loadManifest(manifestPaths.combined);
|
|
1226
|
+
const maps = buildCopyMaps(manifestEntries);
|
|
1227
|
+
const schemas = await buildComponentSchemaRegistry(sourceSpace);
|
|
1228
|
+
let updatedStories = 0;
|
|
1229
|
+
let rewrittenReferences = 0;
|
|
1230
|
+
const writeStoryMapping = async ({ sourceStory, targetStory, targetFullSlug, action, }) => {
|
|
1231
|
+
const entry = {
|
|
1232
|
+
type: "story",
|
|
1233
|
+
source_space_id: sourceSpace,
|
|
1234
|
+
target_space_id: targetSpace,
|
|
1235
|
+
source_id: Number(sourceStory.id),
|
|
1236
|
+
target_id: Number(targetStory.id),
|
|
1237
|
+
source_uuid: String(sourceStory.uuid),
|
|
1238
|
+
target_uuid: String(targetStory.uuid),
|
|
1239
|
+
source_full_slug: String(sourceStory.full_slug ?? ""),
|
|
1240
|
+
target_full_slug: targetStory.full_slug ?? targetFullSlug,
|
|
1241
|
+
action,
|
|
1242
|
+
created_at: new Date().toISOString(),
|
|
1243
|
+
};
|
|
1244
|
+
await appendCopyManifestEntry({
|
|
1245
|
+
combinedPath: manifestPaths.combined,
|
|
1246
|
+
resourcePath: manifestPaths.stories,
|
|
1247
|
+
entry,
|
|
1248
|
+
});
|
|
1249
|
+
maps.storyIds.set(entry.source_id, entry.target_id);
|
|
1250
|
+
maps.storyUuids.set(entry.source_uuid, entry.target_uuid);
|
|
1251
|
+
return entry.target_id;
|
|
1252
|
+
};
|
|
1253
|
+
const createOrMatchReplacementShell = async ({ node, sourceStory, parentId, staleTargetId, }) => {
|
|
1254
|
+
const sourceFullSlug = String(sourceStory.full_slug ?? "");
|
|
1255
|
+
const targetFullSlug = targetSlugBySourceSlug.get(sourceFullSlug);
|
|
1256
|
+
const existingTargetStory = targetFullSlug
|
|
1257
|
+
? await managementApi.stories.getStoryBySlug(targetFullSlug, {
|
|
1258
|
+
...apiConfig,
|
|
1259
|
+
spaceId: targetSpace,
|
|
1260
|
+
})
|
|
1261
|
+
: undefined;
|
|
1262
|
+
if (existingTargetStory?.story?.id &&
|
|
1263
|
+
Number(existingTargetStory.story.id) !== staleTargetId) {
|
|
1264
|
+
return writeStoryMapping({
|
|
1265
|
+
sourceStory,
|
|
1266
|
+
targetStory: existingTargetStory.story,
|
|
1267
|
+
targetFullSlug,
|
|
1268
|
+
action: "matched_by_target_key",
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
const createdStoryResult = await managementApi.stories.createStory(buildStoryShellPayload(node.story ?? sourceStory, parentId), {
|
|
1272
|
+
...apiConfig,
|
|
1273
|
+
spaceId: targetSpace,
|
|
1274
|
+
}, {
|
|
1275
|
+
publish: false,
|
|
1276
|
+
});
|
|
1277
|
+
const targetStory = createdStoryResult?.story;
|
|
1278
|
+
if (!targetStory?.id || !targetStory?.uuid) {
|
|
1279
|
+
throw new Error(`Failed to create replacement target story for '${sourceFullSlug}'.`);
|
|
1280
|
+
}
|
|
1281
|
+
return writeStoryMapping({
|
|
1282
|
+
sourceStory,
|
|
1283
|
+
targetStory,
|
|
1284
|
+
targetFullSlug,
|
|
1285
|
+
action: "created",
|
|
1286
|
+
});
|
|
1287
|
+
};
|
|
1288
|
+
const walk = async (nodes, parentId) => {
|
|
1289
|
+
for (const node of nodes) {
|
|
1290
|
+
const sourceId = Number(node.id ?? node.story?.id);
|
|
1291
|
+
const sourceStory = sourceStoryById.get(sourceId);
|
|
1292
|
+
if (!sourceStory?.id) {
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
let targetStoryId = maps.storyIds.get(Number(sourceStory.id));
|
|
1296
|
+
if (!targetStoryId) {
|
|
1297
|
+
targetStoryId = await createOrMatchReplacementShell({
|
|
1298
|
+
node,
|
|
1299
|
+
sourceStory,
|
|
1300
|
+
parentId,
|
|
69
1301
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
})
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
1302
|
+
}
|
|
1303
|
+
const writeStory = async (id) => {
|
|
1304
|
+
const current = buildRewrittenStoryPayload({
|
|
1305
|
+
sourceStory,
|
|
1306
|
+
content: sourceStory.content,
|
|
1307
|
+
targetParentId: parentId,
|
|
1308
|
+
maps,
|
|
1309
|
+
schemas,
|
|
1310
|
+
});
|
|
1311
|
+
const publishedLayerRecord = publishedLayerRecordBySourceId.get(String(sourceStory.id));
|
|
1312
|
+
if (shouldUsePublishedLayerCopy(publication, sourceStory, publishedLayerRecord)) {
|
|
1313
|
+
const publishedLayerStory = publishedLayerRecord?.publishedLayerItem?.story;
|
|
1314
|
+
const publishedLayer = buildRewrittenStoryPayload({
|
|
1315
|
+
sourceStory: publishedLayerStory,
|
|
1316
|
+
content: publishedLayerStory?.content,
|
|
1317
|
+
targetParentId: parentId,
|
|
1318
|
+
maps,
|
|
1319
|
+
schemas,
|
|
1320
|
+
});
|
|
1321
|
+
const publishedUpdateResult = await managementApi.stories.updateStory(publishedLayer.payload, String(id), {
|
|
1322
|
+
force_update: true,
|
|
1323
|
+
publish: false,
|
|
1324
|
+
}, {
|
|
1325
|
+
...apiConfig,
|
|
1326
|
+
spaceId: targetSpace,
|
|
1327
|
+
});
|
|
1328
|
+
if (!publishedUpdateResult?.ok) {
|
|
83
1329
|
return {
|
|
84
|
-
|
|
85
|
-
|
|
1330
|
+
result: publishedUpdateResult,
|
|
1331
|
+
rewrittenReferences: current.rewrittenReferences +
|
|
1332
|
+
publishedLayer.rewrittenReferences,
|
|
86
1333
|
};
|
|
87
1334
|
}
|
|
1335
|
+
const publishResult = await publishCopiedStory({
|
|
1336
|
+
storyId: id,
|
|
1337
|
+
story: publishedLayer.payload,
|
|
1338
|
+
publication,
|
|
1339
|
+
targetSpace,
|
|
1340
|
+
});
|
|
1341
|
+
if (!publishResult?.ok) {
|
|
1342
|
+
return {
|
|
1343
|
+
result: publishResult,
|
|
1344
|
+
rewrittenReferences: current.rewrittenReferences +
|
|
1345
|
+
publishedLayer.rewrittenReferences,
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
const restoreDraftResult = await managementApi.stories.updateStory(current.payload, String(id), {
|
|
1349
|
+
force_update: true,
|
|
1350
|
+
publish: false,
|
|
1351
|
+
}, {
|
|
1352
|
+
...apiConfig,
|
|
1353
|
+
spaceId: targetSpace,
|
|
1354
|
+
});
|
|
88
1355
|
return {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const treeOfStories = createTree(normalizedStories);
|
|
93
|
-
const entryStory = await attachToFolder(where, targetSpace);
|
|
94
|
-
const reAttachedTreeOfStories = treeOfStories[0]?.children?.map((child) => {
|
|
95
|
-
return {
|
|
96
|
-
...child,
|
|
97
|
-
parent_id: entryStory.story.id,
|
|
98
|
-
story: {
|
|
99
|
-
...child.story,
|
|
100
|
-
parent_id: entryStory.story.id,
|
|
101
|
-
},
|
|
1356
|
+
result: restoreDraftResult,
|
|
1357
|
+
rewrittenReferences: current.rewrittenReferences +
|
|
1358
|
+
publishedLayer.rewrittenReferences,
|
|
102
1359
|
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
1360
|
+
}
|
|
1361
|
+
if (publication.mode === "preserve-layers" &&
|
|
1362
|
+
resolveStoryLayerState(sourceStory) === "dirty-published") {
|
|
1363
|
+
Logger.warning(`Skipping publish for copied story '${sourceStory.full_slug ?? sourceStory.slug}' because source story has unpublished changes and no published layer could be resolved.`);
|
|
1364
|
+
}
|
|
1365
|
+
const result = await managementApi.stories.updateStory(current.payload, String(id), {
|
|
1366
|
+
force_update: true,
|
|
1367
|
+
publish: false,
|
|
108
1368
|
}, {
|
|
109
1369
|
...apiConfig,
|
|
110
1370
|
spaceId: targetSpace,
|
|
111
1371
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const allSourceStories = [rootStory].concat(await managementApi.stories.getAllStories({
|
|
122
|
-
options: {
|
|
123
|
-
starts_with: `${what}/`,
|
|
124
|
-
},
|
|
125
|
-
}, {
|
|
126
|
-
...apiConfig,
|
|
127
|
-
spaceId: sourceSpace,
|
|
128
|
-
}));
|
|
129
|
-
const normalizedStories = allSourceStories
|
|
130
|
-
.map((item) => item.story)
|
|
131
|
-
.map((item) => {
|
|
132
|
-
if (item.full_slug === what) {
|
|
1372
|
+
if (result?.ok &&
|
|
1373
|
+
shouldPublishCopiedCurrentStory(publication, sourceStory)) {
|
|
1374
|
+
const publishResult = await publishCopiedStory({
|
|
1375
|
+
storyId: id,
|
|
1376
|
+
story: current.payload,
|
|
1377
|
+
publication,
|
|
1378
|
+
targetSpace,
|
|
1379
|
+
});
|
|
1380
|
+
if (!publishResult?.ok) {
|
|
133
1381
|
return {
|
|
134
|
-
|
|
135
|
-
|
|
1382
|
+
result: publishResult,
|
|
1383
|
+
rewrittenReferences: current.rewrittenReferences,
|
|
136
1384
|
};
|
|
137
1385
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const treeOfStories = createTree(normalizedStories);
|
|
143
|
-
const entryStory = await attachToFolder(where, targetSpace);
|
|
144
|
-
const enhancedTreeOfStories = {
|
|
145
|
-
...treeOfStories[0],
|
|
146
|
-
parent_id: entryStory.story.id,
|
|
1386
|
+
}
|
|
1387
|
+
return {
|
|
1388
|
+
result,
|
|
1389
|
+
rewrittenReferences: current.rewrittenReferences,
|
|
147
1390
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
1391
|
+
};
|
|
1392
|
+
let update = await writeStory(Number(targetStoryId));
|
|
1393
|
+
if (isStoryUpdateNotFound(update.result)) {
|
|
1394
|
+
Logger.warning(`Ignoring stale story manifest mapping for '${sourceStory.full_slug ?? sourceStory.slug}' because target story '${targetStoryId}' could not be updated in space '${targetSpace}'.`);
|
|
1395
|
+
maps.storyIds.delete(Number(sourceStory.id));
|
|
1396
|
+
maps.storyUuids.delete(String(sourceStory.uuid));
|
|
1397
|
+
targetStoryId = await createOrMatchReplacementShell({
|
|
1398
|
+
node,
|
|
1399
|
+
sourceStory,
|
|
1400
|
+
parentId,
|
|
1401
|
+
staleTargetId: Number(targetStoryId),
|
|
1402
|
+
});
|
|
1403
|
+
update = await writeStory(Number(targetStoryId));
|
|
1404
|
+
}
|
|
1405
|
+
assertStoryUpdateSucceeded({
|
|
1406
|
+
result: update.result,
|
|
1407
|
+
sourceStory,
|
|
1408
|
+
targetStoryId: Number(targetStoryId),
|
|
1409
|
+
targetSpace,
|
|
1410
|
+
});
|
|
1411
|
+
updatedStories += 1;
|
|
1412
|
+
rewrittenReferences += update.rewrittenReferences;
|
|
1413
|
+
await walk(node.children ?? [], Number(targetStoryId));
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
await walk(tree, realParentId);
|
|
1417
|
+
await dedupeManifestFile(manifestPaths.stories);
|
|
1418
|
+
await dedupeManifestFile(manifestPaths.combined);
|
|
1419
|
+
if (updatedStories > 0) {
|
|
1420
|
+
Logger.success(`Updated ${updatedStories} copied story/story shell(s); rewrote ${rewrittenReferences} reference(s).`);
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
const createStoriesAndWriteManifests = async ({ tree, realParentId, sourceStoryById, targetSlugBySourceSlug, sourceSpace, targetSpace, manifestRoot, }) => {
|
|
1424
|
+
const manifestPaths = getDefaultCopyManifestPaths({
|
|
1425
|
+
sourceSpaceId: sourceSpace,
|
|
1426
|
+
targetSpaceId: targetSpace,
|
|
1427
|
+
rootDir: manifestRoot,
|
|
1428
|
+
});
|
|
1429
|
+
const existingManifestEntries = await loadManifest(manifestPaths.combined);
|
|
1430
|
+
const copyMaps = buildCopyMaps(existingManifestEntries);
|
|
1431
|
+
let storiesCreated = 0;
|
|
1432
|
+
let storiesMatched = 0;
|
|
1433
|
+
const walk = async (nodes, parentId) => {
|
|
1434
|
+
for (const node of nodes) {
|
|
1435
|
+
const sourceStory = sourceStoryById.get(Number(node.id ?? node.story.id));
|
|
1436
|
+
const sourceFullSlug = String(sourceStory?.full_slug ?? node.story.full_slug ?? "");
|
|
1437
|
+
const targetFullSlug = targetSlugBySourceSlug.get(sourceFullSlug);
|
|
1438
|
+
if (!sourceStory?.uuid) {
|
|
1439
|
+
throw new Error(`Cannot write story manifest for '${sourceFullSlug}' because source uuid is missing.`);
|
|
1440
|
+
}
|
|
1441
|
+
const mappedTargetId = copyMaps.storyIds.get(Number(sourceStory.id));
|
|
1442
|
+
if (mappedTargetId) {
|
|
1443
|
+
const mappedTargetStory = await getValidMappedTargetStory({
|
|
1444
|
+
sourceStory,
|
|
1445
|
+
targetStoryId: mappedTargetId,
|
|
1446
|
+
targetFullSlug,
|
|
1447
|
+
targetSpace,
|
|
1448
|
+
});
|
|
1449
|
+
if (mappedTargetStory) {
|
|
1450
|
+
storiesMatched += 1;
|
|
1451
|
+
await walk(node.children ?? [], mappedTargetId);
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
copyMaps.storyIds.delete(Number(sourceStory.id));
|
|
1455
|
+
copyMaps.storyUuids.delete(String(sourceStory.uuid));
|
|
1456
|
+
}
|
|
1457
|
+
const existingTargetStory = targetFullSlug
|
|
1458
|
+
? await managementApi.stories.getStoryBySlug(targetFullSlug, {
|
|
153
1459
|
...apiConfig,
|
|
154
1460
|
spaceId: targetSpace,
|
|
1461
|
+
})
|
|
1462
|
+
: undefined;
|
|
1463
|
+
const createdAt = new Date().toISOString();
|
|
1464
|
+
if (existingTargetStory?.story) {
|
|
1465
|
+
const targetStory = existingTargetStory.story;
|
|
1466
|
+
const entry = {
|
|
1467
|
+
type: "story",
|
|
1468
|
+
source_space_id: sourceSpace,
|
|
1469
|
+
target_space_id: targetSpace,
|
|
1470
|
+
source_id: Number(sourceStory.id),
|
|
1471
|
+
target_id: Number(targetStory.id),
|
|
1472
|
+
source_uuid: String(sourceStory.uuid),
|
|
1473
|
+
target_uuid: String(targetStory.uuid),
|
|
1474
|
+
source_full_slug: sourceFullSlug,
|
|
1475
|
+
target_full_slug: targetStory.full_slug ?? targetFullSlug,
|
|
1476
|
+
action: "matched_by_target_key",
|
|
1477
|
+
created_at: createdAt,
|
|
1478
|
+
};
|
|
1479
|
+
await appendCopyManifestEntry({
|
|
1480
|
+
combinedPath: manifestPaths.combined,
|
|
1481
|
+
resourcePath: manifestPaths.stories,
|
|
1482
|
+
entry,
|
|
155
1483
|
});
|
|
1484
|
+
copyMaps.storyIds.set(entry.source_id, entry.target_id);
|
|
1485
|
+
copyMaps.storyUuids.set(entry.source_uuid, entry.target_uuid);
|
|
1486
|
+
storiesMatched += 1;
|
|
1487
|
+
await walk(node.children ?? [], entry.target_id);
|
|
1488
|
+
continue;
|
|
156
1489
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
1490
|
+
const createdStoryResult = await managementApi.stories.createStory(buildStoryShellPayload(node.story, parentId), {
|
|
1491
|
+
...apiConfig,
|
|
1492
|
+
spaceId: targetSpace,
|
|
1493
|
+
}, {
|
|
1494
|
+
publish: false,
|
|
1495
|
+
});
|
|
1496
|
+
const targetStory = createdStoryResult?.story;
|
|
1497
|
+
if (!targetStory?.id || !targetStory?.uuid) {
|
|
1498
|
+
throw new Error(`Failed to create target story for '${sourceFullSlug}'.`);
|
|
1499
|
+
}
|
|
1500
|
+
const entry = {
|
|
1501
|
+
type: "story",
|
|
1502
|
+
source_space_id: sourceSpace,
|
|
1503
|
+
target_space_id: targetSpace,
|
|
1504
|
+
source_id: Number(sourceStory.id),
|
|
1505
|
+
target_id: Number(targetStory.id),
|
|
1506
|
+
source_uuid: String(sourceStory.uuid),
|
|
1507
|
+
target_uuid: String(targetStory.uuid),
|
|
1508
|
+
source_full_slug: sourceFullSlug,
|
|
1509
|
+
target_full_slug: targetStory.full_slug ?? targetFullSlug,
|
|
1510
|
+
action: "created",
|
|
1511
|
+
created_at: createdAt,
|
|
1512
|
+
};
|
|
1513
|
+
await appendCopyManifestEntry({
|
|
1514
|
+
combinedPath: manifestPaths.combined,
|
|
1515
|
+
resourcePath: manifestPaths.stories,
|
|
1516
|
+
entry,
|
|
1517
|
+
});
|
|
1518
|
+
copyMaps.storyIds.set(entry.source_id, entry.target_id);
|
|
1519
|
+
copyMaps.storyUuids.set(entry.source_uuid, entry.target_uuid);
|
|
1520
|
+
storiesCreated += 1;
|
|
1521
|
+
await walk(node.children ?? [], entry.target_id);
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
await walk(tree, realParentId);
|
|
1525
|
+
await dedupeManifestFile(manifestPaths.stories);
|
|
1526
|
+
await dedupeManifestFile(manifestPaths.combined);
|
|
1527
|
+
Logger.success(`Story manifest written to ${manifestPaths.stories}`);
|
|
1528
|
+
const plannedCounts = countTreeStories(tree);
|
|
1529
|
+
return {
|
|
1530
|
+
storyFoldersPlanned: plannedCounts.folders,
|
|
1531
|
+
storiesPlanned: plannedCounts.stories,
|
|
1532
|
+
storiesCreated,
|
|
1533
|
+
storiesMatched,
|
|
1534
|
+
};
|
|
1535
|
+
};
|
|
1536
|
+
const copyAssetsAndWriteManifests = async ({ sourceSpace, targetSpace, selection, input, graph, sourceAssets, sourceAssetFolders, outputPath, manifestRoot, }) => {
|
|
1537
|
+
const manifestPaths = getDefaultCopyManifestPaths({
|
|
1538
|
+
sourceSpaceId: sourceSpace,
|
|
1539
|
+
targetSpaceId: targetSpace,
|
|
1540
|
+
rootDir: manifestRoot,
|
|
1541
|
+
});
|
|
1542
|
+
const existingManifestEntries = await loadManifest(manifestPaths.combined);
|
|
1543
|
+
const copyMaps = buildCopyMaps(existingManifestEntries);
|
|
1544
|
+
const targetAssetFoldersResult = await managementApi.assets.getAllAssetFolders({ spaceId: targetSpace }, {
|
|
1545
|
+
...apiConfig,
|
|
1546
|
+
spaceId: targetSpace,
|
|
1547
|
+
});
|
|
1548
|
+
const targetAssetsResult = await managementApi.assets.getAllAssets({ spaceId: targetSpace }, {
|
|
1549
|
+
...apiConfig,
|
|
1550
|
+
spaceId: targetSpace,
|
|
1551
|
+
});
|
|
1552
|
+
const targetAssetFolders = Array.isArray(targetAssetFoldersResult?.asset_folders)
|
|
1553
|
+
? targetAssetFoldersResult.asset_folders
|
|
1554
|
+
: [];
|
|
1555
|
+
const targetAssets = Array.isArray(targetAssetsResult?.assets)
|
|
1556
|
+
? targetAssetsResult.assets
|
|
1557
|
+
: [];
|
|
1558
|
+
const targetFolderByPath = buildAssetFolderPathMap(targetAssetFolders);
|
|
1559
|
+
const sourceFolderById = new Map(sourceAssetFolders.map((folder) => [Number(folder.id), folder]));
|
|
1560
|
+
const graphFolderBySourceId = new Map(graph.assetFolders.map((folder) => [folder.sourceId, folder]));
|
|
1561
|
+
const graphAssetBySourceId = new Map(graph.assets.map((asset) => [asset.sourceId, asset]));
|
|
1562
|
+
let assetFoldersCreated = 0;
|
|
1563
|
+
let assetFoldersMatched = 0;
|
|
1564
|
+
let assetsCreated = 0;
|
|
1565
|
+
let assetsMatched = 0;
|
|
1566
|
+
Logger.warning(`Copying assets from space '${sourceSpace}' to space '${targetSpace}'.`);
|
|
1567
|
+
for (const folderNode of graph.assetFolders) {
|
|
1568
|
+
const sourceFolder = sourceFolderById.get(folderNode.sourceId);
|
|
1569
|
+
if (!sourceFolder) {
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
const sourceParentId = normalizeAssetFolderParentId(sourceFolder.parent_id);
|
|
1573
|
+
const mappedTargetFolderId = copyMaps.assetFolderIds.get(folderNode.sourceId);
|
|
1574
|
+
if (mappedTargetFolderId) {
|
|
1575
|
+
folderNode.targetParentId =
|
|
1576
|
+
sourceParentId === null
|
|
1577
|
+
? null
|
|
1578
|
+
: (copyMaps.assetFolderIds.get(sourceParentId) ?? null);
|
|
1579
|
+
folderNode.action = "match";
|
|
1580
|
+
assetFoldersMatched += 1;
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
const existingTargetFolder = folderNode.sourcePath
|
|
1584
|
+
? targetFolderByPath.get(folderNode.sourcePath)
|
|
1585
|
+
: undefined;
|
|
1586
|
+
const createdAt = new Date().toISOString();
|
|
1587
|
+
if (existingTargetFolder) {
|
|
1588
|
+
const entry = {
|
|
1589
|
+
type: "asset_folder",
|
|
1590
|
+
source_space_id: sourceSpace,
|
|
1591
|
+
target_space_id: targetSpace,
|
|
1592
|
+
source_id: folderNode.sourceId,
|
|
1593
|
+
target_id: Number(existingTargetFolder.id),
|
|
1594
|
+
source_name: String(sourceFolder.name ?? ""),
|
|
1595
|
+
target_name: String(existingTargetFolder.name ?? ""),
|
|
1596
|
+
source_parent_id: sourceParentId,
|
|
1597
|
+
target_parent_id: normalizeAssetFolderParentId(existingTargetFolder.parent_id),
|
|
1598
|
+
source_path: folderNode.sourcePath,
|
|
1599
|
+
target_path: folderNode.sourcePath,
|
|
1600
|
+
action: "matched_by_target_key",
|
|
1601
|
+
created_at: createdAt,
|
|
1602
|
+
};
|
|
1603
|
+
await appendCopyManifestEntry({
|
|
1604
|
+
combinedPath: manifestPaths.combined,
|
|
1605
|
+
resourcePath: manifestPaths.assetFolders,
|
|
1606
|
+
entry,
|
|
1607
|
+
});
|
|
1608
|
+
copyMaps.assetFolderIds.set(entry.source_id, entry.target_id);
|
|
1609
|
+
folderNode.targetParentId = entry.target_parent_id;
|
|
1610
|
+
folderNode.action = "match";
|
|
1611
|
+
assetFoldersMatched += 1;
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1614
|
+
const targetParentId = sourceParentId === null
|
|
1615
|
+
? null
|
|
1616
|
+
: (copyMaps.assetFolderIds.get(sourceParentId) ?? null);
|
|
1617
|
+
const createdFolder = await managementApi.assets.createAssetFolder({
|
|
1618
|
+
spaceId: targetSpace,
|
|
1619
|
+
payload: {
|
|
1620
|
+
name: String(sourceFolder.name),
|
|
1621
|
+
parent_id: targetParentId,
|
|
1622
|
+
},
|
|
1623
|
+
}, {
|
|
1624
|
+
...apiConfig,
|
|
1625
|
+
spaceId: targetSpace,
|
|
1626
|
+
});
|
|
1627
|
+
const targetFolder = createdFolder.asset_folder;
|
|
1628
|
+
const entry = {
|
|
1629
|
+
type: "asset_folder",
|
|
1630
|
+
source_space_id: sourceSpace,
|
|
1631
|
+
target_space_id: targetSpace,
|
|
1632
|
+
source_id: folderNode.sourceId,
|
|
1633
|
+
target_id: Number(targetFolder.id),
|
|
1634
|
+
source_name: String(sourceFolder.name ?? ""),
|
|
1635
|
+
target_name: String(targetFolder.name ?? ""),
|
|
1636
|
+
source_parent_id: sourceParentId,
|
|
1637
|
+
target_parent_id: normalizeAssetFolderParentId(targetFolder.parent_id),
|
|
1638
|
+
source_path: folderNode.sourcePath,
|
|
1639
|
+
target_path: folderNode.sourcePath,
|
|
1640
|
+
action: "created",
|
|
1641
|
+
created_at: createdAt,
|
|
1642
|
+
};
|
|
1643
|
+
await appendCopyManifestEntry({
|
|
1644
|
+
combinedPath: manifestPaths.combined,
|
|
1645
|
+
resourcePath: manifestPaths.assetFolders,
|
|
1646
|
+
entry,
|
|
1647
|
+
});
|
|
1648
|
+
copyMaps.assetFolderIds.set(entry.source_id, entry.target_id);
|
|
1649
|
+
folderNode.targetParentId = entry.target_parent_id;
|
|
1650
|
+
folderNode.action = "create";
|
|
1651
|
+
assetFoldersCreated += 1;
|
|
1652
|
+
}
|
|
1653
|
+
for (const asset of sourceAssets) {
|
|
1654
|
+
const graphAsset = graphAssetBySourceId.get(Number(asset.id));
|
|
1655
|
+
if (!graphAsset) {
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
const mappedTargetAsset = copyMaps.assetIds.get(Number(asset.id));
|
|
1659
|
+
if (mappedTargetAsset) {
|
|
1660
|
+
graphAsset.targetFilename = mappedTargetAsset.filename;
|
|
1661
|
+
graphAsset.targetAssetFolderId =
|
|
1662
|
+
asset.asset_folder_id === null ||
|
|
1663
|
+
asset.asset_folder_id === undefined
|
|
1664
|
+
? null
|
|
1665
|
+
: (copyMaps.assetFolderIds.get(Number(asset.asset_folder_id)) ?? null);
|
|
1666
|
+
graphAsset.action = "match";
|
|
1667
|
+
assetsMatched += 1;
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
const fileName = getFileName(asset.filename);
|
|
1671
|
+
const existingTargetAsset = findUniqueTargetAssetByFileName(targetAssets, fileName);
|
|
1672
|
+
const targetAssetFolderId = asset.asset_folder_id === null ||
|
|
1673
|
+
asset.asset_folder_id === undefined
|
|
1674
|
+
? null
|
|
1675
|
+
: (copyMaps.assetFolderIds.get(Number(asset.asset_folder_id)) ??
|
|
1676
|
+
null);
|
|
1677
|
+
const createdAt = new Date().toISOString();
|
|
1678
|
+
if (existingTargetAsset) {
|
|
1679
|
+
const entry = {
|
|
1680
|
+
type: "asset",
|
|
1681
|
+
source_space_id: sourceSpace,
|
|
1682
|
+
target_space_id: targetSpace,
|
|
1683
|
+
source_id: Number(asset.id),
|
|
1684
|
+
target_id: Number(existingTargetAsset.id),
|
|
1685
|
+
source_filename: asset.filename,
|
|
1686
|
+
target_filename: existingTargetAsset.filename,
|
|
1687
|
+
source_asset_folder_id: asset.asset_folder_id ?? null,
|
|
1688
|
+
target_asset_folder_id: existingTargetAsset.asset_folder_id ?? null,
|
|
1689
|
+
action: "matched_by_target_key",
|
|
1690
|
+
created_at: createdAt,
|
|
1691
|
+
};
|
|
1692
|
+
await appendCopyManifestEntry({
|
|
1693
|
+
combinedPath: manifestPaths.combined,
|
|
1694
|
+
resourcePath: manifestPaths.assets,
|
|
1695
|
+
entry,
|
|
1696
|
+
});
|
|
1697
|
+
copyMaps.assetIds.set(entry.source_id, {
|
|
1698
|
+
id: entry.target_id,
|
|
1699
|
+
filename: entry.target_filename,
|
|
1700
|
+
});
|
|
1701
|
+
copyMaps.assetFilenames.set(entry.source_filename, entry.target_filename);
|
|
1702
|
+
graphAsset.targetFilename = entry.target_filename;
|
|
1703
|
+
graphAsset.targetAssetFolderId = entry.target_asset_folder_id;
|
|
1704
|
+
graphAsset.action = "match";
|
|
1705
|
+
assetsMatched += 1;
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
const pathToFile = await managementApi.assets.downloadAsset({ payload: asset }, apiConfig);
|
|
1709
|
+
const targetAsset = await managementApi.assets.createAssetAndFinalize({
|
|
1710
|
+
spaceId: targetSpace,
|
|
1711
|
+
pathToFile,
|
|
1712
|
+
payload: {
|
|
1713
|
+
filename: asset.filename,
|
|
1714
|
+
asset_folder_id: targetAssetFolderId,
|
|
1715
|
+
},
|
|
1716
|
+
}, {
|
|
1717
|
+
...apiConfig,
|
|
1718
|
+
spaceId: targetSpace,
|
|
1719
|
+
});
|
|
1720
|
+
if (Object.keys(getAssetMetadataPayload(asset)).length > 0) {
|
|
1721
|
+
await managementApi.assets.updateAsset({
|
|
1722
|
+
spaceId: targetSpace,
|
|
1723
|
+
assetId: Number(targetAsset.id),
|
|
1724
|
+
payload: getAssetMetadataPayload(asset),
|
|
1725
|
+
}, {
|
|
1726
|
+
...apiConfig,
|
|
1727
|
+
spaceId: targetSpace,
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
const entry = {
|
|
1731
|
+
type: "asset",
|
|
1732
|
+
source_space_id: sourceSpace,
|
|
1733
|
+
target_space_id: targetSpace,
|
|
1734
|
+
source_id: Number(asset.id),
|
|
1735
|
+
target_id: Number(targetAsset.id),
|
|
1736
|
+
source_filename: asset.filename,
|
|
1737
|
+
target_filename: targetAsset.filename,
|
|
1738
|
+
source_asset_folder_id: asset.asset_folder_id ?? null,
|
|
1739
|
+
target_asset_folder_id: targetAssetFolderId,
|
|
1740
|
+
action: "created",
|
|
1741
|
+
created_at: createdAt,
|
|
1742
|
+
};
|
|
1743
|
+
await appendCopyManifestEntry({
|
|
1744
|
+
combinedPath: manifestPaths.combined,
|
|
1745
|
+
resourcePath: manifestPaths.assets,
|
|
1746
|
+
entry,
|
|
1747
|
+
});
|
|
1748
|
+
copyMaps.assetIds.set(entry.source_id, {
|
|
1749
|
+
id: entry.target_id,
|
|
1750
|
+
filename: entry.target_filename,
|
|
1751
|
+
});
|
|
1752
|
+
copyMaps.assetFilenames.set(entry.source_filename, entry.target_filename);
|
|
1753
|
+
graphAsset.targetFilename = entry.target_filename;
|
|
1754
|
+
graphAsset.targetAssetFolderId = entry.target_asset_folder_id;
|
|
1755
|
+
graphAsset.action = "create";
|
|
1756
|
+
assetsCreated += 1;
|
|
1757
|
+
}
|
|
1758
|
+
await dedupeManifestFile(manifestPaths.assetFolders);
|
|
1759
|
+
await dedupeManifestFile(manifestPaths.assets);
|
|
1760
|
+
await dedupeManifestFile(manifestPaths.combined);
|
|
1761
|
+
graph.limitations = [];
|
|
1762
|
+
const report = buildCopyAssetsApplyReport({
|
|
1763
|
+
sourceSpace,
|
|
1764
|
+
targetSpace,
|
|
1765
|
+
selection,
|
|
1766
|
+
input,
|
|
1767
|
+
graph,
|
|
1768
|
+
manifestPaths,
|
|
1769
|
+
assetFoldersCreated,
|
|
1770
|
+
assetFoldersMatched,
|
|
1771
|
+
assetsCreated,
|
|
1772
|
+
assetsMatched,
|
|
1773
|
+
});
|
|
1774
|
+
if (outputPath) {
|
|
1775
|
+
await writeJsonReport(outputPath, report);
|
|
1776
|
+
}
|
|
1777
|
+
Logger.success(`Asset copy complete. Created ${assetsCreated} asset(s), matched ${assetsMatched} asset(s).`);
|
|
1778
|
+
Logger.success(`Asset manifest written to ${manifestPaths.assets}`);
|
|
1779
|
+
Logger.success(`Asset folder manifest written to ${manifestPaths.assetFolders}`);
|
|
1780
|
+
return report;
|
|
1781
|
+
};
|
|
1782
|
+
const logDryRunCopyPlan = async ({ report }) => {
|
|
1783
|
+
Logger.warning("[dry-run] Copy stories preview only. No Storyblok writes will be made.");
|
|
1784
|
+
Logger.warning(`[dry-run] Source space: ${report.normalized.sourceSpaceId}`);
|
|
1785
|
+
Logger.warning(`[dry-run] Target space: ${report.normalized.targetSpaceId}`);
|
|
1786
|
+
Logger.warning(`[dry-run] Source: ${report.normalized.source}`);
|
|
1787
|
+
Logger.warning(`[dry-run] Destination: ${report.normalized.destination}`);
|
|
1788
|
+
Logger.warning(`[dry-run] Mode: ${report.normalized.mode}`);
|
|
1789
|
+
Logger.warning(`[dry-run] With assets: ${report.normalized.withAssets ? "yes" : "no"}`);
|
|
1790
|
+
if (report.normalized.mode === "children") {
|
|
1791
|
+
Logger.warning("[dry-run] Source folder root will not be created; only descendants are planned.");
|
|
1792
|
+
}
|
|
1793
|
+
if (report.normalized.mode === "self") {
|
|
1794
|
+
Logger.warning("[dry-run] Descendants will not be copied; only the selected story or folder shell is planned.");
|
|
1795
|
+
}
|
|
1796
|
+
Logger.warning(`[dry-run] Would create ${report.items.length} story/folder item(s):`);
|
|
1797
|
+
for (const item of report.items) {
|
|
1798
|
+
Logger.warning(`[dry-run] ${item.type.padEnd(6)} ${item.targetFullSlug || "<root>"}`);
|
|
1799
|
+
}
|
|
1800
|
+
if (report.graph) {
|
|
1801
|
+
Logger.warning(`[dry-run] Would plan ${report.summary.assetFolders} referenced asset folder(s) and ${report.summary.assets} referenced asset(s).`);
|
|
1802
|
+
Logger.warning(`[dry-run] Asset refs: ${report.summary.assetReferencesMapped} mapped, ${report.summary.assetReferencesPlanned} planned, ${report.summary.assetReferencesUnresolved} unresolved.`);
|
|
1803
|
+
if (report.assetReferenceSummary) {
|
|
1804
|
+
Logger.warning(`[dry-run] Unique asset refs: ${report.assetReferenceSummary.mapped.uniqueAssets} mapped, ${report.assetReferenceSummary.planned.uniqueAssets} planned, ${report.assetReferenceSummary.unresolved.uniqueAssets} unresolved.`);
|
|
1805
|
+
for (const foreignSpace of report.assetReferenceSummary
|
|
1806
|
+
.foreignAssetSpaces) {
|
|
1807
|
+
Logger.warning(`[dry-run] Foreign asset space ${foreignSpace.spaceId}: ${foreignSpace.occurrences} occurrence(s), ${foreignSpace.uniqueAssets} unique asset(s).`);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
Logger.warning(`[dry-run] Story refs: ${report.summary.storyReferencesMapped} mapped, ${report.summary.storyReferencesPreserved} preserved, ${report.summary.storyReferencesUnresolved} unresolved.`);
|
|
1811
|
+
for (const folder of report.graph.assetFolders) {
|
|
1812
|
+
Logger.warning(`[dry-run] asset_folder ${folder.action.padEnd(6)} ${folder.targetPath ?? `#${folder.sourceId}`}`);
|
|
1813
|
+
}
|
|
1814
|
+
for (const asset of report.graph.assets) {
|
|
1815
|
+
Logger.warning(`[dry-run] asset ${asset.action.padEnd(6)} ${asset.targetFilename}`);
|
|
1816
|
+
}
|
|
1817
|
+
for (const reference of report.graph.assetReferences) {
|
|
1818
|
+
Logger.warning(`[dry-run] asset_ref ${reference.status.padEnd(10)} ${reference.filename ?? reference.assetId ?? "<unknown>"} at ${reference.sourceStoryFullSlug ?? reference.path}`);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
report.warnings.forEach((warning) => Logger.warning(`[dry-run] ${warning.message}`));
|
|
1822
|
+
};
|
|
1823
|
+
const logDryRunCopyAssetsPlan = async ({ report, }) => {
|
|
1824
|
+
const selectionLabel = report.normalized.selection === "all"
|
|
1825
|
+
? "all assets and asset folders"
|
|
1826
|
+
: report.normalized.selection.type === "referenced_by_stories"
|
|
1827
|
+
? `referenced_by_stories ${report.normalized.selection.source} (${report.normalized.selection.mode})`
|
|
1828
|
+
: `${report.normalized.selection.type} ${report.normalized.selection.values.join(", ")}`;
|
|
1829
|
+
Logger.warning("[dry-run] Copy assets preview only. No Storyblok writes will be made.");
|
|
1830
|
+
Logger.warning(`[dry-run] Source space: ${report.normalized.sourceSpaceId}`);
|
|
1831
|
+
Logger.warning(`[dry-run] Target space: ${report.normalized.targetSpaceId}`);
|
|
1832
|
+
Logger.warning(`[dry-run] Selection: ${selectionLabel}`);
|
|
1833
|
+
Logger.warning(`[dry-run] Would plan ${report.summary.assetFolders} asset folder(s) and ${report.summary.assets} asset(s).`);
|
|
1834
|
+
for (const folder of report.graph.assetFolders) {
|
|
1835
|
+
Logger.warning(`[dry-run] asset_folder ${folder.targetPath ?? `#${folder.sourceId}`}`);
|
|
1836
|
+
}
|
|
1837
|
+
for (const asset of report.graph.assets) {
|
|
1838
|
+
Logger.warning(`[dry-run] asset ${asset.targetFilename}`);
|
|
1839
|
+
}
|
|
1840
|
+
report.graph.warnings.forEach((warning) => Logger.warning(`[dry-run] ${warning.message}`));
|
|
1841
|
+
report.limitations.forEach((limitation) => Logger.warning(`[dry-run] limitation: ${limitation}`));
|
|
1842
|
+
};
|
|
1843
|
+
export const copyCommand = async (props) => {
|
|
1844
|
+
const { input, flags } = props;
|
|
1845
|
+
const command = input[1];
|
|
1846
|
+
switch (command) {
|
|
1847
|
+
case COPY_COMMANDS.stories: {
|
|
1848
|
+
const sourceSpace = getCopySpace(flags, ["from", "sourceSpace"], apiConfig.spaceId);
|
|
1849
|
+
const targetSpace = getCopySpace(flags, ["to", "targetSpace"], apiConfig.spaceId);
|
|
1850
|
+
const selection = resolveCopySelection(flags);
|
|
1851
|
+
const dryRun = Boolean(flags["dryRun"]);
|
|
1852
|
+
const outputPath = readStringFlag(flags, ["outputPath"]);
|
|
1853
|
+
const manifestRoot = readStringFlag(flags, ["manifestRoot"]);
|
|
1854
|
+
const withAssets = Boolean(flags["withAssets"] ?? flags["with-assets"]);
|
|
1855
|
+
const publication = await resolveCopyPublicationOptions({
|
|
1856
|
+
flags,
|
|
1857
|
+
targetSpace,
|
|
1858
|
+
dryRun,
|
|
1859
|
+
});
|
|
1860
|
+
const destination = readStringFlag(flags, ["destination", "where"]);
|
|
1861
|
+
const destinationParentId = await resolveDestinationParentId(destination, targetSpace);
|
|
1862
|
+
Logger.warning(`Copying stories from space '${sourceSpace}' to space '${targetSpace}'.`);
|
|
1863
|
+
Logger.log(`Source '${selection.source}', mode '${selection.mode}', destination '${destination ?? "root"}'.`);
|
|
1864
|
+
const sourceStories = await getStoriesForSelection(selection, sourceSpace);
|
|
1865
|
+
const normalizedStories = normalizeStoriesForTree(sourceStories, selection);
|
|
1866
|
+
const tree = createTree(normalizedStories);
|
|
1867
|
+
const rootsToCreate = prepareTreeForCreate(selectTreeRoots(tree, selection));
|
|
1868
|
+
if (rootsToCreate.length === 0) {
|
|
1869
|
+
Logger.warning("No stories matched the copy selection.");
|
|
1870
|
+
break;
|
|
1871
|
+
}
|
|
1872
|
+
const plan = buildCopyPlan(rootsToCreate, destination);
|
|
1873
|
+
const manifestPaths = getDefaultCopyManifestPaths({
|
|
1874
|
+
sourceSpaceId: sourceSpace,
|
|
1875
|
+
targetSpaceId: targetSpace,
|
|
1876
|
+
rootDir: manifestRoot,
|
|
1877
|
+
});
|
|
1878
|
+
const manifestEntries = await loadManifest(manifestPaths.combined);
|
|
1879
|
+
const copyMaps = buildCopyMaps(manifestEntries);
|
|
1880
|
+
let dryRunGraph;
|
|
1881
|
+
let withAssetsGraph;
|
|
1882
|
+
let sourceAssets = [];
|
|
1883
|
+
let sourceAssetFolders = [];
|
|
1884
|
+
if (dryRun || withAssets) {
|
|
1885
|
+
const schemasPromise = buildComponentSchemaRegistry(sourceSpace);
|
|
1886
|
+
if (withAssets) {
|
|
1887
|
+
const [schemas, assetsResult, assetFoldersResult] = await Promise.all([
|
|
1888
|
+
schemasPromise,
|
|
1889
|
+
managementApi.assets.getAllAssets({ spaceId: sourceSpace }, {
|
|
1890
|
+
...apiConfig,
|
|
1891
|
+
spaceId: sourceSpace,
|
|
1892
|
+
}),
|
|
1893
|
+
managementApi.assets.getAllAssetFolders({ spaceId: sourceSpace }, {
|
|
1894
|
+
...apiConfig,
|
|
1895
|
+
spaceId: sourceSpace,
|
|
1896
|
+
}),
|
|
1897
|
+
]);
|
|
1898
|
+
sourceAssets = Array.isArray(assetsResult?.assets)
|
|
1899
|
+
? assetsResult.assets
|
|
1900
|
+
: [];
|
|
1901
|
+
sourceAssetFolders = Array.isArray(assetFoldersResult?.asset_folders)
|
|
1902
|
+
? assetFoldersResult.asset_folders
|
|
1903
|
+
: [];
|
|
1904
|
+
Logger.warning(`Planning referenced assets from ${countStoryItems(sourceStories)} stories against ${sourceAssets.length} source asset(s).`);
|
|
1905
|
+
withAssetsGraph = buildReferencedAssetsGraph({
|
|
1906
|
+
sourceSpace,
|
|
1907
|
+
targetSpace,
|
|
1908
|
+
selection,
|
|
1909
|
+
destination,
|
|
1910
|
+
plan,
|
|
1911
|
+
sourceStories,
|
|
1912
|
+
sourceAssets,
|
|
1913
|
+
sourceAssetFolders,
|
|
1914
|
+
schemas,
|
|
1915
|
+
copyMaps,
|
|
1916
|
+
onScanProgress: logReferenceScanProgress,
|
|
1917
|
+
});
|
|
1918
|
+
Logger.success(`Reference planning complete. Found ${withAssetsGraph.assets.length} referenced asset(s), ${withAssetsGraph.assetFolders.length} asset folder(s), ${withAssetsGraph.assetReferences.length} asset reference occurrence(s), and ${withAssetsGraph.storyReferences.length} story reference occurrence(s).`);
|
|
1919
|
+
dryRunGraph = withAssetsGraph;
|
|
1920
|
+
}
|
|
1921
|
+
else if (dryRun) {
|
|
1922
|
+
const schemas = await schemasPromise;
|
|
1923
|
+
Logger.warning(`Scanning ${countStoryItems(sourceStories)} stories for copy references.`);
|
|
1924
|
+
dryRunGraph = buildStoryReferenceDryRunGraph({
|
|
1925
|
+
sourceSpace,
|
|
1926
|
+
targetSpace,
|
|
1927
|
+
selection,
|
|
1928
|
+
destination,
|
|
1929
|
+
plan,
|
|
1930
|
+
sourceStories,
|
|
1931
|
+
schemas,
|
|
1932
|
+
copyMaps,
|
|
1933
|
+
onScanProgress: logReferenceScanProgress,
|
|
1934
|
+
});
|
|
1935
|
+
Logger.success(`Reference planning complete. Found ${dryRunGraph.assetReferences.length} asset reference occurrence(s) and ${dryRunGraph.storyReferences.length} story reference occurrence(s).`);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
if (dryRun) {
|
|
1939
|
+
const conflicts = await findTargetConflicts(plan, targetSpace);
|
|
1940
|
+
Logger.warning("Building dry-run copy report.");
|
|
1941
|
+
const report = buildCopyDryRunReport({
|
|
1942
|
+
sourceSpace,
|
|
1943
|
+
targetSpace,
|
|
1944
|
+
selection,
|
|
1945
|
+
destination,
|
|
1946
|
+
withAssets,
|
|
1947
|
+
input: { ...flags },
|
|
1948
|
+
plan,
|
|
1949
|
+
conflicts,
|
|
1950
|
+
graph: dryRunGraph,
|
|
1951
|
+
outputPath,
|
|
162
1952
|
});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
{
|
|
166
|
-
|
|
167
|
-
|
|
1953
|
+
await logDryRunCopyPlan({ report });
|
|
1954
|
+
if (outputPath) {
|
|
1955
|
+
Logger.warning(`Writing dry-run copy report to ${outputPath}.`);
|
|
1956
|
+
await writeDryRunReport(outputPath, report);
|
|
1957
|
+
}
|
|
1958
|
+
break;
|
|
1959
|
+
}
|
|
1960
|
+
let assetCopyReport;
|
|
1961
|
+
if (withAssetsGraph) {
|
|
1962
|
+
Logger.warning("Copying referenced assets before stories because --with-assets was passed.");
|
|
1963
|
+
assetCopyReport = await copyAssetsAndWriteManifests({
|
|
1964
|
+
sourceSpace,
|
|
1965
|
+
targetSpace,
|
|
1966
|
+
selection: {
|
|
1967
|
+
type: "asset",
|
|
1968
|
+
values: withAssetsGraph.assets.map((asset) => String(asset.sourceId)),
|
|
168
1969
|
},
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
parent_id: null,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
return {
|
|
178
|
-
...item,
|
|
179
|
-
};
|
|
1970
|
+
input: { ...flags },
|
|
1971
|
+
graph: withAssetsGraph,
|
|
1972
|
+
sourceAssets,
|
|
1973
|
+
sourceAssetFolders,
|
|
1974
|
+
manifestRoot,
|
|
180
1975
|
});
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
1976
|
+
}
|
|
1977
|
+
const publishedLayerRecordBySourceId = new Map();
|
|
1978
|
+
if (publication.mode === "preserve-layers") {
|
|
1979
|
+
const publishedLayerContext = await buildPublishedLayerContext({
|
|
1980
|
+
items: sourceStories,
|
|
1981
|
+
from: sourceSpace,
|
|
1982
|
+
}, apiConfig);
|
|
1983
|
+
for (const record of publishedLayerContext.records) {
|
|
1984
|
+
publishedLayerRecordBySourceId.set(String(record.storyId), record);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
const storySummary = await createStoriesAndWriteManifests({
|
|
1988
|
+
tree: rootsToCreate,
|
|
1989
|
+
realParentId: destinationParentId,
|
|
1990
|
+
sourceStoryById: buildSourceStoryById(sourceStories),
|
|
1991
|
+
targetSlugBySourceSlug: buildTargetSlugBySourceSlug(plan),
|
|
1992
|
+
sourceSpace,
|
|
1993
|
+
targetSpace,
|
|
1994
|
+
manifestRoot,
|
|
1995
|
+
});
|
|
1996
|
+
await rewriteCopiedStoryContents({
|
|
1997
|
+
tree: rootsToCreate,
|
|
1998
|
+
realParentId: destinationParentId,
|
|
1999
|
+
sourceStoryById: buildSourceStoryById(sourceStories),
|
|
2000
|
+
targetSlugBySourceSlug: buildTargetSlugBySourceSlug(plan),
|
|
2001
|
+
publication,
|
|
2002
|
+
publishedLayerRecordBySourceId,
|
|
2003
|
+
sourceSpace,
|
|
2004
|
+
targetSpace,
|
|
2005
|
+
manifestRoot,
|
|
2006
|
+
});
|
|
2007
|
+
if (outputPath) {
|
|
2008
|
+
const report = buildCopyStoriesApplyReport({
|
|
2009
|
+
sourceSpace,
|
|
2010
|
+
targetSpace,
|
|
2011
|
+
selection,
|
|
2012
|
+
destination,
|
|
2013
|
+
withAssets,
|
|
2014
|
+
input: { ...flags },
|
|
2015
|
+
plan,
|
|
2016
|
+
storySummary,
|
|
2017
|
+
graph: withAssetsGraph,
|
|
2018
|
+
assetCopyReport,
|
|
2019
|
+
manifestRoot,
|
|
2020
|
+
});
|
|
2021
|
+
await writeJsonReport(outputPath, report);
|
|
2022
|
+
}
|
|
2023
|
+
break;
|
|
2024
|
+
}
|
|
2025
|
+
case COPY_COMMANDS.assets: {
|
|
2026
|
+
const sourceSpace = getCopySpace(flags, ["from", "sourceSpace"], apiConfig.spaceId);
|
|
2027
|
+
const targetSpace = getCopySpace(flags, ["to", "targetSpace"], apiConfig.spaceId);
|
|
2028
|
+
const dryRun = Boolean(flags["dryRun"]);
|
|
2029
|
+
const outputPath = readStringFlag(flags, ["outputPath"]);
|
|
2030
|
+
const manifestRoot = readStringFlag(flags, ["manifestRoot"]);
|
|
2031
|
+
const selection = resolveCopyAssetsSelection(flags);
|
|
2032
|
+
Logger.warning(dryRun
|
|
2033
|
+
? `Planning asset copy from space '${sourceSpace}' to space '${targetSpace}'.`
|
|
2034
|
+
: `Preparing asset copy from space '${sourceSpace}' to space '${targetSpace}'.`);
|
|
2035
|
+
const [assetsResult, assetFoldersResult] = await Promise.all([
|
|
2036
|
+
managementApi.assets.getAllAssets({ spaceId: sourceSpace }, {
|
|
187
2037
|
...apiConfig,
|
|
188
|
-
spaceId:
|
|
2038
|
+
spaceId: sourceSpace,
|
|
2039
|
+
}),
|
|
2040
|
+
managementApi.assets.getAllAssetFolders({ spaceId: sourceSpace }, {
|
|
2041
|
+
...apiConfig,
|
|
2042
|
+
spaceId: sourceSpace,
|
|
2043
|
+
}),
|
|
2044
|
+
]);
|
|
2045
|
+
const sourceAssets = Array.isArray(assetsResult?.assets)
|
|
2046
|
+
? assetsResult.assets
|
|
2047
|
+
: [];
|
|
2048
|
+
const sourceAssetFolders = Array.isArray(assetFoldersResult?.asset_folders)
|
|
2049
|
+
? assetFoldersResult.asset_folders
|
|
2050
|
+
: [];
|
|
2051
|
+
let graph;
|
|
2052
|
+
let scopedSource;
|
|
2053
|
+
if (selection.type === "referenced_by_stories") {
|
|
2054
|
+
const storySelection = selection.storySelection;
|
|
2055
|
+
const [schemas, sourceStories] = await Promise.all([
|
|
2056
|
+
buildComponentSchemaRegistry(sourceSpace),
|
|
2057
|
+
getStoriesForSelection(storySelection, sourceSpace),
|
|
2058
|
+
]);
|
|
2059
|
+
const normalizedStories = normalizeStoriesForTree(sourceStories, storySelection);
|
|
2060
|
+
const tree = createTree(normalizedStories);
|
|
2061
|
+
const rootsToCreate = prepareTreeForCreate(selectTreeRoots(tree, storySelection));
|
|
2062
|
+
const plan = buildCopyPlan(rootsToCreate, undefined);
|
|
2063
|
+
const manifestPaths = getDefaultCopyManifestPaths({
|
|
2064
|
+
sourceSpaceId: sourceSpace,
|
|
2065
|
+
targetSpaceId: targetSpace,
|
|
2066
|
+
rootDir: manifestRoot,
|
|
2067
|
+
});
|
|
2068
|
+
const manifestEntries = await loadManifest(manifestPaths.combined);
|
|
2069
|
+
const copyMaps = buildCopyMaps(manifestEntries);
|
|
2070
|
+
Logger.warning(`Planning referenced assets from ${countStoryItems(sourceStories)} stories against ${sourceAssets.length} source asset(s).`);
|
|
2071
|
+
graph = buildReferencedAssetsGraph({
|
|
2072
|
+
sourceSpace,
|
|
2073
|
+
targetSpace,
|
|
2074
|
+
selection: storySelection,
|
|
2075
|
+
destination: undefined,
|
|
2076
|
+
plan,
|
|
2077
|
+
sourceStories,
|
|
2078
|
+
sourceAssets,
|
|
2079
|
+
sourceAssetFolders,
|
|
2080
|
+
schemas,
|
|
2081
|
+
copyMaps,
|
|
2082
|
+
onScanProgress: logReferenceScanProgress,
|
|
2083
|
+
});
|
|
2084
|
+
Logger.success(`Reference planning complete. Found ${graph.assets.length} referenced asset(s), ${graph.assetFolders.length} asset folder(s), ${graph.assetReferences.length} asset reference occurrence(s), and ${graph.storyReferences.length} story reference occurrence(s).`);
|
|
2085
|
+
graph.scope = {
|
|
2086
|
+
command: "copy assets",
|
|
2087
|
+
source: storySelection.source,
|
|
2088
|
+
mode: storySelection.mode,
|
|
2089
|
+
referencePolicy: "preserve",
|
|
2090
|
+
};
|
|
2091
|
+
scopedSource = selectSourceAssetsFromGraph({
|
|
2092
|
+
graph,
|
|
2093
|
+
sourceAssets,
|
|
2094
|
+
sourceAssetFolders,
|
|
189
2095
|
});
|
|
190
2096
|
}
|
|
2097
|
+
else {
|
|
2098
|
+
scopedSource = selectSourceAssetsForCopy({
|
|
2099
|
+
selection,
|
|
2100
|
+
sourceAssets,
|
|
2101
|
+
sourceAssetFolders,
|
|
2102
|
+
});
|
|
2103
|
+
graph = buildCopyAssetsGraph({
|
|
2104
|
+
sourceSpaceId: sourceSpace,
|
|
2105
|
+
targetSpaceId: targetSpace,
|
|
2106
|
+
assets: scopedSource.assets,
|
|
2107
|
+
assetFolders: scopedSource.assetFolders,
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
if (dryRun) {
|
|
2111
|
+
const report = buildCopyAssetsDryRunReport({
|
|
2112
|
+
sourceSpace,
|
|
2113
|
+
targetSpace,
|
|
2114
|
+
selection,
|
|
2115
|
+
input: { ...flags },
|
|
2116
|
+
outputPath,
|
|
2117
|
+
graph,
|
|
2118
|
+
});
|
|
2119
|
+
await logDryRunCopyAssetsPlan({ report });
|
|
2120
|
+
if (outputPath) {
|
|
2121
|
+
await writeDryRunReport(outputPath, report);
|
|
2122
|
+
}
|
|
2123
|
+
break;
|
|
2124
|
+
}
|
|
2125
|
+
await copyAssetsAndWriteManifests({
|
|
2126
|
+
sourceSpace,
|
|
2127
|
+
targetSpace,
|
|
2128
|
+
selection,
|
|
2129
|
+
input: { ...flags },
|
|
2130
|
+
graph,
|
|
2131
|
+
sourceAssets: scopedSource.assets,
|
|
2132
|
+
sourceAssetFolders: scopedSource.assetFolders,
|
|
2133
|
+
outputPath,
|
|
2134
|
+
manifestRoot,
|
|
2135
|
+
});
|
|
191
2136
|
break;
|
|
2137
|
+
}
|
|
192
2138
|
default:
|
|
193
|
-
|
|
2139
|
+
Logger.warning("Unsupported copy command. Use: sb-mig copy stories --from <sourceSpaceId> --to <targetSpaceId> --source <full_slug> --destination <target_folder>, or sb-mig copy assets --from <sourceSpaceId> --to <targetSpaceId> --all --dry-run");
|
|
194
2140
|
}
|
|
195
2141
|
};
|