sb-mig 5.8.0 → 6.0.0-beta.10

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.
Files changed (46) hide show
  1. package/README.md +36 -2
  2. package/dist/api/assets/assets.d.ts +3 -1
  3. package/dist/api/assets/assets.js +60 -18
  4. package/dist/api/assets/assets.types.d.ts +35 -2
  5. package/dist/api/assets/index.d.ts +2 -1
  6. package/dist/api/assets/index.js +1 -1
  7. package/dist/api/components/components.js +8 -2
  8. package/dist/api/components/components.sync.js +35 -8
  9. package/dist/api/data-migration/component-data-migration.d.ts +8 -3
  10. package/dist/api/data-migration/component-data-migration.js +52 -7
  11. package/dist/api/data-migration/migration-run-log.d.ts +77 -0
  12. package/dist/api/data-migration/migration-run-log.js +130 -0
  13. package/dist/api/data-migration/write-summary.d.ts +7 -0
  14. package/dist/api/managementApi.d.ts +15 -0
  15. package/dist/api/stories/index.d.ts +1 -1
  16. package/dist/api/stories/index.js +1 -1
  17. package/dist/api/stories/stories.d.ts +35 -1
  18. package/dist/api/stories/stories.js +212 -7
  19. package/dist/api/stories/stories.types.d.ts +3 -0
  20. package/dist/api/testApi.d.ts +15 -0
  21. package/dist/api-v2/discover/discover.js +3 -3
  22. package/dist/api-v2/precompile/precompile.d.ts +5 -1
  23. package/dist/api-v2/precompile/precompile.js +16 -8
  24. package/dist/cli/cli-descriptions.d.ts +1 -1
  25. package/dist/cli/cli-descriptions.js +5 -0
  26. package/dist/cli/commands/migrate.js +23 -0
  27. package/dist/cli/commands/sync.js +4 -3
  28. package/dist/cli/index.js +7 -0
  29. package/dist/cli/utils/discover.js +28 -20
  30. package/dist/config/helper.js +6 -7
  31. package/dist/rollup/build-on-the-fly.d.ts +2 -1
  32. package/dist/rollup/build-on-the-fly.js +88 -6
  33. package/dist/utils/async-utils.d.ts +1 -0
  34. package/dist/utils/async-utils.js +16 -0
  35. package/dist/utils/files.d.ts +1 -0
  36. package/dist/utils/files.js +26 -15
  37. package/dist/utils/path-utils.d.ts +7 -4
  38. package/dist/utils/path-utils.js +11 -8
  39. package/dist-cjs/api/components/components.js +8 -2
  40. package/dist-cjs/api/components/components.sync.js +35 -8
  41. package/dist-cjs/api/stories/stories.js +217 -8
  42. package/dist-cjs/api-v2/discover/discover.js +2 -2
  43. package/dist-cjs/api-v2/precompile/precompile.js +16 -8
  44. package/dist-cjs/utils/async-utils.js +34 -0
  45. package/dist-cjs/utils/path-utils.js +11 -8
  46. package/package.json +7 -7
package/README.md CHANGED
@@ -13,7 +13,7 @@ If you've found an issue or you have feature request - <a href="https://github.c
13
13
 
14
14
  | | |
15
15
  | ---- | ------------ |
16
- | Node | LTS (18.x.x) |
16
+ | Node | 22.x.x or >=24.x.x |
17
17
 
18
18
  # 5.x.x version released!
19
19
 
@@ -32,7 +32,7 @@ If you've found an issue or you have feature request - <a href="https://github.c
32
32
 
33
33
  ## Breaking changes
34
34
 
35
- - Please note that sb-mig no longer extends support for Node versions older than 18.x.x, as part of its adoption of native ESM support.
35
+ - Please note that sb-mig no longer extends support for Node versions older than 22.x.x, as part of its adoption of native ESM support.
36
36
  - The sb-mig backup command has now been aligned with all other commands, causing minor changes in its execution (although functionalities have been preserved).
37
37
 
38
38
  Do not hesitate to get in touch if you encounter any issues or require further clarification on any points.
@@ -113,6 +113,40 @@ module.exports = {
113
113
  };
