sb-mig
Version:
CLI to rule the world. (and handle stuff related to Storyblok CMS)
286 lines (285 loc) • 12.6 kB
JavaScript
import { apiConfig } from "../cli/api-config.js";
import { compare, discover, discoverMany, discoverManyByPackageName, discoverStories, LOOKUP_TYPE, SCOPE, } from "../cli/utils/discover.js";
import { dumpToFile, getFileContentWithRequire, getFilesContentWithRequire, } from "../utils/files.js";
import Logger from "../utils/logger.js";
import { getAllAssets, migrateAsset } from "./assets/assets.js";
import { syncComponentsData } from "./components/components.sync.js";
import { contentHubApi } from "./contentHubApi.js";
import { managementApi } from "./managementApi.js";
import { backupStories } from "./stories/backup.js";
import { getAllStories } from "./stories/stories.js";
import { createTree, traverseAndCreate } from "./stories/tree.js";
import { resolveGlobalTransformations } from "./utils/resolverTransformations.js";
const _checkAndPrepareGroups = async (groupsToCheck, config) => {
const componentsGroups = await managementApi.components.getAllComponentsGroups(config);
const groupExist = (groupName) => componentsGroups.find((group) => group.name === groupName);
for (const groupName of groupsToCheck) {
if (!groupExist(groupName)) {
await managementApi.components.createComponentsGroup(groupName, config);
}
}
};
export const removeAllComponents = async (config) => {
const components = await managementApi.components.getAllComponents(config);
const component_groups = await managementApi.components.getAllComponentsGroups(config);
return Promise.all([
...components.map(async (component) => {
await managementApi.components.removeComponent(component, config);
}),
...component_groups.map(async (componentGroup) => {
await managementApi.components.removeComponentGroup(componentGroup, config);
}),
]);
};
export const removeSpecifiedComponents = async (components, config) => {
const remoteComponents = await managementApi.components.getAllComponents(config);
const componentsToRemove = [];
components.map((component) => {
const shouldBeRemoved = remoteComponents.find((remoteComponent) => component === remoteComponent.name);
shouldBeRemoved && componentsToRemove.push(shouldBeRemoved);
});
return (componentsToRemove.length > 0 &&
Promise.all(componentsToRemove.map((component) => {
return managementApi.components.removeComponent(component, config);
})));
};
const _resolveGroups = async (component, existedGroups, remoteComponentsGroups) => {
if (!component.component_group_name) {
return { ...component, component_group_uuid: null };
}
const componentsGroup = existedGroups.find((group) => component.component_group_name === group);
if (componentsGroup) {
const component_group_uuid = remoteComponentsGroups.find((remoteComponentsGroup) => remoteComponentsGroup.name === componentsGroup).uuid;
return { ...component, component_group_uuid };
}
};
export const syncComponents = async (specifiedComponents, presets, config, options = {}) => {
Logger.log("sync2Components: ");
let specifiedComponentsContent = await Promise.all(specifiedComponents.map((component) => {
return getFileContentWithRequire({ file: component.p });
}));
/**
* Resolve all the global transformations you find
* - storyblok.config file resolvers
* - .sb.resolvers.ts files resolvers
*/
specifiedComponentsContent = await resolveGlobalTransformations(specifiedComponentsContent);
await syncComponentsData({
components: specifiedComponentsContent,
presets,
ssot: options.ssot,
dryRun: options.dryRun,
}, config);
};
export const syncAllComponents = async (presets, config, options = {}) => {
// #1: discover all external .sb.js files
const allLocalSbComponentsSchemaFiles = await discover({
scope: SCOPE.local,
type: LOOKUP_TYPE.fileName,
});
// #2: discover all local .sb.js files
const allExternalSbComponentsSchemaFiles = await discover({
scope: SCOPE.external,
type: LOOKUP_TYPE.fileName,
});
// // #3: compare results, prefare local ones (so we have to create final external paths array and local array of things to sync from where)
const { local, external } = compare({
local: allLocalSbComponentsSchemaFiles,
external: allExternalSbComponentsSchemaFiles,
});
// #4: sync - do all stuff already done (groups resolving, and so on)
return await syncComponents([...local, ...external], presets, config, {
dryRun: options.dryRun,
ssot: options.ssot,
});
};
export const syncProvidedComponents = async (presets, components, packageName, config, options = {}) => {
if (!packageName) {
// #1: discover all external .sb.js files
const allLocalSbComponentsSchemaFiles = await discoverMany({
scope: SCOPE.local,
type: LOOKUP_TYPE.fileName,
fileNames: components,
});
// #2: discover all local .sb.js files
const allExternalSbComponentsSchemaFiles = await discoverMany({
scope: SCOPE.external,
type: LOOKUP_TYPE.fileName,
fileNames: components,
});
// #3: compare results, prefer local ones (so we have to create final external paths array and local array of things to sync from where)
const { local, external } = compare({
local: allLocalSbComponentsSchemaFiles,
external: allExternalSbComponentsSchemaFiles,
});
// #4: sync - do all stuff already done (groups resolving, and so on)
return await syncComponents([...local, ...external], presets, config, {
dryRun: options.dryRun,
});
}
else {
// implement discovering and syncrhonizing with packageName
// #1: discover all external .sb.js files
const allLocalSbComponentsSchemaFiles = discoverManyByPackageName({
scope: SCOPE.local,
packageNames: components,
});
// #2: discover all local .sb.js files
const allExternalSbComponentsSchemaFiles = discoverManyByPackageName({
scope: SCOPE.external,
packageNames: components,
});
// #3: compare results, prefer local ones (so we have to create final external paths array and local array of things to sync from where)
const { local, external } = compare({
local: allLocalSbComponentsSchemaFiles,
external: allExternalSbComponentsSchemaFiles,
});
// #4: sync - do all stuff already done (groups resolving, and so on)
return syncComponents([...local, ...external], presets, config, {
dryRun: options.dryRun,
});
}
};
export const syncAssets = async ({ transmission: { from, to }, syncDirection, dryRun }, config) => {
Logger.log(`We would try to migrate Assets data from: ${from} to: ${to}`);
const allAssets = await getAllAssets({ spaceId: from }, config);
const assets = Array.isArray(allAssets?.assets) ? allAssets.assets : [];
if (dryRun) {
Logger.warning(`[dry-run] Would sync ${assets.length} assets from ${from} to ${to}.`);
return true;
}
await Promise.all(assets.map((asset) => {
const { id, created_at, updated_at, ...newAssetPayload } = asset;
return migrateAsset({
migrateTo: to,
payload: newAssetPayload,
syncDirection,
}, config);
}));
return true;
};
const syncStories = async ({ transmission: { from, to }, stories, toSpaceId, dryRun }, config) => {
Logger.log(`We would try to migrate Stories data from: ${from} to: ${to}`);
const storiesToPass = stories
.map((item) => item.story)
.map((item) => item.parent_id === 0 ? { ...item, parent_id: null } : item);
Logger.warning(`Amount of all stories to migrate: ${storiesToPass.length}`);
if (dryRun) {
Logger.warning(`[dry-run] Would sync ${storiesToPass.length} stories from ${from} to ${to}.`);
return true;
}
const storiesToPassJson = JSON.stringify(storiesToPass, null, 2);
if (config.debug) {
dumpToFile("storiesToPass.json", storiesToPassJson);
}
const tree = createTree(storiesToPass);
const jsonString = JSON.stringify(tree, null, 2);
if (config.debug) {
dumpToFile("tree.json", jsonString);
}
await traverseAndCreate({ tree, realParentId: null, spaceId: toSpaceId }, config);
return true;
};
export const syncContent = async ({ type, transmission, syncDirection, filename, dryRun }, config) => {
if (dryRun) {
Logger.warning("[dry-run] Content sync will only read source data and report planned changes.");
}
if (type === "stories") {
if (syncDirection === "fromSpaceToFile") {
if (dryRun) {
const stories = await getAllStories({}, {
...config,
spaceId: transmission.from,
sbApi: config.sbApi,
});
Logger.warning(`[dry-run] Would back up ${stories.length} stories from ${transmission.from} to '${transmission.to}'.`);
return true;
}
await backupStories({
filename: transmission.to,
suffix: ".sb.stories",
spaceId: transmission.from,
}, config);
}
if (syncDirection === "fromSpaceToSpace") {
const stories = await getAllStories({}, {
...config,
spaceId: transmission.from,
sbApi: config.sbApi,
});
await syncStories({
transmission,
stories,
toSpaceId: transmission.to,
dryRun,
}, config);
}
if (syncDirection === "fromFileToSpace") {
const allLocalStories = discoverStories({
scope: SCOPE.local,
type: LOOKUP_TYPE.fileName,
fileNames: [transmission.from],
});
const storiesFileContent = getFilesContentWithRequire({
files: allLocalStories,
});
await syncStories({
transmission,
stories: storiesFileContent[0],
toSpaceId: transmission.to,
dryRun,
}, config);
}
if (syncDirection === "fromAWSToSpace") {
const data = await contentHubApi.getAllStories({ spaceId: transmission.from, storiesFilename: filename }, apiConfig);
await syncStories({
transmission,
stories: data.stories,
toSpaceId: transmission.to,
dryRun,
}, config);
}
return true;
}
else if (type === "assets") {
if (syncDirection === "fromFileToSpace") {
Logger.warning(`${syncDirection} with ${type} This is not implemented yet!`);
}
else if (syncDirection === "fromSpaceToSpace" ||
syncDirection === "fromSpaceToFile") {
await syncAssets({ transmission, syncDirection, dryRun }, config);
}
else {
Logger.warning(`${syncDirection} with ${type} This is not implemented yet!`);
}
return true;
}
else {
throw Error("This should never happen!");
}
};
const setToDefaultPreset = (allPresets) => {
return allPresets.find((preset) => preset.name === "Default");
};
export const setComponentDefaultPreset = async ({ presets, componentsToSync, apiConfig, }) => {
if (presets) {
Logger.warning("Setting default presets for components...");
const remoteComponents = await managementApi.components.getAllComponents(apiConfig);
const filteredRemoteComponents = componentsToSync
? remoteComponents.filter((component) => componentsToSync.includes(component.name))
: remoteComponents;
const finalRemoteComponents = filteredRemoteComponents.map((component) => {
return {
...component,
preset_id: setToDefaultPreset(component.all_presets)?.id,
};
});
Logger.warning("Updating componet with default presets...");
return Promise.all(finalRemoteComponents.map((component) => {
return managementApi.components.updateComponent(component, false, apiConfig);
}));
}
else {
return false;
}
};