sb-mig 6.2.0 → 6.3.0-beta.2
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/data-migration/component-data-migration.d.ts +81 -0
- package/dist/api/data-migration/component-data-migration.js +391 -95
- package/dist/api/data-migration/migration-run-log.d.ts +3 -1
- package/dist/api/data-migration/migration-run-log.js +2 -1
- 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 +3 -3
- package/dist/cli/cli-descriptions.js +92 -18
- package/dist/cli/commands/copy.js +2091 -145
- package/dist/cli/commands/migrate.js +27 -1
- package/dist/cli/index.js +3 -0
- package/dist/utils/files.d.ts +2 -0
- package/dist/utils/files.js +86 -3
- package/dist-cjs/api/stories/stories.js +3 -3
- package/dist-cjs/api-v2/requestConfig.js +29 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,13 +9,20 @@ If you've found an issue or you have feature request - <a href="https://github.c
|
|
|
9
9
|
[](https://github.com/sb-mig/sb-mig/issues?q=is%3Aopen+is%3Aissue)
|
|
10
10
|

|
|
11
11
|
|
|
12
|
+
# Documentation
|
|
13
|
+
|
|
14
|
+
- [Docs site](https://sb-mig.vercel.app) is the canonical documentation for current sb-mig workflows.
|
|
15
|
+
- [Quickstart](https://sb-mig.vercel.app/docs/quickstart) covers installation, credentials, and first safe commands.
|
|
16
|
+
- [CLI reference](https://sb-mig.vercel.app/docs/reference/cli) covers command families and safety conventions.
|
|
17
|
+
- [LLM docs index](https://sb-mig.vercel.app/llms.txt) and [full Markdown corpus](https://sb-mig.vercel.app/llms-full.txt) are available for coding agents and other LLM tools.
|
|
18
|
+
|
|
12
19
|
# Requirements:
|
|
13
20
|
|
|
14
21
|
| | |
|
|
15
22
|
| ---- | ------------ |
|
|
16
23
|
| Node | 22.x.x or >=24.x.x |
|
|
17
24
|
|
|
18
|
-
# 6.x.x
|
|
25
|
+
# Current release notes: 6.x.x
|
|
19
26
|
|
|
20
27
|
## Important Updates
|
|
21
28
|
|
|
@@ -38,7 +45,11 @@ If you've found an issue or you have feature request - <a href="https://github.c
|
|
|
38
45
|
- `--publishLanguages` is now `--publicationLanguages`
|
|
39
46
|
- `--preservePublishedLayer` is now `--publicationMode preserve-layers`
|
|
40
47
|
|
|
41
|
-
#
|
|
48
|
+
# Historical release notes
|
|
49
|
+
|
|
50
|
+
The sections below are retained for historical context. For current usage, prefer the [docs site](https://sb-mig.vercel.app) and `sb-mig --help`.
|
|
51
|
+
|
|
52
|
+
## 5.x.x version released!
|
|
42
53
|
|
|
43
54
|
## Important Updates
|
|
44
55
|
|
|
@@ -60,13 +71,13 @@ If you've found an issue or you have feature request - <a href="https://github.c
|
|
|
60
71
|
|
|
61
72
|
Do not hesitate to get in touch if you encounter any issues or require further clarification on any points.
|
|
62
73
|
|
|
63
|
-
|
|
74
|
+
## 4.x.x version released!
|
|
64
75
|
|
|
65
76
|
- Whole deployment now, is handled by [semantic-release](https://github.com/semantic-release/semantic-release). And is just normal repository, instead of Lerna monorepo which is not needed anymore, and it was recently unmaintained (now it was passed to `nrwl` to maintain (https://github.com/lerna/lerna/issues/3121) will see what will happen in future with it :)
|
|
66
77
|
- Fromt the code perspective, there are no **breaking changes** between **3.x.x** and **4.x.x** but i'm going to fix some stuff now and add some more functionalities to it. So stay tuned!
|
|
67
78
|
- This Readme, which is also kinda documentation, will also be updated. Cause in some places there is misleading information. The best documentation though is just `sb-mig --help` command.
|
|
68
79
|
|
|
69
|
-
|
|
80
|
+
## 3.x.x version released!
|
|
70
81
|
|
|
71
82
|
- completely rewritten to simple [Meow](https://github.com/sindresorhus/meow) lib with help of Typescript. Check [migration guide](https://github.com/sb-mig/sb-mig/blob/oclif-research/MIGRATION-GUIDE-v3.md)
|
|
72
83
|
- support native es modules
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import Logger from "../../utils/logger.js";
|
|
2
|
+
import { getAllItemsWithPagination } from "../utils/request.js";
|
|
3
|
+
const joinQueryValues = (values) => values && values.length > 0 ? values.join(",") : undefined;
|
|
4
|
+
export const getAllAssetFolders = async (args, config) => {
|
|
5
|
+
const { spaceId, search, withParent, byIds, byUuids } = args;
|
|
6
|
+
const { sbApi } = config;
|
|
7
|
+
const assetFolders = await getAllItemsWithPagination({
|
|
8
|
+
apiFn: ({ per_page, page }) => sbApi
|
|
9
|
+
.get(`spaces/${spaceId}/asset_folders/`, {
|
|
10
|
+
...(search ? { search } : {}),
|
|
11
|
+
...(withParent !== undefined && withParent !== null
|
|
12
|
+
? { with_parent: String(withParent) }
|
|
13
|
+
: {}),
|
|
14
|
+
...(joinQueryValues(byIds)
|
|
15
|
+
? { by_ids: joinQueryValues(byIds) }
|
|
16
|
+
: {}),
|
|
17
|
+
...(joinQueryValues(byUuids)
|
|
18
|
+
? { by_uuids: joinQueryValues(byUuids) }
|
|
19
|
+
: {}),
|
|
20
|
+
per_page,
|
|
21
|
+
page,
|
|
22
|
+
})
|
|
23
|
+
.catch((err) => {
|
|
24
|
+
if (err.response?.status === 404) {
|
|
25
|
+
Logger.error(`There are no asset folders in your Storyblok ${spaceId} space.`);
|
|
26
|
+
return {
|
|
27
|
+
data: { asset_folders: [] },
|
|
28
|
+
total: 0,
|
|
29
|
+
perPage: 100,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
Logger.error(err);
|
|
33
|
+
throw err;
|
|
34
|
+
}),
|
|
35
|
+
params: {},
|
|
36
|
+
itemsKey: "asset_folders",
|
|
37
|
+
});
|
|
38
|
+
return { asset_folders: assetFolders };
|
|
39
|
+
};
|
|
40
|
+
export const createAssetFolder = async ({ spaceId, payload }, config) => {
|
|
41
|
+
const { sbApi } = config;
|
|
42
|
+
return sbApi
|
|
43
|
+
.post(`spaces/${spaceId}/asset_folders/`, {
|
|
44
|
+
asset_folder: {
|
|
45
|
+
name: payload.name,
|
|
46
|
+
...(payload.parent_id === undefined
|
|
47
|
+
? {}
|
|
48
|
+
: { parent_id: payload.parent_id }),
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
.then(({ data }) => data)
|
|
52
|
+
.catch((err) => {
|
|
53
|
+
Logger.error(err);
|
|
54
|
+
throw err;
|
|
55
|
+
});
|
|
56
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RequestBaseConfig } from "../utils/request.js";
|
|
2
|
+
export interface SBAssetFolder {
|
|
3
|
+
id: number;
|
|
4
|
+
name: string;
|
|
5
|
+
parent_id: number | null;
|
|
6
|
+
uuid?: string;
|
|
7
|
+
parent_uuid?: string | null;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface SBAllAssetFoldersRequestResult {
|
|
11
|
+
asset_folders: SBAssetFolder[];
|
|
12
|
+
}
|
|
13
|
+
export type CreateAssetFolderPayload = {
|
|
14
|
+
name: string;
|
|
15
|
+
parent_id?: number | null;
|
|
16
|
+
};
|
|
17
|
+
export type GetAllAssetFolders = ({ spaceId, search, withParent, byIds, byUuids, }: {
|
|
18
|
+
spaceId: string;
|
|
19
|
+
search?: string;
|
|
20
|
+
withParent?: number | string;
|
|
21
|
+
byIds?: Array<number | string>;
|
|
22
|
+
byUuids?: string[];
|
|
23
|
+
}, config: RequestBaseConfig) => Promise<SBAllAssetFoldersRequestResult>;
|
|
24
|
+
export type CreateAssetFolder = ({ spaceId, payload, }: {
|
|
25
|
+
spaceId: string;
|
|
26
|
+
payload: CreateAssetFolderPayload;
|
|
27
|
+
}, config: RequestBaseConfig) => Promise<{
|
|
28
|
+
asset_folder: SBAssetFolder;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type { CreateAsset, GetAllAssets, GetAssetById, GetAssetByName, MigrateAsset, UpdateAsset } from "./assets.types.js";
|
|
1
|
+
import type { CreateAsset, CreateAssetAndFinalize, GetAllAssets, GetAssetById, GetAssetByName, MigrateAsset, UpdateAsset, DownloadAsset, FinishAssetUpload } from "./assets.types.js";
|
|
2
2
|
export declare const getAllAssets: GetAllAssets;
|
|
3
3
|
export declare const getAssetByName: GetAssetByName;
|
|
4
|
+
export declare const finishAssetUpload: FinishAssetUpload;
|
|
5
|
+
export declare const downloadAsset: DownloadAsset;
|
|
4
6
|
export declare const migrateAsset: MigrateAsset;
|
|
5
7
|
export declare const createAsset: CreateAsset;
|
|
8
|
+
export declare const createAssetAndFinalize: CreateAssetAndFinalize;
|
|
6
9
|
export declare const updateAsset: UpdateAsset;
|
|
7
10
|
export declare const getAssetById: GetAssetById;
|
|
8
11
|
export declare const getAsset: (assetName: string | undefined) => Promise<void>;
|
|
@@ -5,6 +5,7 @@ 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
|
+
import { getAllItemsWithPagination } from "../utils/request.js";
|
|
8
9
|
const isStoryblokSize = (size) => Boolean(size && /^\d+x\d+$/i.test(size));
|
|
9
10
|
const prepareSignedUploadPayload = (payload) => {
|
|
10
11
|
const { filename, asset_folder_id, id, size, validate_upload } = payload;
|
|
@@ -27,23 +28,26 @@ const prepareSignedUploadPayload = (payload) => {
|
|
|
27
28
|
export const getAllAssets = async (args, config) => {
|
|
28
29
|
const { spaceId, search } = args;
|
|
29
30
|
const { sbApi } = config;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
const assets = await getAllItemsWithPagination({
|
|
32
|
+
apiFn: ({ per_page, page }) => sbApi
|
|
33
|
+
.get(`spaces/${spaceId}/assets/`, {
|
|
34
|
+
// @ts-ignore TODO: have to submit ISSUE to storyblok-js-client (in documentation it is search, in typescript its search_term STORYBLOK_ISSUE
|
|
35
|
+
search: search ? search : "",
|
|
36
|
+
per_page,
|
|
37
|
+
page,
|
|
38
|
+
})
|
|
39
|
+
.catch((err) => {
|
|
40
|
+
if (err.response?.status === 404) {
|
|
41
|
+
Logger.error(`There is no assets in your Storyblok ${spaceId} space.`);
|
|
42
|
+
return { data: { assets: [] }, total: 0, perPage: 100 };
|
|
43
|
+
}
|
|
43
44
|
Logger.error(err);
|
|
44
|
-
|
|
45
|
-
}
|
|
45
|
+
throw err;
|
|
46
|
+
}),
|
|
47
|
+
params: {},
|
|
48
|
+
itemsKey: "assets",
|
|
46
49
|
});
|
|
50
|
+
return { assets };
|
|
47
51
|
};
|
|
48
52
|
export const getAssetByName = async ({ spaceId, fileName }, config) => {
|
|
49
53
|
const result = await getAllAssets({ spaceId, search: fileName }, config);
|
|
@@ -95,7 +99,31 @@ const uploadFile = ({ signedResponseObject, pathToFile }) => {
|
|
|
95
99
|
});
|
|
96
100
|
});
|
|
97
101
|
};
|
|
98
|
-
const
|
|
102
|
+
const getSignedUploadAssetId = (signedResponseObject) => {
|
|
103
|
+
const assetId = Number(signedResponseObject?.id ?? signedResponseObject?.asset?.id);
|
|
104
|
+
if (!Number.isFinite(assetId)) {
|
|
105
|
+
throw new Error("Signed upload response did not include an asset id, so upload cannot be finalized.");
|
|
106
|
+
}
|
|
107
|
+
return assetId;
|
|
108
|
+
};
|
|
109
|
+
const normalizeFinishedUploadAsset = (finishedUpload) => {
|
|
110
|
+
const asset = finishedUpload?.asset ?? finishedUpload?.data?.asset ?? finishedUpload;
|
|
111
|
+
if (!asset?.id || !asset?.filename) {
|
|
112
|
+
throw new Error("Finish upload response did not include a target asset id and filename.");
|
|
113
|
+
}
|
|
114
|
+
return asset;
|
|
115
|
+
};
|
|
116
|
+
export const finishAssetUpload = async ({ spaceId, assetId }, config) => {
|
|
117
|
+
const { sbApi } = config;
|
|
118
|
+
return sbApi
|
|
119
|
+
.get(`spaces/${spaceId}/assets/${assetId}/finish_upload`, {})
|
|
120
|
+
.then(({ data }) => data)
|
|
121
|
+
.catch((err) => {
|
|
122
|
+
Logger.error(err);
|
|
123
|
+
throw err;
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
export const downloadAsset = async (args, config) => {
|
|
99
127
|
const { debug, sbmigWorkingDirectory } = config;
|
|
100
128
|
const { payload } = args;
|
|
101
129
|
if (!sbmigWorkingDirectory) {
|
|
@@ -149,6 +177,23 @@ export const createAsset = async ({ spaceId, pathToFile, payload = {} }, config)
|
|
|
149
177
|
await uploadFile({ signedResponseObject, pathToFile });
|
|
150
178
|
return signedResponseObject;
|
|
151
179
|
};
|
|
180
|
+
export const createAssetAndFinalize = async ({ spaceId, pathToFile, payload = {} }, config) => {
|
|
181
|
+
const signedResponseObject = await createAsset({
|
|
182
|
+
spaceId,
|
|
183
|
+
pathToFile,
|
|
184
|
+
payload: {
|
|
185
|
+
...payload,
|
|
186
|
+
filename: payload.filename ?? pathToFile,
|
|
187
|
+
validate_upload: payload.validate_upload ?? 1,
|
|
188
|
+
},
|
|
189
|
+
}, config);
|
|
190
|
+
const assetId = getSignedUploadAssetId(signedResponseObject);
|
|
191
|
+
const finishedUpload = await finishAssetUpload({
|
|
192
|
+
spaceId,
|
|
193
|
+
assetId,
|
|
194
|
+
}, config);
|
|
195
|
+
return normalizeFinishedUploadAsset(finishedUpload);
|
|
196
|
+
};
|
|
152
197
|
export const updateAsset = async ({ spaceId, assetId, payload }, config) => {
|
|
153
198
|
const { sbApi } = config;
|
|
154
199
|
Logger.log(`Trying to update asset with id ${assetId}.`);
|
|
@@ -7,7 +7,7 @@ export interface SBAsset {
|
|
|
7
7
|
space_id: number;
|
|
8
8
|
created_at: string;
|
|
9
9
|
updated_at: string;
|
|
10
|
-
asset_folder_id?: null;
|
|
10
|
+
asset_folder_id?: number | null;
|
|
11
11
|
deleted_at: null;
|
|
12
12
|
content_length: number;
|
|
13
13
|
content_type: AssetTypes;
|
|
@@ -80,6 +80,11 @@ export type CreateAsset = ({ spaceId, pathToFile, payload, }: {
|
|
|
80
80
|
pathToFile: string;
|
|
81
81
|
payload?: CreateAssetPayload;
|
|
82
82
|
}, config: RequestBaseConfig) => Promise<SignedResponseObject>;
|
|
83
|
+
export type CreateAssetAndFinalize = ({ spaceId, pathToFile, payload, }: {
|
|
84
|
+
spaceId: string;
|
|
85
|
+
pathToFile: string;
|
|
86
|
+
payload?: CreateAssetPayload;
|
|
87
|
+
}, config: RequestBaseConfig) => Promise<SBAsset>;
|
|
83
88
|
export type UpdateAsset = ({ spaceId, assetId, payload, }: {
|
|
84
89
|
spaceId: string;
|
|
85
90
|
assetId: number;
|
|
@@ -92,6 +97,12 @@ export type UploadFile = ({ signedResponseObject, pathToFile, }: {
|
|
|
92
97
|
export type FinalizeUpload = ({ signedResponseObject, }: {
|
|
93
98
|
signedResponseObject: SignedResponseObject;
|
|
94
99
|
}) => void;
|
|
100
|
+
export type FinishAssetUpload = ({ spaceId, assetId, }: {
|
|
101
|
+
spaceId: string;
|
|
102
|
+
assetId: number;
|
|
103
|
+
}, config: RequestBaseConfig) => Promise<{
|
|
104
|
+
asset: Partial<SBAsset> & Pick<SBAsset, "id" | "filename">;
|
|
105
|
+
}>;
|
|
95
106
|
export type RequestSignedUploadUrl = ({ spaceId, payload, }: {
|
|
96
107
|
spaceId: string;
|
|
97
108
|
payload: AssetPayload | SignedUploadPayload;
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
export { getAllAssets, createAsset, migrateAsset, getAsset, getAssetById, getAssetByName, updateAsset, } from "./assets.js";
|
|
2
|
-
export
|
|
1
|
+
export { getAllAssets, createAsset, migrateAsset, getAsset, getAssetById, getAssetByName, createAssetAndFinalize, downloadAsset, finishAssetUpload, updateAsset, } from "./assets.js";
|
|
2
|
+
export { createAssetFolder, getAllAssetFolders } from "./asset-folders.js";
|
|
3
|
+
export type { CreateAssetAndFinalize, CreateAssetPayload, FinishAssetUpload, SignedUploadPayload, UpdateAssetPayload, } from "./assets.types.js";
|
|
4
|
+
export type { CreateAssetFolder, CreateAssetFolderPayload, SBAllAssetFoldersRequestResult, SBAssetFolder, } from "./asset-folders.types.js";
|
package/dist/api/assets/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { getAllAssets, createAsset, migrateAsset, getAsset, getAssetById, getAssetByName, updateAsset, } from "./assets.js";
|
|
1
|
+
export { getAllAssets, createAsset, migrateAsset, getAsset, getAssetById, getAssetByName, createAssetAndFinalize, downloadAsset, finishAssetUpload, updateAsset, } from "./assets.js";
|
|
2
|
+
export { createAssetFolder, getAllAssetFolders } from "./asset-folders.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CopyGraph } from "./types.js";
|
|
2
|
+
import type { SBAssetFolder } from "../assets/asset-folders.types.js";
|
|
3
|
+
import type { SBAsset } from "../assets/assets.types.js";
|
|
4
|
+
export declare const COPY_ASSETS_DRY_RUN_LIMITATIONS: string[];
|
|
5
|
+
export declare const normalizeAssetFolderParentId: (parentId: number | null | undefined) => number | null;
|
|
6
|
+
export declare const buildCopyAssetsGraph: ({ sourceSpaceId, targetSpaceId, assets, assetFolders, generatedAt, }: {
|
|
7
|
+
sourceSpaceId: string;
|
|
8
|
+
targetSpaceId: string;
|
|
9
|
+
assets: SBAsset[];
|
|
10
|
+
assetFolders: SBAssetFolder[];
|
|
11
|
+
generatedAt?: string;
|
|
12
|
+
}) => CopyGraph;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createCopyGraph } from "./graph.js";
|
|
2
|
+
export const COPY_ASSETS_DRY_RUN_LIMITATIONS = [
|
|
3
|
+
"target_conflicts_not_checked",
|
|
4
|
+
"target_asset_identity_not_resolved",
|
|
5
|
+
"manifests_not_written_in_dry_run",
|
|
6
|
+
];
|
|
7
|
+
export const normalizeAssetFolderParentId = (parentId) => (parentId === 0 || parentId == null ? null : parentId);
|
|
8
|
+
const getAssetFolderPath = (folder, folderById) => {
|
|
9
|
+
const names = [folder.name];
|
|
10
|
+
const visited = new Set([folder.id]);
|
|
11
|
+
let parentId = normalizeAssetFolderParentId(folder.parent_id);
|
|
12
|
+
while (parentId !== null) {
|
|
13
|
+
const parent = folderById.get(parentId);
|
|
14
|
+
if (!parent || visited.has(parent.id)) {
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
names.unshift(parent.name);
|
|
18
|
+
visited.add(parent.id);
|
|
19
|
+
parentId = normalizeAssetFolderParentId(parent.parent_id);
|
|
20
|
+
}
|
|
21
|
+
return names.join("/");
|
|
22
|
+
};
|
|
23
|
+
const getAssetFolderDepth = (folder, folderById) => {
|
|
24
|
+
let depth = 0;
|
|
25
|
+
const visited = new Set([folder.id]);
|
|
26
|
+
let parentId = normalizeAssetFolderParentId(folder.parent_id);
|
|
27
|
+
while (parentId !== null) {
|
|
28
|
+
const parent = folderById.get(parentId);
|
|
29
|
+
if (!parent || visited.has(parent.id)) {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
depth += 1;
|
|
33
|
+
visited.add(parent.id);
|
|
34
|
+
parentId = normalizeAssetFolderParentId(parent.parent_id);
|
|
35
|
+
}
|
|
36
|
+
return depth;
|
|
37
|
+
};
|
|
38
|
+
export const buildCopyAssetsGraph = ({ sourceSpaceId, targetSpaceId, assets, assetFolders, generatedAt, }) => {
|
|
39
|
+
const graph = createCopyGraph({
|
|
40
|
+
sourceSpaceId,
|
|
41
|
+
targetSpaceId,
|
|
42
|
+
generatedAt,
|
|
43
|
+
scope: {
|
|
44
|
+
command: "copy assets",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
graph.limitations.push(...COPY_ASSETS_DRY_RUN_LIMITATIONS);
|
|
48
|
+
const folderById = new Map(assetFolders.map((folder) => [folder.id, folder]));
|
|
49
|
+
const folderNodes = assetFolders
|
|
50
|
+
.map((folder) => ({
|
|
51
|
+
type: "asset_folder",
|
|
52
|
+
sourceId: folder.id,
|
|
53
|
+
sourcePath: getAssetFolderPath(folder, folderById),
|
|
54
|
+
targetPath: getAssetFolderPath(folder, folderById),
|
|
55
|
+
sourceParentId: normalizeAssetFolderParentId(folder.parent_id),
|
|
56
|
+
action: "create",
|
|
57
|
+
depth: getAssetFolderDepth(folder, folderById),
|
|
58
|
+
}))
|
|
59
|
+
.sort((left, right) => left.depth - right.depth || left.sourceId - right.sourceId)
|
|
60
|
+
.map(({ depth: _depth, ...node }) => node);
|
|
61
|
+
graph.assetFolders.push(...folderNodes);
|
|
62
|
+
for (const folder of assetFolders) {
|
|
63
|
+
const parentId = normalizeAssetFolderParentId(folder.parent_id);
|
|
64
|
+
if (parentId !== null && !folderById.has(parentId)) {
|
|
65
|
+
graph.warnings.push({
|
|
66
|
+
code: "asset_folder_parent_missing",
|
|
67
|
+
message: `Asset folder '${folder.name}' references parent id ${parentId}, but that parent was not returned by Storyblok.`,
|
|
68
|
+
path: getAssetFolderPath(folder, folderById),
|
|
69
|
+
sourceValue: parentId,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
graph.assets.push(...assets
|
|
74
|
+
.map((asset) => ({
|
|
75
|
+
type: "asset",
|
|
76
|
+
sourceId: asset.id,
|
|
77
|
+
sourceFilename: asset.filename,
|
|
78
|
+
targetFilename: asset.filename,
|
|
79
|
+
sourceAssetFolderId: asset.asset_folder_id ?? null,
|
|
80
|
+
action: "create",
|
|
81
|
+
}))
|
|
82
|
+
.sort((left, right) => left.sourceId - right.sourceId));
|
|
83
|
+
return graph;
|
|
84
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CopyGraph, CopyGraphSummary, CopyScope } from "./types.js";
|
|
2
|
+
export declare const createCopyGraph: ({ sourceSpaceId, targetSpaceId, scope, generatedAt, }: {
|
|
3
|
+
sourceSpaceId: string;
|
|
4
|
+
targetSpaceId: string;
|
|
5
|
+
scope: CopyScope;
|
|
6
|
+
generatedAt?: string;
|
|
7
|
+
}) => CopyGraph;
|
|
8
|
+
export declare const summarizeCopyGraph: (graph: CopyGraph) => CopyGraphSummary;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const createCopyGraph = ({ sourceSpaceId, targetSpaceId, scope, generatedAt = new Date().toISOString(), }) => ({
|
|
2
|
+
schemaVersion: 1,
|
|
3
|
+
sourceSpaceId,
|
|
4
|
+
targetSpaceId,
|
|
5
|
+
generatedAt,
|
|
6
|
+
scope,
|
|
7
|
+
stories: [],
|
|
8
|
+
assets: [],
|
|
9
|
+
assetFolders: [],
|
|
10
|
+
storyReferences: [],
|
|
11
|
+
assetReferences: [],
|
|
12
|
+
opaqueFields: [],
|
|
13
|
+
warnings: [],
|
|
14
|
+
errors: [],
|
|
15
|
+
limitations: [],
|
|
16
|
+
});
|
|
17
|
+
export const summarizeCopyGraph = (graph) => ({
|
|
18
|
+
stories: graph.stories.length,
|
|
19
|
+
assets: graph.assets.length,
|
|
20
|
+
assetFolders: graph.assetFolders.length,
|
|
21
|
+
storyReferences: graph.storyReferences.length,
|
|
22
|
+
assetReferences: graph.assetReferences.length,
|
|
23
|
+
opaqueFields: graph.opaqueFields.length,
|
|
24
|
+
warnings: graph.warnings.length,
|
|
25
|
+
errors: graph.errors.length,
|
|
26
|
+
limitations: graph.limitations.length,
|
|
27
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CopyManifestEntry, CopyMaps } from "./types.js";
|
|
2
|
+
export type CopyManifestPaths = {
|
|
3
|
+
rootDir: string;
|
|
4
|
+
combined: string;
|
|
5
|
+
stories: string;
|
|
6
|
+
assets: string;
|
|
7
|
+
assetFolders: string;
|
|
8
|
+
report: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const getDefaultCopyManifestPaths: ({ sourceSpaceId, targetSpaceId, rootDir, }: {
|
|
11
|
+
sourceSpaceId: string;
|
|
12
|
+
targetSpaceId: string;
|
|
13
|
+
rootDir?: string;
|
|
14
|
+
}) => CopyManifestPaths;
|
|
15
|
+
export declare const createEmptyCopyMaps: () => CopyMaps;
|
|
16
|
+
export declare const loadManifest: <T extends CopyManifestEntry>(filePath: string) => Promise<T[]>;
|
|
17
|
+
export declare const parseManifestJsonl: <T extends CopyManifestEntry>(content: string, filePath?: string) => T[];
|
|
18
|
+
export declare const appendManifestEntry: (filePath: string, entry: CopyManifestEntry) => Promise<void>;
|
|
19
|
+
export declare const appendManifestEntries: (filePath: string, entries: CopyManifestEntry[]) => Promise<void>;
|
|
20
|
+
export declare const writeManifest: (filePath: string, entries: CopyManifestEntry[]) => Promise<void>;
|
|
21
|
+
export declare const dedupeManifestEntries: <T extends CopyManifestEntry>(entries: T[]) => T[];
|
|
22
|
+
export declare const dedupeManifestFile: (filePath: string) => Promise<CopyManifestEntry[]>;
|
|
23
|
+
export declare const buildCopyMaps: (entries: CopyManifestEntry[]) => CopyMaps;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
export const getDefaultCopyManifestPaths = ({ sourceSpaceId, targetSpaceId, rootDir = ".sb-mig", }) => {
|
|
4
|
+
const copyRoot = path.join(rootDir, "copy", sourceSpaceId, targetSpaceId);
|
|
5
|
+
return {
|
|
6
|
+
rootDir: copyRoot,
|
|
7
|
+
combined: path.join(copyRoot, "manifest.jsonl"),
|
|
8
|
+
stories: path.join(copyRoot, "stories.manifest.jsonl"),
|
|
9
|
+
assets: path.join(copyRoot, "assets.manifest.jsonl"),
|
|
10
|
+
assetFolders: path.join(copyRoot, "asset-folders.manifest.jsonl"),
|
|
11
|
+
report: path.join(copyRoot, "report.json"),
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export const createEmptyCopyMaps = () => ({
|
|
15
|
+
storyIds: new Map(),
|
|
16
|
+
storyUuids: new Map(),
|
|
17
|
+
assetIds: new Map(),
|
|
18
|
+
assetFilenames: new Map(),
|
|
19
|
+
assetFolderIds: new Map(),
|
|
20
|
+
});
|
|
21
|
+
export const loadManifest = async (filePath) => {
|
|
22
|
+
try {
|
|
23
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
24
|
+
return parseManifestJsonl(content, filePath);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error?.code === "ENOENT") {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
export const parseManifestJsonl = (content, filePath = "manifest.jsonl") => content
|
|
34
|
+
.split("\n")
|
|
35
|
+
.map((line, index) => ({ line: line.trim(), lineNumber: index + 1 }))
|
|
36
|
+
.filter(({ line }) => line.length > 0)
|
|
37
|
+
.map(({ line, lineNumber }) => {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(line);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new Error(`Failed to parse manifest '${filePath}' at line ${lineNumber}: ${error.message}`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
export const appendManifestEntry = async (filePath, entry) => {
|
|
46
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
47
|
+
await fs.appendFile(filePath, `${JSON.stringify(entry)}\n`, "utf8");
|
|
48
|
+
};
|
|
49
|
+
export const appendManifestEntries = async (filePath, entries) => {
|
|
50
|
+
if (entries.length === 0) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
54
|
+
await fs.appendFile(filePath, entries.map((entry) => JSON.stringify(entry)).join("\n") + "\n", "utf8");
|
|
55
|
+
};
|
|
56
|
+
export const writeManifest = async (filePath, entries) => {
|
|
57
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
58
|
+
const content = entries.length > 0
|
|
59
|
+
? entries.map((entry) => JSON.stringify(entry)).join("\n") + "\n"
|
|
60
|
+
: "";
|
|
61
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
62
|
+
};
|
|
63
|
+
export const dedupeManifestEntries = (entries) => {
|
|
64
|
+
const byKey = new Map();
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
byKey.set(getManifestEntrySourceKey(entry), entry);
|
|
67
|
+
}
|
|
68
|
+
return Array.from(byKey.values());
|
|
69
|
+
};
|
|
70
|
+
export const dedupeManifestFile = async (filePath) => {
|
|
71
|
+
const entries = await loadManifest(filePath);
|
|
72
|
+
const deduped = dedupeManifestEntries(entries);
|
|
73
|
+
await writeManifest(filePath, deduped);
|
|
74
|
+
return deduped;
|
|
75
|
+
};
|
|
76
|
+
export const buildCopyMaps = (entries) => {
|
|
77
|
+
const maps = createEmptyCopyMaps();
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (isStoryManifestEntry(entry)) {
|
|
80
|
+
maps.storyIds.set(entry.source_id, entry.target_id);
|
|
81
|
+
maps.storyUuids.set(entry.source_uuid, entry.target_uuid);
|
|
82
|
+
}
|
|
83
|
+
if (isAssetManifestEntry(entry)) {
|
|
84
|
+
maps.assetIds.set(entry.source_id, {
|
|
85
|
+
id: entry.target_id,
|
|
86
|
+
filename: entry.target_filename,
|
|
87
|
+
});
|
|
88
|
+
maps.assetFilenames.set(entry.source_filename, entry.target_filename);
|
|
89
|
+
}
|
|
90
|
+
if (isAssetFolderManifestEntry(entry)) {
|
|
91
|
+
maps.assetFolderIds.set(entry.source_id, entry.target_id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return maps;
|
|
95
|
+
};
|
|
96
|
+
const getManifestEntrySourceKey = (entry) => {
|
|
97
|
+
if (isStoryManifestEntry(entry)) {
|
|
98
|
+
return [
|
|
99
|
+
entry.type,
|
|
100
|
+
entry.source_space_id,
|
|
101
|
+
entry.target_space_id,
|
|
102
|
+
entry.source_id,
|
|
103
|
+
entry.source_uuid,
|
|
104
|
+
].join(":");
|
|
105
|
+
}
|
|
106
|
+
return [
|
|
107
|
+
entry.type,
|
|
108
|
+
entry.source_space_id,
|
|
109
|
+
entry.target_space_id,
|
|
110
|
+
entry.source_id,
|
|
111
|
+
].join(":");
|
|
112
|
+
};
|
|
113
|
+
const isStoryManifestEntry = (entry) => entry.type === "story";
|
|
114
|
+
const isAssetManifestEntry = (entry) => entry.type === "asset";
|
|
115
|
+
const isAssetFolderManifestEntry = (entry) => entry.type === "asset_folder";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CopyComponentSchemaRegistry, CopyMaps, CopyRewriteResult } from "./types.js";
|
|
2
|
+
export declare const rewriteCopyReferences: <T>({ value, maps, schemas, }: {
|
|
3
|
+
value: T;
|
|
4
|
+
maps: CopyMaps;
|
|
5
|
+
schemas?: CopyComponentSchemaRegistry;
|
|
6
|
+
}) => CopyRewriteResult<T>;
|