UNPKG

contentful-utility-suite

Version:
358 lines (357 loc) 12.1 kB
import { existsSync, mkdirSync, readFileSync, writeFileSync, promises as fsPromises } from "fs"; import { dirname } from "path"; import inquirer from "inquirer"; import { readdirSync, statSync } from "fs"; import { join } from "path"; import { GQL_FIELDS_DIR, GQL_OUTPUT_DIR } from "./constants.js"; import { ContentfulManagementAPI } from "./ContentfulManagementAPI.js"; import { renameSync } from "fs"; import { parse, format } from "path"; import chalk from "chalk"; const fileExistsSync = (filePath) => { return existsSync(filePath); }; const createFileSync = ({ content, filePath }) => { const dir = dirname(filePath); try { mkdirSync(dir, { recursive: true }); writeFileSync(filePath, content, { flag: "w" }); } catch (e) { return { errorMessage: e.message, error: true, }; } return { error: false, }; }; const yesNoPrompt = async ({ question, _default }) => { const { answer } = await inquirer.prompt([ { type: "confirm", name: "answer", message: question, default: _default, }, ]); return answer; }; export const choicesPrompt = async ({ message, choices }) => { const { selectedChoice } = await inquirer.prompt([ { type: "list", name: "selectedChoice", message, choices, }, ]); return selectedChoice; }; export const inputPrompt = async ({ message, defaultValue, validate }) => { const { userInput } = await inquirer.prompt([ { type: "input", name: "userInput", message, default: defaultValue, validate, }, ]); return userInput; }; const readFile = (filePath) => { return readFileSync(filePath, { encoding: "utf-8" }); }; const getMostRecentFileInDir = (directory) => { try { const files = readdirSync(directory); if (files.length === 0) { return { error: true, errorMessage: "Directory is empty", }; } const filesWithStats = files.map((file) => { const fullPath = join(directory, file); const stats = statSync(fullPath); return { file, fullPath, ctime: stats.ctime }; }); const mostRecentFile = filesWithStats.reduce((latest, current) => { return current.ctime > latest.ctime ? current : latest; }); return { error: false, res: mostRecentFile.fullPath, }; } catch (e) { return { error: true, errorMessage: e.message, }; } }; export const getValidGraphQLFieldFiles = async () => { mkdirSync(GQL_FIELDS_DIR, { recursive: true }); try { const files = readdirSync(GQL_FIELDS_DIR); const gqlFiles = files.filter((file) => file.endsWith(".json")); const promises = gqlFiles.map(async (file) => { const filePath = join(GQL_FIELDS_DIR, file); const content = await fsPromises.readFile(filePath, "utf-8"); const parsed = JSON.parse(content); if (!parsed.collectionsKey || !parsed.fields.length) { throw new Error("failed"); } return { ...parsed, fileName: file, }; }); const validFiles = (await Promise.allSettled(promises)) .filter((promise) => promise.status === "fulfilled") .map((promise) => promise.value); return { error: false, res: validFiles, }; } catch (error) { return { error: true, errorMessage: error.message, }; } }; const initGQLFieldsDir = () => { mkdirSync(GQL_FIELDS_DIR, { recursive: true }); const example = { collectionsKey: "pageCollection", fields: ["name", "slug"], }; try { writeFileSync(GQL_FIELDS_DIR + "example.json", JSON.stringify(example, null, 2)); } catch (e) { return { error: true, errorMessage: e.message, }; } return { error: false, }; }; async function fetchGraphQL({ space, query, envID }) { const res = await fetch(`https://graphql.contentful.com/content/v1/spaces/${space.spaceID}/environments/${envID}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${space.deliveryToken}`, }, body: JSON.stringify({ query }), }); return (await res.json()); } const selectEnvironmentIDs = async ({ space, selectOne, customMessage, }) => { const environmentDataRes = await ContentfulManagementAPI.getAllEnvironments(space); if (environmentDataRes.error) { return { error: true, errorMessage: `There was an error fetching environment data from Contentful: ${environmentDataRes.errorMessage}`, }; } const items = environmentDataRes.res?.items || []; if (items?.length < 2 && !selectOne) { return { error: true, errorMessage: `Your selected space must have at least 2 environments to use this command`, }; } const aliases = (items .map((item) => item.sys.aliases?.map((alias) => alias.sys.id)) .flat() .filter((alias) => !!alias) || []); const aliasChoices = Array.from(new Set(aliases)); const aliasMap = Array.from(new Map(items.flatMap((item) => [ ...(item.sys.aliases ?? []).map((alias) => [alias.sys.id, item.sys.id]), ...(item.sys.aliasedEnvironment ? [[item.sys.id, item.sys.aliasedEnvironment.sys.id]] : []), ])), ([alias, aliasedEnvironment]) => ({ alias, aliasedEnvironment })); const environments = Array.from(new Set(items.map((item) => item.name))); if (environments.length < 1) { return { error: true, errorMessage: `You have no available environments`, }; } if (environments.length === 1 && !selectOne) { return { error: true, errorMessage: `You need at least 2 environments`, }; } const environmentChoices = [...environments, ...aliasChoices]; const environmentChoicesWithAliasDisplayed = [...environments, ...aliasChoices]; const message = customMessage || "Select an environment"; const sourceEnvID = await Utils.choicesPrompt({ choices: environmentChoicesWithAliasDisplayed, message: selectOne ? message : "Select source environment", }); if (selectOne) { return { error: false, res: { id: sourceEnvID, }, }; } const indexOfSource = environmentChoicesWithAliasDisplayed.findIndex((env) => env === sourceEnvID); if (indexOfSource === -1) { throw new Error(`Unexpected error attempting to choose environments. This is most likely a code error.`); } const environmentOrAliasToRemove = environmentChoices[indexOfSource]; const choiceIsAlias = !!aliasMap.find((entry) => entry.alias === environmentOrAliasToRemove); const envToRemove = choiceIsAlias ? aliasMap.find((entry) => entry.alias === environmentOrAliasToRemove)?.aliasedEnvironment : environmentOrAliasToRemove; const choicesWithMatchingAliasedEnvRemoved = environmentChoices.filter((env) => { const pivotIsAlias = !!aliasMap.find((entry) => entry.alias === env); const pivotedEnvironment = pivotIsAlias ? aliasMap.find((entry) => entry.alias === env)?.aliasedEnvironment : env; if (pivotedEnvironment === undefined) { throw new Error(`Unexpected indexing alias. This is most likely a code error.`); } return pivotedEnvironment !== envToRemove; }); const targetEnvID = await Utils.choicesPrompt({ choices: choicesWithMatchingAliasedEnvRemoved, message: "Select target environment" }); return { error: false, res: { id: sourceEnvID, id2: targetEnvID, }, }; }; const writeGraphQLResponse = (content, collectionsKey) => { mkdirSync(GQL_OUTPUT_DIR, { recursive: true }); const epoch = Math.floor(Date.now()); const fileName = `${epoch}-${collectionsKey}.json`; const filePath = join(GQL_OUTPUT_DIR, fileName); const fileContent = JSON.stringify(content, null, 2); try { writeFileSync(filePath, fileContent, "utf-8"); } catch (e) { return { error: true, errorMessage: e.message, }; } return { error: false, res: filePath, }; }; const toKebabCase = (input) => { return (input ?.trim() .toLowerCase() .replace(/[^a-z0-9\s]/g, "") .replace(/\s+/g, "-") || ""); }; const renameToCjs = (path) => { const { dir, name } = parse(path); const newFilePath = format({ dir, name, ext: ".cjs" }); renameSync(path, newFilePath); return newFilePath; }; const testSpaceAuthorization = async ({ spaceID, managementToken, deliveryToken }) => { let possibleAuthIssue = false; console.log(chalk.yellow("Testing your information against Contentful...")); try { const managementRes = await fetch(`https://api.contentful.com/spaces/${spaceID}`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${managementToken}`, }, }); if (managementRes.status !== 200) { if (managementRes.status === 401) { possibleAuthIssue = true; console.log(chalk.red("Your management token test failed ❌")); } else { console.log(chalk.red(`Your space ID: ${spaceID} is (most likely) incorrect ❌`)); } throw new Error(`Management step failure`); } const deliveryRes = await fetch(`https://cdn.contentful.com/spaces/${spaceID}`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${deliveryToken}`, }, }); if (deliveryRes.status !== 200) { possibleAuthIssue = true; console.log(chalk.red("Your delivery token test failed ❌")); throw new Error(`Delivery step failure`); } console.log(chalk.green("All tests passed, your Space is configured properly ✅")); return { error: false, res: true, }; } catch (err) { if (possibleAuthIssue) { console.log(chalk.yellow(`Please check if your API keys have expired: https://app.contentful.com/spaces/${spaceID}/api/keys and https://app.contentful.com/spaces/${spaceID}/api/cma_tokens`)); } return { error: true, errorMessage: err.message, }; } }; const getAllJsonFilesInDir = (directory) => { try { const files = readdirSync(directory); const jsonFiles = files .map((file) => join(directory, file)) .filter((fullPath) => { const isFile = statSync(fullPath).isFile(); const isJson = fullPath.endsWith(".json"); return isFile && isJson; }); return { error: false, res: jsonFiles, }; } catch (e) { return { error: true, errorMessage: e.message, }; } }; export const Utils = { fileExistsSync, createFileSync, yesNoPrompt, choicesPrompt, inputPrompt, readFile, getMostRecentFileInDir, getValidGraphQLFieldFiles, fetchGraphQL, initGQLFieldsDir, selectEnvironmentIDs, writeGraphQLResponse, toKebabCase, renameToCjs, testSpaceAuthorization, getAllJsonFilesInDir, };