diff --git a/__tests__/cmds/docs.test.js b/__tests__/cmds/docs.test.js index 846d36ed4..f25e78d37 100644 --- a/__tests__/cmds/docs.test.js +++ b/__tests__/cmds/docs.test.js @@ -682,6 +682,41 @@ describe('rdme docs:single', () => { postMock.done(); versionMock.done(); }); + + it('should fail if some other error when retrieving page slug', async () => { + const slug = 'fail-doc'; + + const errorObject = { + error: 'INTERNAL_ERROR', + message: 'Unknown error (yikes)', + suggestion: '...a suggestion to resolve the issue...', + help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', + }; + + const getMock = getNockWithVersionHeader(version) + .get(`/api/v1/docs/${slug}`) + .basicAuth({ user: key }) + .reply(500, errorObject); + + const versionMock = getApiNock() + .get(`/api/v1/version/${version}`) + .basicAuth({ user: key }) + .reply(200, { version }); + + const filepath = './__tests__/__fixtures__/failure-docs/fail-doc.md'; + + const formattedErrorObject = { + ...errorObject, + message: `Error uploading ${chalk.underline(`${filepath}`)}:\n\n${errorObject.message}`, + }; + + await expect(docsSingle.run({ filepath: `${filepath}`, key, version })).rejects.toStrictEqual( + new APIError(formattedErrorObject) + ); + + getMock.done(); + versionMock.done(); + }); }); describe('slug metadata', () => { diff --git a/src/cmds/docs/index.js b/src/cmds/docs/index.js index 62c7cb4b8..66f20616b 100644 --- a/src/cmds/docs/index.js +++ b/src/cmds/docs/index.js @@ -1,16 +1,11 @@ const chalk = require('chalk'); +const config = require('config'); const fs = require('fs'); const path = require('path'); -const config = require('config'); -const crypto = require('crypto'); -const frontMatter = require('gray-matter'); -const { promisify } = require('util'); + const { getProjectVersion } = require('../../lib/versionSelect'); -const fetch = require('../../lib/fetch'); -const { cleanHeaders, handleRes } = require('../../lib/fetch'); const { debug } = require('../../lib/logger'); - -const readFile = promisify(fs.readFile); +const pushDoc = require('../../lib/pushDoc'); module.exports = class DocsCommand { constructor() { @@ -88,98 +83,9 @@ module.exports = class DocsCommand { return Promise.reject(new Error(`We were unable to locate Markdown files in ${folder}.`)); } - function createDoc(slug, file, filename, hash, err) { - if (err.error !== 'DOC_NOTFOUND') return Promise.reject(err); - - if (dryRun) { - return `🎭 dry run! This will create '${slug}' with contents from ${filename} with the following metadata: ${JSON.stringify( - file.data - )}`; - } - - return fetch(`${config.get('host')}/api/v1/docs`, { - method: 'post', - headers: cleanHeaders(key, { - 'x-readme-version': selectedVersion, - 'Content-Type': 'application/json', - }), - body: JSON.stringify({ - slug, - body: file.content, - ...file.data, - lastUpdatedHash: hash, - }), - }) - .then(res => handleRes(res)) - .then(res => `🌱 successfully created '${res.slug}' with contents from ${filename}`); - } - - function updateDoc(slug, file, filename, hash, existingDoc) { - if (hash === existingDoc.lastUpdatedHash) { - return `${dryRun ? '🎭 dry run! ' : ''}\`${slug}\` ${ - dryRun ? 'will not be' : 'was not' - } updated because there were no changes.`; - } - - if (dryRun) { - return `🎭 dry run! This will update '${slug}' with contents from ${filename} with the following metadata: ${JSON.stringify( - file.data - )}`; - } - - return fetch(`${config.get('host')}/api/v1/docs/${slug}`, { - method: 'put', - headers: cleanHeaders(key, { - 'x-readme-version': selectedVersion, - 'Content-Type': 'application/json', - }), - body: JSON.stringify( - Object.assign(existingDoc, { - body: file.content, - ...file.data, - lastUpdatedHash: hash, - }) - ), - }) - .then(res => handleRes(res)) - .then(res => `✏️ successfully updated '${res.slug}' with contents from ${filename}`); - } - const updatedDocs = await Promise.all( files.map(async filename => { - debug(`reading file ${filename}`); - const file = await readFile(filename, 'utf8'); - const matter = frontMatter(file); - debug(`frontmatter for ${filename}: ${JSON.stringify(matter)}`); - - // Stripping the subdirectories and markdown extension from the filename and lowercasing to get the default slug. - const slug = matter.data.slug || path.basename(filename).replace(path.extname(filename), '').toLowerCase(); - const hash = crypto.createHash('sha1').update(file).digest('hex'); - - debug(`fetching data for ${slug}`); - - return fetch(`${config.get('host')}/api/v1/docs/${slug}`, { - method: 'get', - headers: cleanHeaders(key, { - 'x-readme-version': selectedVersion, - Accept: 'application/json', - }), - }) - .then(res => res.json()) - .then(res => { - debug(`GET /docs/:slug API response for ${slug}: ${JSON.stringify(res)}`); - if (res.error) { - debug(`error retrieving data for ${slug}, creating doc`); - return createDoc(slug, matter, filename, hash, res); - } - debug(`data received for ${slug}, updating doc`); - return updateDoc(slug, matter, filename, hash, res); - }) - .catch(err => { - // eslint-disable-next-line no-param-reassign - err.message = `Error uploading ${chalk.underline(filename)}:\n\n${err.message}`; - throw err; - }); + return pushDoc(key, selectedVersion, dryRun, filename); }) ); diff --git a/src/cmds/docs/single.js b/src/cmds/docs/single.js index f0d95fc29..a4747eb46 100644 --- a/src/cmds/docs/single.js +++ b/src/cmds/docs/single.js @@ -1,16 +1,9 @@ const chalk = require('chalk'); -const fs = require('fs'); -const path = require('path'); const config = require('config'); -const crypto = require('crypto'); -const frontMatter = require('gray-matter'); -const { promisify } = require('util'); -const { getProjectVersion } = require('../../lib/versionSelect'); -const fetch = require('../../lib/fetch'); -const { cleanHeaders, handleRes } = require('../../lib/fetch'); -const { debug } = require('../../lib/logger'); -const readFile = promisify(fs.readFile); +const { debug } = require('../../lib/logger'); +const { getProjectVersion } = require('../../lib/versionSelect'); +const pushDoc = require('../../lib/pushDoc'); module.exports = class SingleDocCommand { constructor() { @@ -69,95 +62,7 @@ module.exports = class SingleDocCommand { debug(`selectedVersion: ${selectedVersion}`); - debug(`reading file ${filepath}`); - const file = await readFile(filepath, 'utf8'); - const matter = frontMatter(file); - debug(`frontmatter for ${filepath}: ${JSON.stringify(matter)}`); - - // Stripping the subdirectories and markdown extension from the filename and lowercasing to get the default slug. - const slug = matter.data.slug || path.basename(filepath).replace(path.extname(filepath), '').toLowerCase(); - const hash = crypto.createHash('sha1').update(file).digest('hex'); - - function createDoc(err) { - if (err.error !== 'DOC_NOTFOUND') return Promise.reject(err); - - if (dryRun) { - return `🎭 dry run! This will create '${slug}' with contents from ${filepath} with the following metadata: ${JSON.stringify( - matter.data - )}`; - } - - return fetch(`${config.get('host')}/api/v1/docs`, { - method: 'post', - headers: cleanHeaders(key, { - 'x-readme-version': selectedVersion, - 'Content-Type': 'application/json', - }), - body: JSON.stringify({ - slug, - body: matter.content, - ...matter.data, - lastUpdatedHash: hash, - }), - }) - .then(res => handleRes(res)) - .then(res => `🌱 successfully created '${res.slug}' with contents from ${filepath}`); - } - - function updateDoc(existingDoc) { - if (hash === existingDoc.lastUpdatedHash) { - return `${dryRun ? '🎭 dry run! ' : ''}\`${slug}\` ${ - dryRun ? 'will not be' : 'was not' - } updated because there were no changes.`; - } - - if (dryRun) { - return `🎭 dry run! This will update '${slug}' with contents from ${filepath} with the following metadata: ${JSON.stringify( - matter.data - )}`; - } - - return fetch(`${config.get('host')}/api/v1/docs/${slug}`, { - method: 'put', - headers: cleanHeaders(key, { - 'x-readme-version': selectedVersion, - 'Content-Type': 'application/json', - }), - body: JSON.stringify( - Object.assign(existingDoc, { - body: matter.content, - ...matter.data, - lastUpdatedHash: hash, - }) - ), - }) - .then(res => handleRes(res)) - .then(res => `✏️ successfully updated '${res.slug}' with contents from ${filepath}`); - } - - debug(`creating doc for ${slug}`); - const createdDoc = await fetch(`${config.get('host')}/api/v1/docs/${slug}`, { - method: 'get', - headers: cleanHeaders(key, { - 'x-readme-version': selectedVersion, - Accept: 'application/json', - }), - }) - .then(res => res.json()) - .then(res => { - debug(`GET /docs/:slug API response for ${slug}: ${JSON.stringify(res)}`); - if (res.error) { - debug(`error retrieving data for ${slug}, creating doc`); - return createDoc(res); - } - debug(`data received for ${slug}, updating doc`); - return updateDoc(res); - }) - .catch(err => { - // eslint-disable-next-line no-param-reassign - err.message = `Error uploading ${chalk.underline(filepath)}:\n\n${err.message}`; - throw err; - }); + const createdDoc = await pushDoc(key, selectedVersion, dryRun, filepath); return chalk.green(createdDoc); } diff --git a/src/lib/fetch.js b/src/lib/fetch.js index b826ced58..3bab01792 100644 --- a/src/lib/fetch.js +++ b/src/lib/fetch.js @@ -55,7 +55,7 @@ module.exports.handleRes = async function handleRes(res) { const extension = mime.extension(contentType); if (extension === 'json') { const body = await res.json(); - debug(`received status code ${res.status} with JSON response: ${JSON.stringify(body)}`); + debug(`received status code ${res.status} from ${res.url} with JSON response: ${JSON.stringify(body)}`); if (body.error) { return Promise.reject(new APIError(body)); } @@ -64,7 +64,7 @@ module.exports.handleRes = async function handleRes(res) { // If we receive a non-JSON response, it's likely an error. // Let's debug the raw response body and throw it. const body = await res.text(); - debug(`received status code ${res.status} with non-JSON response: ${body}`); + debug(`received status code ${res.status} from ${res.url} with non-JSON response: ${body}`); return Promise.reject(body); }; diff --git a/src/lib/pushDoc.js b/src/lib/pushDoc.js new file mode 100644 index 000000000..7d309ea78 --- /dev/null +++ b/src/lib/pushDoc.js @@ -0,0 +1,113 @@ +const chalk = require('chalk'); +const config = require('config'); +const crypto = require('crypto'); +const fs = require('fs'); +const grayMatter = require('gray-matter'); +const path = require('path'); + +const APIError = require('./apiError'); +const { cleanHeaders, handleRes } = require('./fetch'); +const fetch = require('./fetch'); +const { debug } = require('./logger'); + +/** + * Reads the contents of the specified Markdown file + * and creates/updates the doc in ReadMe + * + * @param {String} key the project API key + * @param {String} selectedVersion the project version + * @param {Boolean} dryRun boolean indicating dry run mode + * @param {String} filepath path to the Markdown file + * (file extension must end in `.md` or `.markdown`) + * @returns {Promise} a string containing the result + */ +module.exports = async function pushDoc(key, selectedVersion, dryRun, filepath) { + debug(`reading file ${filepath}`); + const file = fs.readFileSync(filepath, 'utf8'); + const matter = grayMatter(file); + debug(`frontmatter for ${filepath}: ${JSON.stringify(matter)}`); + + // Stripping the subdirectories and markdown extension from the filename and lowercasing to get the default slug. + const slug = matter.data.slug || path.basename(filepath).replace(path.extname(filepath), '').toLowerCase(); + const hash = crypto.createHash('sha1').update(file).digest('hex'); + + function createDoc(err) { + if (err.error !== 'DOC_NOTFOUND') return Promise.reject(new APIError(err)); + + if (dryRun) { + return `🎭 dry run! This will create '${slug}' with contents from ${filepath} with the following metadata: ${JSON.stringify( + matter.data + )}`; + } + + return fetch(`${config.get('host')}/api/v1/docs`, { + method: 'post', + headers: cleanHeaders(key, { + 'x-readme-version': selectedVersion, + 'Content-Type': 'application/json', + }), + body: JSON.stringify({ + slug, + body: matter.content, + ...matter.data, + lastUpdatedHash: hash, + }), + }) + .then(res => handleRes(res)) + .then(res => `🌱 successfully created '${res.slug}' with contents from ${filepath}`); + } + + function updateDoc(existingDoc) { + if (hash === existingDoc.lastUpdatedHash) { + return `${dryRun ? '🎭 dry run! ' : ''}\`${slug}\` ${ + dryRun ? 'will not be' : 'was not' + } updated because there were no changes.`; + } + + if (dryRun) { + return `🎭 dry run! This will update '${slug}' with contents from ${filepath} with the following metadata: ${JSON.stringify( + matter.data + )}`; + } + + return fetch(`${config.get('host')}/api/v1/docs/${slug}`, { + method: 'put', + headers: cleanHeaders(key, { + 'x-readme-version': selectedVersion, + 'Content-Type': 'application/json', + }), + body: JSON.stringify( + Object.assign(existingDoc, { + body: matter.content, + ...matter.data, + lastUpdatedHash: hash, + }) + ), + }) + .then(res => handleRes(res)) + .then(res => `✏️ successfully updated '${res.slug}' with contents from ${filepath}`); + } + + return fetch(`${config.get('host')}/api/v1/docs/${slug}`, { + method: 'get', + headers: cleanHeaders(key, { + 'x-readme-version': selectedVersion, + Accept: 'application/json', + }), + }) + .then(res => res.json()) + .then(res => { + debug(`GET /docs/:slug API response for ${slug}: ${JSON.stringify(res)}`); + if (res.error) { + debug(`error retrieving data for ${slug}, creating doc`); + return createDoc(res); + } + debug(`data received for ${slug}, updating doc`); + return updateDoc(res); + }) + .catch(err => { + // eslint-disable-next-line no-param-reassign + err.message = `Error uploading ${chalk.underline(filepath)}:\n\n${err.message}`; + throw err; + }); +};