UNPKG

sb-mig

Version:

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

467 lines (466 loc) 18.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.deepUpsertStory = exports.upsertStory = exports.updateStories = exports.publishStoryLanguages = exports.updateStory = exports.createStory = exports.getStoryBySlug = exports.getStoryById = exports.getAllStories = exports.removeAllStories = exports.removeStory = exports.resolvePublishLanguageCodes = exports.parsePublishLanguagesOption = exports.resolveStoryPublishState = void 0; const chalk_1 = __importDefault(require("chalk")); const async_utils_js_1 = require("../../utils/async-utils.js"); const logger_js_1 = __importDefault(require("../../utils/logger.js")); const object_utils_js_1 = require("../../utils/object-utils.js"); const request_js_1 = require("../utils/request.js"); const resolveStoryLabel = (content, storyId) => content?.full_slug || content?.slug || content?.name || String(storyId); const DEFAULT_PUBLISH_LANGUAGE = "[default]"; const STORY_CONTENT_FETCH_CONCURRENCY = 10; const isSkippedStoryPublishState = (publishState) => publishState?.shouldPublish === false; const isDefaultLanguageToken = (language) => language.toLowerCase() === "default" || language === DEFAULT_PUBLISH_LANGUAGE; const normalizePublishLanguageCodes = (languages) => { const normalized = languages.map((language) => { const trimmed = language.trim(); return isDefaultLanguageToken(trimmed) ? DEFAULT_PUBLISH_LANGUAGE : trimmed; }); const cleanLanguages = normalized.filter((language) => language.length > 0); if (cleanLanguages.length === 0) { throw new Error("Publish languages cannot be empty."); } return Array.from(new Set(cleanLanguages)); }; const resolveStoryPublishState = (story) => { if (story?.published !== true) { return { status: "draft", shouldPublish: false, skipReason: "source_story_draft", message: "source story was draft-only", }; } if (story.unpublished_changes === true) { return { status: "published_with_unpublished_changes", shouldPublish: false, skipReason: "source_story_has_unpublished_changes", message: "source story had unpublished draft changes", }; } if (story.unpublished_changes !== false) { return { status: "published_unknown", shouldPublish: false, skipReason: "source_story_publish_state_unknown", message: "source story publish state was missing unpublished_changes", }; } return { status: "published_clean", shouldPublish: true, }; }; exports.resolveStoryPublishState = resolveStoryPublishState; const withSkippedPublish = ({ updateResult, story, storyId, spaceId, publishLanguages, publishState, }) => { const storyLabel = resolveStoryLabel(story, String(storyId)); logger_js_1.default.warning(`Skipping publish for story '${storyLabel}' in space '${spaceId}' because ${publishState.message}.`); return { ...updateResult, sourcePublishState: publishState.status, publishSkippedReason: publishState.skipReason, ...(publishLanguages ? { publishLanguages } : {}), }; }; const parsePublishLanguagesOption = (publishLanguages) => { if (!publishLanguages) { return "default"; } const trimmed = publishLanguages.trim(); if (trimmed.length === 0) { return "default"; } if (trimmed.toLowerCase() === "all") { return "all"; } const languages = normalizePublishLanguageCodes(trimmed.split(",")); if (languages.length === 1 && languages[0] === DEFAULT_PUBLISH_LANGUAGE) { return "default"; } return languages; }; exports.parsePublishLanguagesOption = parsePublishLanguagesOption; const readSpaceLanguageCodes = (spaceResponse) => { const space = spaceResponse?.data?.space || spaceResponse?.space || {}; const languageSources = [ space.languages, space.language_codes, space.options?.languages, ]; for (const source of languageSources) { if (!Array.isArray(source)) { continue; } return source .map((language) => { if (typeof language === "string") { return language; } if (typeof language?.code === "string") { return language.code; } return ""; }) .filter((language) => language.trim().length > 0); } return []; }; const resolvePublishLanguageCodes = async (publishLanguages, config) => { if (!publishLanguages || publishLanguages === "default") { return [DEFAULT_PUBLISH_LANGUAGE]; } if (Array.isArray(publishLanguages)) { return normalizePublishLanguageCodes(publishLanguages); } const spaceResponse = await config.sbApi.get(`spaces/${config.spaceId}`); const spaceLanguageCodes = readSpaceLanguageCodes(spaceResponse); if (spaceLanguageCodes.length === 0) { logger_js_1.default.warning(`No configured Storyblok languages were found for space '${config.spaceId}'. Publishing only ${DEFAULT_PUBLISH_LANGUAGE}.`); } return normalizePublishLanguageCodes([ DEFAULT_PUBLISH_LANGUAGE, ...spaceLanguageCodes, ]); }; exports.resolvePublishLanguageCodes = resolvePublishLanguageCodes; const resolveStoryblokErrorResponse = (err) => { if (typeof err?.response === "string" && err.response.trim().length > 0) { return err.response.trim(); } if (typeof err?.response?.data === "string" && err.response.data.trim().length > 0) { return err.response.data.trim(); } if (typeof err?.response?.message === "string" && err.response.message.trim().length > 0) { return err.response.message.trim(); } if (typeof err?.message === "string" && err.message.trim().length > 0) { return err.message.trim(); } return undefined; }; const removeStory = (args, config) => { const { storyId } = args; const { spaceId, sbApi } = config; logger_js_1.default.log(`Removing ${storyId} from ${spaceId}`); return sbApi .delete(`spaces/${spaceId}/stories/${storyId}`, {}) .then((res) => { logger_js_1.default.success(`Successfully removed: ${res.data.story.name} with ${res.data.story.id} id.`); return res; }) .catch(() => { logger_js_1.default.error(`Unable to remove: ${storyId}`); }); }; exports.removeStory = removeStory; const removeAllStories = async (config) => { const { spaceId } = config; logger_js_1.default.warning(`Trying to remove all stories from space with spaceId: ${spaceId}`); const stories = await (0, exports.getAllStories)({}, config); const onlyRootStories = (story) => story.story.parent_id === 0 || story.story.parent_id === null; const allResponses = Promise.all(stories .filter(onlyRootStories) .map(async (story) => await (0, exports.removeStory)({ storyId: story?.story?.id }, config))); return allResponses; }; exports.removeAllStories = removeAllStories; // GET const getAllStories = async (args, config) => { const { options } = args; const { spaceId, sbApi } = config; logger_js_1.default.log(`Trying to get all Stories from: ${spaceId}`); const params = (0, object_utils_js_1.notNullish)({ with_slug: options?.with_slug, starts_with: options?.starts_with, language: options?.language, }); console.log("These are params i will use: "); console.log(params); const allStoriesWithoutContent = await (0, request_js_1.getAllItemsWithPagination)({ apiFn: ({ per_page, page }) => sbApi.get(`spaces/${spaceId}/stories/`, { ...params, per_page, page, }), params: { spaceId, }, itemsKey: "stories", }); logger_js_1.default.success(`Successfully pre-fetched ${allStoriesWithoutContent.length} stories.`); let heartBeat = 0; const allStories = await (0, async_utils_js_1.mapWithConcurrency)(allStoriesWithoutContent, STORY_CONTENT_FETCH_CONCURRENCY, async (story) => { const result = await (0, exports.getStoryById)(story.id, config); heartBeat++; if (heartBeat % 10 === 0 || heartBeat === allStoriesWithoutContent.length) { logger_js_1.default.success(`Successfully fetched ${heartBeat} stories with full content.`); } return result; }); return allStories; }; exports.getAllStories = getAllStories; // GET const getStoryById = (storyId, config) => { const { spaceId, sbApi, debug } = config; if (debug) { console.log(`Trying to get Story with id: ${storyId} from space: ${spaceId}, to fill content field.`); } return sbApi .get(`spaces/${spaceId}/stories/${storyId}`) .then((res) => { if (debug) { logger_js_1.default.success(`Successfuly fetched story with content, with id: ${storyId} from space: ${spaceId}.`); } return res.data; }) .catch((err) => logger_js_1.default.error(err)); }; exports.getStoryById = getStoryById; const getStoryBySlug = async (slug, config) => { const { spaceId, sbApi } = config; const storiesWithoutContent = await sbApi .get(`spaces/${spaceId}/stories/`, { per_page: 100, // @ts-ignore with_slug: slug, }) .then((res) => res.data.stories) .catch((err) => console.error(err)); const storiesWithContent = await Promise.all(storiesWithoutContent.map(async (story) => await (0, exports.getStoryById)(story.id, config))); return storiesWithContent[0]; }; exports.getStoryBySlug = getStoryBySlug; // CREATE const createStory = (content, config) => { const { spaceId, sbApi } = config; logger_js_1.default.log(`Creating story with name: ${content.name} in space: ${spaceId}`); return sbApi .post(`spaces/${spaceId}/stories/`, { story: content, publish: true, }) .then((res) => res.data) .catch((err) => console.error(err)); }; exports.createStory = createStory; // UPDATE const updateStory = (content, storyId, options, config) => { const { spaceId, sbApi } = config; const storyLabel = resolveStoryLabel(content, storyId); logger_js_1.default.warning("Trying to update Story..."); logger_js_1.default.log(`Updating story with name: ${content.name} in space: ${spaceId}`); // console.log("THis is content to update: "); // console.log(JSON.stringify(content, null, 2)); return sbApi .put(`spaces/${spaceId}/stories/${storyId}`, { story: content, publish: options.publish === true, force_update: options.force_update === true, }) .then((res) => { console.log(`${chalk_1.default.green(res.data.story.full_slug)} updated.`); return { ok: true, stage: "update", id: res.data.story.id, name: res.data.story.name, slug: res.data.story.full_slug, data: res.data, }; }) .catch((err) => { const status = err?.status || err?.response?.status; const responseMessage = resolveStoryblokErrorResponse(err); const statusLabel = status ? `status ${status}` : "unknown status"; const responseLabel = responseMessage ? ` Response: ${responseMessage}` : ""; logger_js_1.default.error(`Failed to update story '${storyLabel}' in space '${spaceId}' (${statusLabel}).${responseLabel}`); return { ok: false, stage: "update", id: storyId, name: content?.name, slug: content?.full_slug || content?.slug, spaceId, status, response: responseMessage, error: err, }; }); }; exports.updateStory = updateStory; const publishStoryLanguages = async ({ storyId, story, languages, }, config) => { const { spaceId, sbApi } = config; const lang = languages.join(","); const storyLabel = resolveStoryLabel(story, String(storyId)); logger_js_1.default.log(`Publishing story '${storyLabel}' in space '${spaceId}' for languages: ${lang}`); return sbApi .get(`spaces/${spaceId}/stories/${storyId}/publish`, { lang }) .then((res) => { const publishedStory = res?.data?.story || story || {}; logger_js_1.default.success(`Published story '${storyLabel}' for languages: ${lang}`); return { ok: true, stage: "publish", id: publishedStory.id || storyId, name: publishedStory.name || story?.name, slug: publishedStory.full_slug || publishedStory.slug || story?.full_slug || story?.slug, spaceId, publishLanguages: languages, data: res?.data, }; }) .catch((err) => { const status = err?.status || err?.response?.status; const responseMessage = resolveStoryblokErrorResponse(err); const statusLabel = status ? `status ${status}` : "unknown status"; const responseLabel = responseMessage ? ` Response: ${responseMessage}` : ""; logger_js_1.default.error(`Failed to publish story '${storyLabel}' in space '${spaceId}' (${statusLabel}).${responseLabel}`); return { ok: false, stage: "publish", id: storyId, name: story?.name, slug: story?.full_slug || story?.slug, spaceId, status, response: responseMessage, publishLanguages: languages, error: err, }; }); }; exports.publishStoryLanguages = publishStoryLanguages; const updateStories = async (args, config) => { const { stories, options, spaceId } = args; const shouldPublishLanguages = options.publish && options.publishLanguages !== undefined; const shouldPreservePublishState = Boolean(options.preservePublishState); const publishLanguages = shouldPublishLanguages ? await (0, exports.resolvePublishLanguageCodes)(options.publishLanguages, { ...config, spaceId, }) : undefined; return Promise.allSettled( // Run through stories, and update the space with migrated version of stories stories.map(async (stories) => { const story = stories.story; const publishState = shouldPreservePublishState ? (0, exports.resolveStoryPublishState)(story) : undefined; const shouldPublishStory = options.publish && (!publishState || publishState.shouldPublish); const updateResult = await (0, exports.updateStory)(story, story.id, { publish: shouldPublishStory && !shouldPublishLanguages, force_update: options.force_update, }, { ...config, spaceId }); if (options.publish && updateResult?.ok && isSkippedStoryPublishState(publishState)) { return withSkippedPublish({ updateResult, story, storyId: story.id, spaceId, publishLanguages, publishState, }); } if (!shouldPublishLanguages || !publishLanguages || !updateResult?.ok) { return updateResult; } return (0, exports.publishStoryLanguages)({ storyId: story.id, story, languages: publishLanguages, }, { ...config, spaceId }); })); }; exports.updateStories = updateStories; const upsertStory = async (args, config) => { console.log("Modifying story... in space with id:"); console.log(config.spaceId); const { storyId, storySlug, content } = args; console.log("This are args passed: "); console.log(args); if (storyId) { // if this exist than we update story with this id console.log("You've selected storyid!"); } else if (storySlug) { // if this exist than we update story with this slug (probably when we try to add story from one space to another, console.log("You've selected slug!"); const foundStory = await (0, exports.getStoryBySlug)(storySlug, config); console.log("This is story"); console.log(foundStory); if (foundStory) { // then update the story } else { const { story: { parent_id, id, parent, ...rest }, } = content; console.log("We are going to create story"); const response = await (0, exports.createStory)(rest, config); console.log("This is response"); console.log(response); } } else { // if this exist than we create new story console.log("Nothing passed, creating story..."); } }; exports.upsertStory = upsertStory; const deepUpsertStory = async (args, config) => { console.log("Modifying story... in space with id:"); console.log(config.spaceId); const { storyId, storySlug, content } = args; console.log("This are args passed: "); console.log(args); if (storyId) { // if this exist than we update story with this id console.log("You've selected storyid!"); } else if (storySlug) { // if this exist than we update story with this slug (probably when we try to add story from one space to another, console.log("You've selected slug!"); const slugs = storySlug.split("/"); console.log("Slugs for which we need to check for existence of stories"); console.log(slugs); // const foundStory = await managementApi.stories.getStoryBySlug(storySlug, config) // console.log("This is story") // console.log(foundStory) // if(foundStory) { // // then update the story // } else { // const {story: {parent_id, id, parent,...rest}} = content // console.log("We are going to create story") // const response = await managementApi.stories.createStory(rest, config) // console.log("This is response") // console.log(response) // } } else { // if this exist than we create new story console.log("Nothing passed, creating story..."); } }; exports.deepUpsertStory = deepUpsertStory;