UNPKG

sb-mig

Version:

CLI to rule the world. (and handle stuff related to Storyblok CMS)

294 lines (293 loc) 13.8 kB
import { apiConfig } from "../cli/api-config.js"; import { compare, discover, discoverMany, discoverManyByPackageName, discoverStories, LOOKUP_TYPE, SCOPE, } from "../cli/utils/discover.js"; import { dumpToFile } from "../utils/files.js"; import Logger from "../utils/logger.js"; import { getFileContentWithRequire, getFilesContentWithRequire, isObjectEmpty, } from "../utils/main.js"; import { getAllAssets, migrateAsset } from "./assets/assets.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 { _uniqueValuesFrom } from "./utils/helper-functions.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); groupsToCheck.forEach(async (groupName) => { 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); }), ]); return []; }; 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) => { 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) => { 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); const groupsToCheck = _uniqueValuesFrom(specifiedComponentsContent .filter((component) => component.component_group_name) .map((component) => component.component_group_name)); await _checkAndPrepareGroups(groupsToCheck, config); // after checkAndPrepareGroups remoteComponents will have synced groups with local groups // updates of the groups had to happen before creation of them, cause creation/updates of components // happens async, so if one component will have the same group, as other one // it will be race of condition kinda issue - we will never now, if the group for current processed component // already exist or is being created by other request const remoteComponents = await managementApi.components.getAllComponents(config); const componentsToUpdate = []; const componentsToCreate = []; for (const component of specifiedComponentsContent) { if (!isObjectEmpty(component)) { const shouldBeUpdated = remoteComponents.find((remoteComponent) => component.name === remoteComponent.name); if (shouldBeUpdated) { componentsToUpdate.push({ id: shouldBeUpdated.id, ...component, }); } else { componentsToCreate.push(component); } } } const componentsGroups = await managementApi.components.getAllComponentsGroups(config); const groupsResolvedForComponentsToUpdate = componentsToUpdate.length > 0 && (await Promise.all(componentsToUpdate.map((component) => _resolveGroups(component, groupsToCheck, componentsGroups, config)))); Logger.log("Components to update after check: "); if (groupsResolvedForComponentsToUpdate) { await Promise.allSettled(groupsResolvedForComponentsToUpdate.map((component) => { Logger.warning(` ${component.name}`); return managementApi.components.updateComponent(component, presets, config); })); } const groupsResolvedForComponentToCreate = componentsToCreate.length > 0 && (await Promise.all(componentsToCreate.map((component) => _resolveGroups(component, groupsToCheck, componentsGroups, config)))); Logger.log("Components to create after check: "); if (groupsResolvedForComponentToCreate) { await Promise.allSettled(groupsResolvedForComponentToCreate.map((component) => { Logger.warning(` ${component.name}`); return managementApi.components.createComponent(component, presets, config); })); } }; export const syncAllComponents = async (presets, config) => { // #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); }; export const syncProvidedComponents = async (presets, components, packageName, config) => { 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); } 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); } }; export const syncAssets = async ({ transmission: { from, to }, syncDirection }, config) => { Logger.log(`We would try to migrate Assets data from: ${from} to: ${to}`); const allAssets = await getAllAssets({ spaceId: from }, config); allAssets.assets.map((asset) => { const { id, created_at, updated_at, ...newAssetPayload } = asset; migrateAsset({ migrateTo: to, payload: newAssetPayload, syncDirection, }, config); }); }; const syncStories = async ({ transmission: { from, to }, stories, toSpaceId }, 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}`); 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); }; export const syncContent = async ({ type, transmission, syncDirection, filename }, config) => { if (type === "stories") { if (syncDirection === "fromSpaceToFile") { 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, }, 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, }, config); } if (syncDirection === "fromAWSToSpace") { const data = await contentHubApi.getAllStories({ spaceId: transmission.from, storiesFilename: filename }, apiConfig); await syncStories({ transmission, stories: data.stories, toSpaceId: transmission.to, }, 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 }, 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) => { managementApi.components.updateComponent(component, false, apiConfig); })); } else { return false; } };