contentful-utility-suite
Version:
Suite of utilities for Contentful CMS
358 lines (357 loc) • 12.1 kB
JavaScript
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,
};