114
114
  ```
115
115
 
116
+ ## Programmatic asset API
117
+
118
+ The public management API exposes asset helpers for uploading a local file and updating existing asset metadata:
119
+
120
+ ```ts
121
+ import { managementApi } from "sb-mig/dist/api/managementApi.js";
122
+
123
+ await managementApi.assets.createAsset(
124
+ {
125
+ spaceId: "12345",
126
+ pathToFile: "./public/image.jpg",
127
+ payload: {
128
+ asset_folder_id: 67890,
129
+ validate_upload: 1,
130
+ },
131
+ },
132
+ config,
133
+ );
134
+
135
+ await managementApi.assets.updateAsset(
136
+ {
137
+ spaceId: "12345",
138
+ assetId: 98765,
139
+ payload: {
140
+ meta_data: {
141
+ alt: "Image alt text",
142
+ title: "Image title",
143
+ },
144
+ },
145
+ },
146
+ config,
147
+ );
148
+ ```
149
+
116
150
  # Schema documentation:
117
151
 
118
152
  ## Basics
@@ -1,6 +1,8 @@
1
- import type { GetAllAssets, GetAssetById, GetAssetByName, MigrateAsset } from "./assets.types.js";
1
+ import type { CreateAsset, GetAllAssets, GetAssetById, GetAssetByName, MigrateAsset, UpdateAsset } from "./assets.types.js";
2
2
  export declare const getAllAssets: GetAllAssets;
3
3
  export declare const getAssetByName: GetAssetByName;
4
4
  export declare const migrateAsset: MigrateAsset;
5
+ export declare const createAsset: CreateAsset;
6
+ export declare const updateAsset: UpdateAsset;
5
7
  export declare const getAssetById: GetAssetById;
6
8
  export declare const getAsset: (assetName: string | undefined) => Promise<void>;
@@ -5,6 +5,24 @@ import FormData from "form-data";
5
5
  import { createDir, isDirectoryExists } from "../../utils/files.js";
6
6
  import Logger from "../../utils/logger.js";
7
7
  import { getFileName, getSizeFromURL } from "../../utils/string-utils.js";
8
+ const isStoryblokSize = (size) => Boolean(size && /^\d+x\d+$/i.test(size));
9
+ const prepareSignedUploadPayload = (payload) => {
10
+ const { filename, asset_folder_id, id, size, validate_upload } = payload;
11
+ const inferredSize = isStoryblokSize(size)
12
+ ? size
13
+ : isStoryblokSize(getSizeFromURL(filename))
14
+ ? getSizeFromURL(filename)
15
+ : undefined;
16
+ return {
17
+ filename: getFileName(filename),
18
+ ...(asset_folder_id === undefined || asset_folder_id === null
19
+ ? {}
20
+ : { asset_folder_id }),
21
+ ...(id === undefined ? {} : { id }),
22
+ ...(inferredSize ? { size: inferredSize } : {}),
23
+ ...(validate_upload === undefined ? {} : { validate_upload }),
24
+ };
25
+ };
8
26
  // GET
9
27
  export const getAllAssets = async (args, config) => {
10
28
  const { spaceId, search } = args;
@@ -38,18 +56,12 @@ export const getAssetByName = async ({ spaceId, fileName }, config) => {
38
56
  };
39
57
  const requestSignedUploadUrl = ({ spaceId, payload }, config) => {
40
58
  const { sbApi, debug } = config;
41
- const { filename: _1, asset_folder_id, ext_id, space_id, deleted_at: _2, ...restPayload } = payload;
42
- const filename = getFileName(payload.filename);
43
- const size = getSizeFromURL(payload.filename);
59
+ const signedUploadPayload = prepareSignedUploadPayload(payload);
44
60
  return sbApi
45
- .post(`spaces/${spaceId}/assets/`, {
46
- filename,
47
- size,
48
- ...restPayload,
49
- })
61
+ .post(`spaces/${spaceId}/assets/`, signedUploadPayload)
50
62
  .then((signedResponseObject) => {
51
63
  if (debug) {
52
- Logger.log(`Signed upload URL has been requested for ${filename}.`);
64
+ Logger.log(`Signed upload URL has been requested for ${signedUploadPayload.filename}.`);
53
65
  }
54
66
  return signedResponseObject.data; // this is very bad... but storyblok-js-client types are pretty broken
55
67
  })
@@ -67,15 +79,20 @@ const uploadFile = ({ signedResponseObject, pathToFile }) => {
67
79
  // also append the file read stream
68
80
  form.append("file", fs.createReadStream(file));
69
81
  // submit your form
70
- form.submit(signedResponseObject.post_url, async (err, res) => {
71
- const statusCode = res.statusCode;
72
- if (statusCode === 204) {
73
- Logger.upload(`Asset uploaded ${getFileName(file)}`);
74
- }
75
- else {
76
- if (err)
77
- throw err;
78
- }
82
+ return new Promise((resolve, reject) => {
83
+ form.submit(signedResponseObject.post_url, (err, res) => {
84
+ if (err) {
85
+ reject(err);
86
+ return;
87
+ }
88
+ const statusCode = res?.statusCode;
89
+ if (statusCode === 204) {
90
+ Logger.upload(`Asset uploaded ${getFileName(file)}`);
91
+ resolve();
92
+ return;
93
+ }
94
+ reject(new Error(`Asset upload failed with status code ${statusCode ?? "unknown"}`));
95
+ });
79
96
  });
80
97
  };
81
98
  const downloadAsset = async (args, config) => {
@@ -121,6 +138,31 @@ export const migrateAsset = async ({ migrateTo, payload, syncDirection }, config
121
138
  }
122
139
  return true;
123
140
  };
141
+ export const createAsset = async ({ spaceId, pathToFile, payload = {} }, config) => {
142
+ const signedResponseObject = await requestSignedUploadUrl({
143
+ spaceId,
144
+ payload: {
145
+ ...payload,
146
+ filename: payload.filename ?? pathToFile,
147
+ },
148
+ }, config);
149
+ await uploadFile({ signedResponseObject, pathToFile });
150
+ return signedResponseObject;
151
+ };
152
+ export const updateAsset = async ({ spaceId, assetId, payload }, config) => {
153
+ const { sbApi } = config;
154
+ Logger.log(`Trying to update asset with id ${assetId}.`);
155
+ return sbApi
156
+ .put(`spaces/${spaceId}/assets/${assetId}`, payload)
157
+ .then((res) => {
158
+ Logger.success(`Asset '${assetId}' has been updated.`);
159
+ return res.data;
160
+ })
161
+ .catch((err) => {
162
+ Logger.error(`${err.message} in updateAsset function for asset ${assetId}`);
163
+ throw err;
164
+ });
165
+ };
124
166
  // GET
125
167
  export const getAssetById = async ({ spaceId, assetId }, config) => {
126
168
  const { sbApi } = config;
@@ -35,6 +35,29 @@ export interface SBAllAssetRequestResult {
35
35
  }
36
36
  export type SignedResponseObject = any;
37
37
  export type AssetPayload = Omit<SBAsset, "updated_at" | "created_at" | "id">;
38
+ export interface SignedUploadPayload {
39
+ filename: string;
40
+ id?: number;
41
+ asset_folder_id?: number | null;
42
+ size?: string;
43
+ validate_upload?: number;
44
+ }
45
+ export type CreateAssetPayload = Omit<SignedUploadPayload, "filename" | "id"> & Partial<Pick<SignedUploadPayload, "filename">>;
46
+ export type UpdateAssetPayload = {
47
+ asset_folder_id?: number | null;
48
+ internal_tag_ids?: number[];
49
+ is_private?: boolean;
50
+ locked?: boolean;
51
+ meta_data?: {
52
+ alt?: string;
53
+ copyright?: string;
54
+ source?: string;
55
+ title?: string;
56
+ [key: string]: unknown;
57
+ };
58
+ publish_at?: string | null;
59
+ [key: string]: unknown;
60
+ };
38
61
  export type GetAllAssets = ({ spaceId, }: {
39
62
  spaceId: string;
40
63
  search?: string;
@@ -52,16 +75,26 @@ export type MigrateAsset = ({ migrateTo, payload, syncDirection, }: {
52
75
  payload: AssetPayload;
53
76
  syncDirection: SyncDirection;
54
77
  }, config: RequestBaseConfig) => Promise<boolean>;
78
+ export type CreateAsset = ({ spaceId, pathToFile, payload, }: {
79
+ spaceId: string;
80
+ pathToFile: string;
81
+ payload?: CreateAssetPayload;
82
+ }, config: RequestBaseConfig) => Promise<SignedResponseObject>;
83
+ export type UpdateAsset = ({ spaceId, assetId, payload, }: {
84
+ spaceId: string;
85
+ assetId: number;
86
+ payload: UpdateAssetPayload;
87
+ }, config: RequestBaseConfig) => Promise<any>;
55
88
  export type UploadFile = ({ signedResponseObject, pathToFile, }: {
56
89
  signedResponseObject: SignedResponseObject;
57
90
  pathToFile: string;
58
- }) => void;
91
+ }) => Promise<void>;
59
92
  export type FinalizeUpload = ({ signedResponseObject, }: {
60
93
  signedResponseObject: SignedResponseObject;
61
94
  }) => void;
62
95
  export type RequestSignedUploadUrl = ({ spaceId, payload, }: {
63
96
  spaceId: string;
64
- payload: AssetPayload;
97
+ payload: AssetPayload | SignedUploadPayload;
65
98
  }, config: RequestBaseConfig) => Promise<any>;
66
99
  export type DownloadAsset = (args: {
67
100
  payload: AssetPayload;
@@ -1 +1,2 @@
1
- export { getAllAssets, migrateAsset, getAsset, getAssetById, getAssetByName, } from "./assets.js";
1
+ export { getAllAssets, createAsset, migrateAsset, getAsset, getAssetById, getAssetByName, updateAsset, } from "./assets.js";
2
+ export type { CreateAssetPayload, SignedUploadPayload, UpdateAssetPayload, } from "./assets.types.js";
@@ -1 +1 @@
1
- export { getAllAssets, migrateAsset, getAsset, getAssetById, getAssetByName, } from "./assets.js";
1
+ export { getAllAssets, createAsset, migrateAsset, getAsset, getAssetById, getAssetByName, updateAsset, } from "./assets.js";
@@ -109,7 +109,10 @@ export const removeComponent = (component, config) => {
109
109
  return sbApi
110
110
  .delete(`spaces/${spaceId}/components/${id}`, {})
111
111
  .then((res) => res.data)
112
- .catch((err) => console.error(err));
112
+ .catch((err) => {
113
+ console.error(err);
114
+ throw err;
115
+ });
113
116
  };
114
117
  /*
115
118
  *
@@ -165,7 +168,10 @@ export const removeComponentGroup = (componentGroup, config) => {
165
168
  return sbApi
166
169
  .delete(`spaces/${spaceId}/component_groups/${id}`, {})
167
170
  .then((res) => res.data)
168
- .catch((err) => console.error(err));
171
+ .catch((err) => {
172
+ console.error(err);
173
+ throw err;
174
+ });
169
175
  };
170
176
  export const createComponentsGroup = (groupName, config) => {
171
177
  const { spaceId, sbApi } = config;
@@ -27,6 +27,9 @@ const defaultProgress = (event) => {
27
27
  }
28
28
  };
29
29
  import { createComponent, createComponentsGroup, getAllComponents, getAllComponentsGroups, removeComponent, removeComponentGroup, updateComponent, } from "./components.js";
30
+ function getErrorMessage(error) {
31
+ return error instanceof Error ? error.message : String(error);
32
+ }
30
33
  async function ensureComponentGroupsExist(groupNames, config, options = {}) {
31
34
  try {
32
35
  const existing = await getAllComponentsGroups(config);
@@ -79,10 +82,34 @@ export async function syncComponentsData(args, config) {
79
82
  }
80
83
  }
81
84
  else {
82
- await Promise.allSettled([
83
- ...(existingComponents ?? []).map((c) => removeComponent(c, config)),
84
- ...(existingGroups ?? []).map((g) => removeComponentGroup(g, config)),
85
- ]);
85
+ const removalTargets = [
86
+ ...(existingComponents ?? []).map((component) => ({
87
+ type: "component",
88
+ name: String(component?.name ?? component?.id ?? "unknown"),
89
+ remove: () => removeComponent(component, config),
90
+ })),
91
+ ...(existingGroups ?? []).map((group) => ({
92
+ type: "component group",
93
+ name: String(group?.name ?? group?.id ?? "unknown"),
94
+ remove: () => removeComponentGroup(group, config),
95
+ })),
96
+ ];
97
+ const removalResults = await Promise.allSettled(removalTargets.map((target) => target.remove()));
98
+ removalResults.forEach((removalResult, index) => {
99
+ if (removalResult.status === "fulfilled")
100
+ return;
101
+ const target = removalTargets[index];
102
+ if (!target)
103
+ return;
104
+ const name = `${target.type} '${target.name}'`;
105
+ const message = getErrorMessage(removalResult.reason);
106
+ result.skipped.push(name);
107
+ result.errors.push({
108
+ name,
109
+ message: `SSOT removal failed: ${message}`,
110
+ });
111
+ Logger.warning(`Could not remove ${name} during SSOT sync: ${message}`);
112
+ });
86
113
  }
87
114
  }
88
115
  const nonEmptyComponents = components.filter((c) => !isObjectEmpty(c));
@@ -164,7 +191,7 @@ export async function syncComponentsData(args, config) {
164
191
  catch (error) {
165
192
  result.errors.push({
166
193
  name,
167
- message: error instanceof Error ? error.message : String(error),
194
+ message: getErrorMessage(error),
168
195
  });
169
196
  progress({
170
197
  type: "progress",
@@ -172,7 +199,7 @@ export async function syncComponentsData(args, config) {
172
199
  total: totalComponents,
173
200
  name,
174
201
  action: "error",
175
- message: error instanceof Error ? error.message : String(error),
202
+ message: getErrorMessage(error),
176
203
  });
177
204
  }
178
205
  }
@@ -213,7 +240,7 @@ export async function syncComponentsData(args, config) {
213
240
  catch (error) {
214
241
  result.errors.push({
215
242
  name,
216
- message: error instanceof Error ? error.message : String(error),
243
+ message: getErrorMessage(error),
217
244
  });
218
245
  progress({
219
246
  type: "progress",
@@ -221,7 +248,7 @@ export async function syncComponentsData(args, config) {
221
248
  total: totalComponents,
222
249
  name,
223
250
  action: "error",
224
- message: error instanceof Error ? error.message : String(error),
251
+ message: getErrorMessage(error),
225
252
  });
226
253
  }
227
254
  }
@@ -1,3 +1,4 @@
1
+ import type { PublishLanguagesOption } from "../stories/stories.types.js";
1
2
  import type { RequestBaseConfig } from "../utils/request.js";
2
3
  import { type MigrationComponentAliasesByMigration, type MigrationComponentOverridesByMigration } from "./migration-component-scope.js";
3
4
  import { type PreparedMigrationValidator } from "./migration-validation.js";
@@ -43,6 +44,8 @@ interface MigrateItems {
43
44
  startsWith?: string;
44
45
  };
45
46
  dryRun?: boolean;
47
+ publish?: boolean;
48
+ publishLanguages?: PublishLanguagesOption;
46
49
  fromFilePath?: string;
47
50
  fileName?: string;
48
51
  preparedMigrationConfigs?: PreparedMigrationConfig[];
@@ -67,8 +70,8 @@ export declare const runMigrationPipelineInMemory: ({ itemType, itemsToMigrate,
67
70
  itemsToMigrate: any[];
68
71
  preparedMigrationConfigs: PreparedMigrationConfig[];
69
72
  }) => MigrationPipelineResult;
70
- export declare const migrateAllComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, fromFilePath, fileName, migrationComponentAliases, migrationComponentOverrides, }: Omit<MigrateItems, "componentsToMigrate" | "preparedMigrationConfigs">, config: RequestBaseConfig) => Promise<void>;
71
- export declare const doTheMigration: ({ itemType, from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, migrateFrom, fromFilePath, fileName, }: {
73
+ export declare const migrateAllComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, migrationComponentAliases, migrationComponentOverrides, }: Omit<MigrateItems, "componentsToMigrate" | "preparedMigrationConfigs">, config: RequestBaseConfig) => Promise<void>;
74
+ export declare const doTheMigration: ({ itemType, from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, publish, publishLanguages, migrateFrom, fromFilePath, fileName, }: {
72
75
  itemType?: "story" | "preset";
73
76
  from: string;
74
77
  itemsToMigrate: any[];
@@ -76,9 +79,11 @@ export declare const doTheMigration: ({ itemType, from, itemsToMigrate, migratio
76
79
  migrationConfigs?: PreparedMigrationConfig[];
77
80
  to: string;
78
81
  dryRun?: boolean;
82
+ publish?: boolean;
83
+ publishLanguages?: PublishLanguagesOption;
79
84
  migrateFrom: MigrateFrom;
80
85
  fromFilePath?: string;
81
86
  fileName?: string;
82
87
  }, config: RequestBaseConfig) => Promise<void>;
83
- export declare const migrateProvidedComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, fromFilePath, fileName, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }: MigrateItems, config: RequestBaseConfig) => Promise<void>;
88
+ export declare const migrateProvidedComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }: MigrateItems, config: RequestBaseConfig) => Promise<void>;
84
89
  export {};
@@ -9,6 +9,7 @@ import { isObjectEmpty } from "../../utils/object-utils.js";
9
9
  import { managementApi } from "../managementApi.js";
10
10
  import { buildPreMigrationBackupBaseName, resolveOutputFileBaseName, shouldUseDatestampForArtifacts, } from "./file-naming.js";
11
11
  import { extendMigrationMapperWithAliases, resolveMigrationComponentsToMigrate, } from "./migration-component-scope.js";
12
+ import { saveMigrationRunLog } from "./migration-run-log.js";
12
13
  import { discoverMigrationValidatorForMigrationFile, MigrationValidationFailedError, runPreparedMigrationValidator, } from "./migration-validation.js";
13
14
  import { summarizeMutationWriteResults } from "./write-summary.js";
14
15
  export const normalizeMigrationConfigNames = (migrationConfig) => {
@@ -245,7 +246,6 @@ const applySingleMigrationToItems = ({ itemType, itemsToMigrate, preparedMigrati
245
246
  };
246
247
  };
247
248
  export const runMigrationPipelineInMemory = ({ itemType, itemsToMigrate, preparedMigrationConfigs, }) => {
248
- const originalItems = deepClone(itemsToMigrate);
249
249
  let workingItems = deepClone(itemsToMigrate);
250
250
  const stepReports = [];
251
251
  for (const preparedMigrationConfig of preparedMigrationConfigs) {
@@ -282,7 +282,7 @@ export const runMigrationPipelineInMemory = ({ itemType, itemsToMigrate, prepare
282
282
  stepReports.push(stepReport);
283
283
  }
284
284
  const changedItems = workingItems.filter((item, index) => {
285
- const originalItem = originalItems[index];
285
+ const originalItem = itemsToMigrate[index];
286
286
  if (!originalItem) {
287
287
  return true;
288
288
  }
@@ -295,7 +295,7 @@ export const runMigrationPipelineInMemory = ({ itemType, itemsToMigrate, prepare
295
295
  totalItems: workingItems.length,
296
296
  };
297
297
  };
298
- const savePipelineSummary = async ({ artifactBaseName, useDatestamp, from, itemType, dryRun, migrateFrom, fromFilePath, pipelineResult, }, config) => {
298
+ const savePipelineSummary = async ({ artifactBaseName, useDatestamp, from, itemType, dryRun, publish, publishLanguages, migrateFrom, fromFilePath, pipelineResult, }, config) => {
299
299
  await createAndSaveToFile({
300
300
  datestamp: useDatestamp,
301
301
  ext: "json",
@@ -308,6 +308,12 @@ const savePipelineSummary = async ({ artifactBaseName, useDatestamp, from, itemT
308
308
  from,
309
309
  fromFilePath: fromFilePath || null,
310
310
  },
311
+ writeMode: itemType === "story" && publish ? "publish" : "save",
312
+ publishLanguages: itemType === "story" && publish
313
+ ? {
314
+ requested: publishLanguages,
315
+ }
316
+ : null,
311
317
  totalItems: pipelineResult.totalItems,
312
318
  totalChangedItems: pipelineResult.changedItems.length,
313
319
  steps: pipelineResult.stepReports,
@@ -378,7 +384,7 @@ const loadItemsToMigrate = async ({ itemType, migrateFrom, from, filters, fromFi
378
384
  spaceId: from,
379
385
  });
380
386
  };
381
- export const migrateAllComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, fromFilePath, fileName, migrationComponentAliases, migrationComponentOverrides, }, config) => {
387
+ export const migrateAllComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, migrationComponentAliases, migrationComponentOverrides, }, config) => {
382
388
  Logger.warning(`Trying to migrate all ${itemType} from ${migrateFrom}, ${from} to ${to}...`);
383
389
  const preparedMigrationConfigs = prepareMigrationConfigs({
384
390
  migrationConfig,
@@ -397,12 +403,14 @@ export const migrateAllComponentsDataInStories = async ({ itemType, migrationCon
397
403
  to,
398
404
  filters,
399
405
  dryRun,
406
+ publish,
407
+ publishLanguages,
400
408
  fromFilePath,
401
409
  fileName,
402
410
  preparedMigrationConfigs,
403
411
  }, config);
404
412
  };
405
- export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, migrateFrom, fromFilePath, fileName, }, config) => {
413
+ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, publish, publishLanguages, migrateFrom, fromFilePath, fileName, }, config) => {
406
414
  const preparedMigrationConfigs = migrationConfigs ||
407
415
  prepareMigrationConfigs({
408
416
  migrationConfig: migrationConfig || [],
@@ -472,6 +480,8 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
472
480
  from,
473
481
  itemType,
474
482
  dryRun,
483
+ publish,
484
+ publishLanguages,
475
485
  migrateFrom,
476
486
  fromFilePath,
477
487
  pipelineResult,
@@ -489,11 +499,23 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
489
499
  return;
490
500
  }
491
501
  let writeResults = [];
502
+ let resolvedPublishLanguages;
492
503
  if (itemType === "story") {
504
+ if (publish && publishLanguages !== undefined) {
505
+ resolvedPublishLanguages =
506
+ await managementApi.stories.resolvePublishLanguageCodes(publishLanguages, {
507
+ ...config,
508
+ spaceId: to,
509
+ });
510
+ }
493
511
  writeResults = await managementApi.stories.updateStories({
494
512
  stories: pipelineResult.changedItems,
495
513
  spaceId: to,
496
- options: { publish: false },
514
+ options: {
515
+ publish: Boolean(publish),
516
+ publishLanguages: resolvedPublishLanguages,
517
+ preservePublishState: Boolean(publish),
518
+ },
497
519
  }, config);
498
520
  }
499
521
  else if (itemType === "preset") {
@@ -504,6 +526,27 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
504
526
  }, config);
505
527
  }
506
528
  const writeSummary = summarizeMutationWriteResults(writeResults);
529
+ try {
530
+ await saveMigrationRunLog({
531
+ artifactBaseName,
532
+ useDatestamp,
533
+ from,
534
+ to,
535
+ itemType,
536
+ dryRun,
537
+ publish,
538
+ publishLanguages,
539
+ resolvedPublishLanguages,
540
+ migrateFrom,
541
+ fromFilePath,
542
+ pipelineResult,
543
+ writeResults,
544
+ writeSummary,
545
+ }, config);
546
+ }
547
+ catch (error) {
548
+ Logger.warning(`[MIGRATION] Could not write migration run log: ${error instanceof Error ? error.message : String(error)}`);
549
+ }
507
550
  if (writeSummary.failed === 0) {
508
551
  Logger.success(`[MIGRATION] Update complete. ${writeSummary.successful}/${writeSummary.total} ${itemType}(s) updated successfully.`);
509
552
  return;
@@ -527,7 +570,7 @@ const saveBackupToFile = async ({ itemType, res, folder, filename }, config) =>
527
570
  res: res,
528
571
  }, config);
529
572
  };
530
- export const migrateProvidedComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, fromFilePath, fileName, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }, config) => {
573
+ export const migrateProvidedComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }, config) => {
531
574
  const resolvedMigrationConfigs = preparedMigrationConfigs ||
532
575
  prepareMigrationConfigs({
533
576
  migrationConfig,
@@ -562,6 +605,8 @@ export const migrateProvidedComponentsDataInStories = async ({ itemType, migrati
562
605
  from,
563
606
  to,
564
607
  dryRun,
608
+ publish,
609
+ publishLanguages,
565
610
  migrateFrom,
566
611
  fromFilePath,
567
612
  fileName,
@@ -0,0 +1,77 @@
1
+ import type { MigrateFrom, MigrationPipelineResult } from "./component-data-migration.js";
2
+ import type { MutationWriteResult, MutationWriteSummary } from "./write-summary.js";
3
+ import type { PublishLanguagesOption } from "../stories/stories.types.js";
4
+ import type { RequestBaseConfig } from "../utils/request.js";
5
+ type MigrationRunLogEvent = "update_success" | "update_failed" | "publish_success" | "publish_failed" | "publish_skipped" | "migration_write_summary";
6
+ export interface MigrationRunLogRecord {
7
+ timestamp: string;
8
+ event: MigrationRunLogEvent;
9
+ runId: string;
10
+ itemType: "story" | "preset";
11
+ source: {
12
+ migrateFrom: MigrateFrom;
13
+ from: string;
14
+ fromFilePath: string | null;
15
+ };
16
+ target: {
17
+ to: string;
18
+ };
19
+ writeMode: "publish" | "save";
20
+ publishLanguages?: {
21
+ requested?: PublishLanguagesOption;
22
+ resolved?: string[];
23
+ };
24
+ dryRun: boolean;
25
+ migrationConfigs: string[];
26
+ totalItems: number;
27
+ totalChangedItems: number;
28
+ writeSummary?: {
29
+ total: number;
30
+ successful: number;
31
+ failed: number;
32
+ failedItems: Array<{
33
+ id?: number | string;
34
+ name?: string;
35
+ slug?: string;
36
+ spaceId?: string;
37
+ status?: number | string;
38
+ response?: string | null;
39
+ stage?: "update" | "publish";
40
+ sourcePublishState?: string;
41
+ publishSkippedReason?: string;
42
+ }>;
43
+ };
44
+ item?: {
45
+ index: number;
46
+ id?: number | string;
47
+ name?: string;
48
+ slug?: string;
49
+ spaceId?: string;
50
+ };
51
+ status?: number | string | null;
52
+ response?: string | null;
53
+ stage?: "update" | "publish";
54
+ sourcePublishState?: string;
55
+ publishSkippedReason?: string;
56
+ error?: unknown;
57
+ }
58
+ interface SaveMigrationRunLogArgs {
59
+ artifactBaseName: string;
60
+ useDatestamp: boolean;
61
+ from: string;
62
+ to: string;
63
+ itemType: "story" | "preset";
64
+ dryRun?: boolean;
65
+ publish?: boolean;
66
+ publishLanguages?: PublishLanguagesOption;
67
+ resolvedPublishLanguages?: string[];
68
+ migrateFrom: MigrateFrom;
69
+ fromFilePath?: string;
70
+ pipelineResult: MigrationPipelineResult;
71
+ writeResults: PromiseSettledResult<MutationWriteResult>[];
72
+ writeSummary: MutationWriteSummary;
73
+ }
74
+ export declare const buildMigrationRunLogRecords: ({ from, to, itemType, dryRun, publish, publishLanguages, resolvedPublishLanguages, migrateFrom, fromFilePath, pipelineResult, writeResults, writeSummary, }: Omit<SaveMigrationRunLogArgs, "artifactBaseName" | "useDatestamp">) => MigrationRunLogRecord[];
75
+ export declare const recordsToJsonl: (records: MigrationRunLogRecord[]) => string;
76
+ export declare const saveMigrationRunLog: (args: SaveMigrationRunLogArgs, config: RequestBaseConfig) => Promise<void>;
77
+ export {};