Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(docs): adds ability to delete docs from ReadMe if they're no longer in local folder #581

Merged
merged 30 commits into from
Sep 12, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0873fef
feat(openapi): Add `updateSingleSpec` option to automatically update …
shaiarmis Aug 23, 2022
8d44028
feat(docs): Updated docs
shaiarmis Aug 23, 2022
f0da91b
Fix eslint errors
shaiarmis Aug 23, 2022
60e1513
Throw error if using --updateSingleSpec when there are multiple spec …
shaiarmis Aug 23, 2022
8e83532
Show a warning when passing both `--updateSingleSpec` and `--id`
shaiarmis Aug 23, 2022
e1e9f92
Update tests snapshot
shaiarmis Aug 23, 2022
b68e0ed
docs: copy edits, rename flag to `--update`
kanadgupta Aug 23, 2022
bba9448
fix: add logic for if create and update flags passed together
kanadgupta Aug 23, 2022
95a8148
ci: attempt to skip failing step if API key isn't present
kanadgupta Aug 23, 2022
8ca1596
Revert "ci: attempt to skip failing step if API key isn't present"
kanadgupta Aug 23, 2022
1a2658b
Merge branch 'main' into pr/579
kanadgupta Aug 23, 2022
0f9730f
Merge branch 'readmeio:main' into main
shaiarmis Aug 24, 2022
3f76bca
Add --deleteMissing option to `rdme docs` command to delete from Read…
shaiarmis Aug 24, 2022
0292a09
Take into account only "guide" categories
shaiarmis Aug 24, 2022
daa9c17
chore: markdown formatting
kanadgupta Aug 24, 2022
276f6f0
Add warning about what the new option does
shaiarmis Aug 25, 2022
082d4c1
Improved typing of functions
shaiarmis Aug 25, 2022
2b31260
Move "if" to a more proper location
shaiarmis Aug 25, 2022
8b1ba91
Refactor "getSlug" to "readDoc" and reuse code
shaiarmis Aug 25, 2022
e174386
Removed unused code
shaiarmis Aug 25, 2022
4145c99
Simplify code
shaiarmis Aug 25, 2022
e5c6eac
Refactored tests to use a folder with a document that should not be d…
shaiarmis Aug 25, 2022
27d3fa0
Merge remote-tracking branch 'origin/add-delete-missing-option' into …
shaiarmis Aug 25, 2022
637a7af
Added a confirmation prompt when folder is empty
shaiarmis Aug 25, 2022
771ccf2
Rename --deleteMissing to --cleanup
shaiarmis Aug 30, 2022
d679349
Merge branch 'main' into add-delete-missing-option
shaiarmis Sep 7, 2022
bd51962
CR
shaiarmis Sep 8, 2022
ac79f09
Merge branch 'main' into add-delete-missing-option
shaiarmis Sep 8, 2022
5af81ee
Undo accidental commit
shaiarmis Sep 8, 2022
46e2323
Update index.ts
kanadgupta Sep 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ rdme docs path-to-markdown-files --version={project-version}

This command also has a dry run mode, which can be useful for initial setup and debugging. You can read more about dry run mode [in our docs](https://docs.readme.com/docs/rdme#dry-run-mode).

#### Deleting missing documents

If you wish to delete documents from ReadMe that are no longer present in the directory, pass the `--deleteMissing` option to the command.
kanadgupta marked this conversation as resolved.
Show resolved Hide resolved

#### Edit a Single ReadMe Doc on Your Local Machine

```sh
Expand Down
57 changes: 57 additions & 0 deletions __tests__/cmds/docs/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,63 @@ describe('rdme docs', () => {
});
});

describe('delete docs', () => {
it('should delete doc if file is missing and --deleteMissing option is used', async () => {
const versionMock = getAPIMock()
.get(`/api/v1/version/${version}`)
.basicAuth({ user: key })
.reply(200, { version });
const apiMocks = getAPIMockWithVersionHeader(version)
.get('/api/v1/categories?perPage=20&page=1')
.basicAuth({ user: key })
.reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' })
.get('/api/v1/categories/category1/docs')
.basicAuth({ user: key })
.reply(200, [{ slug: 'thisDocShouldBeMissingInFolder' }])
.delete('/api/v1/docs/thisDocShouldBeMissingInFolder')
.basicAuth({ user: key })
.reply(204, '');
const folderWithoutDocs = './__tests__/__fixtures__/ref-oas';
kanadgupta marked this conversation as resolved.
Show resolved Hide resolved
await expect(
docs.run({
folder: folderWithoutDocs,
key,
version,
deleteMissing: true,
})
).resolves.toBe('successfully deleted `thisDocShouldBeMissingInFolder`');

apiMocks.done();
versionMock.done();
});

it('should return doc delete info for dry run', async () => {
const versionMock = getAPIMock()
.get(`/api/v1/version/${version}`)
.basicAuth({ user: key })
.reply(200, { version });
const apiMocks = getAPIMockWithVersionHeader(version)
.get('/api/v1/categories?perPage=20&page=1')
.basicAuth({ user: key })
.reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' })
.get('/api/v1/categories/category1/docs')
.basicAuth({ user: key })
.reply(200, [{ slug: 'thisDocShouldBeMissingInFolder' }]);
const folderWithoutDocs = './__tests__/__fixtures__/ref-oas';
await expect(
docs.run({
folder: folderWithoutDocs,
key,
version,
deleteMissing: true,
dryRun: true,
})
).resolves.toBe('🎭 dry run! This will delete `thisDocShouldBeMissingInFolder`');
apiMocks.done();
versionMock.done();
});
});

describe('slug metadata', () => {
it('should use provided slug', async () => {
const slug = 'new-doc-slug';
Expand Down
27 changes: 23 additions & 4 deletions src/cmds/docs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import chalk from 'chalk';
import config from 'config';

import Command, { CommandCategories } from '../../lib/baseCommand';
import deleteDoc from '../../lib/deleteDoc';
import getDocs from '../../lib/getDocs';
import getSlug from '../../lib/getSlug';
import pushDoc from '../../lib/pushDoc';
import readdirRecursive from '../../lib/readdirRecursive';
import { getProjectVersion } from '../../lib/versionSelect';

export type Options = {
dryRun?: boolean;
folder?: string;
deleteMissing?: boolean;
};

export default class DocsCommand extends Command {
Expand Down Expand Up @@ -41,13 +45,18 @@ export default class DocsCommand extends Command {
type: Boolean,
description: 'Runs the command without creating/updating any docs in ReadMe. Useful for debugging.',
},
{
name: 'deleteMissing',
type: Boolean,
description: "Delete a doc from ReadMe if its slug can't be found in the target folder.",
},
];
}

async run(opts: CommandOptions<Options>) {
super.run(opts);

const { dryRun, folder, key, version } = opts;
const { dryRun, folder, key, version, deleteMissing } = opts;

if (!folder) {
return Promise.reject(new Error(`No folder provided. Usage \`${config.get('cli')} ${this.usage}\`.`));
Expand All @@ -67,7 +76,17 @@ export default class DocsCommand extends Command {

Command.debug(`number of files: ${files.length}`);

if (!files.length) {
const changes: string[] = [];
if (deleteMissing) {
const docs = await getDocs(key, selectedVersion);
kanadgupta marked this conversation as resolved.
Show resolved Hide resolved
const docSlugs = docs.map(({ slug }: { slug: string }) => slug);
const fileSlugs = new Set(files.map(getSlug));
const slugsToDelete = docSlugs.filter((slug: string) => !fileSlugs.has(slug));
const deletedDocs = await Promise.all(
slugsToDelete.map((slug: string) => deleteDoc(key, selectedVersion, dryRun, slug, this.cmdCategory))
);
changes.push(...deletedDocs);
} else if (!files.length) {
kanadgupta marked this conversation as resolved.
Show resolved Hide resolved
return Promise.reject(new Error(`We were unable to locate Markdown files in ${folder}.`));
}

Expand All @@ -76,7 +95,7 @@ export default class DocsCommand extends Command {
return pushDoc(key, selectedVersion, dryRun, filename, this.cmdCategory);
})
);

return chalk.green(updatedDocs.join('\n'));
changes.push(...updatedDocs);
return chalk.green(changes.join('\n'));
}
}
40 changes: 40 additions & 0 deletions src/lib/deleteDoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { CommandCategories } from './baseCommand';

import config from 'config';
import { Headers } from 'node-fetch';

import fetch, { cleanHeaders, handleRes } from './fetch';

/**
* Delete a document from ReadMe
*
* @param {String} key the project API key
* @param {String} selectedVersion the project version
* @param {Boolean} dryRun boolean indicating dry run mode
* @param {String} slug The slug of the document to delete
* @param {String} type module within ReadMe to update (e.g. docs, changelogs, etc.)
* @returns {Promise<String>} a string containing the result
erunion marked this conversation as resolved.
Show resolved Hide resolved
*/
export default async function deleteDoc(
key: string,
selectedVersion: string,
dryRun: boolean,
slug: string,
type: CommandCategories
) {
if (dryRun) {
return Promise.resolve(`🎭 dry run! This will delete \`${slug}\``);
}
return fetch(`${config.get('host')}/api/v1/${type}/${slug}`, {
method: 'delete',
headers: cleanHeaders(
key,
new Headers({
'x-readme-version': selectedVersion,
'Content-Type': 'application/json',
})
),
})
.then(handleRes)
.then(() => `successfully deleted \`${slug}\``);
}
5 changes: 5 additions & 0 deletions src/lib/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import APIError from './apiError';
import isGHA from './isGitHub';
import { debug } from './logger';

const SUCCESS_NO_CONTENT = 204;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DELETE /api/v1/docs/{slug} endpoint returns 204 with an empty body if the deletion was successful.


/**
* Getter function for a string to be used in the user-agent header based on the current
* environment.
Expand Down Expand Up @@ -74,6 +76,9 @@ async function handleRes(res: Response) {
// 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();
if (res.status === SUCCESS_NO_CONTENT) {
erunion marked this conversation as resolved.
Show resolved Hide resolved
return {};
}
debug(`received status code ${res.status} from ${res.url} with non-JSON response: ${body}`);
return Promise.reject(body);
}
Expand Down
38 changes: 38 additions & 0 deletions src/lib/getDocs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import config from 'config';
import { Headers } from 'node-fetch';

import fetch, { cleanHeaders, handleRes } from './fetch';
import getCategories from './getCategories';

async function getCategoryDocs(key: string, selectedVersion: string, category: string) {
return fetch(`${config.get('host')}/api/v1/categories/${category}/docs`, {
method: 'get',
headers: cleanHeaders(
key,
new Headers({
'x-readme-version': selectedVersion,
'Content-Type': 'application/json',
})
),
}).then(handleRes);
}

/**
* Retrieve the docs under all categories or under a specific one
*
* @param {String} key the project API key
* @param {String} selectedVersion the project version
* @param {String} [category] Get docs from the specified category only
* @returns {Promise<Array<Object>>} an array containing the docs
kanadgupta marked this conversation as resolved.
Show resolved Hide resolved
*/
export default async function getDocs(key: string, selectedVersion: string, category?: string) {
kanadgupta marked this conversation as resolved.
Show resolved Hide resolved
if (category) {
return getCategoryDocs(key, selectedVersion, category);
}

return getCategories(key, selectedVersion)
.then(categories => categories.filter(({ type }: { type: string }) => type === 'guide'))
.then(categories => categories.map(({ slug }: { slug: string }) => getCategoryDocs(key, selectedVersion, slug)))
.then(Promise.all.bind(Promise))
.then(args => [].concat(...args));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two lines are difficult to follow, any way they can be simplified?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (I think ;) )

}
23 changes: 23 additions & 0 deletions src/lib/getSlug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import fs from 'fs';
import path from 'path';

import grayMatter from 'gray-matter';

import { debug } from './logger';

/**
* Returns the slug of the specified Markdown or HTML file
*
* @param {String} filepath path to the HTML/Markdown file
* (file extension must end in `.html`, `.md`., or `.markdown`)
* @returns {String} a string containing the slug of the file
*/
export default function getSlug(filepath: string) {
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.
return matter.data.slug || path.basename(filepath).replace(path.extname(filepath), '').toLowerCase();
kanadgupta marked this conversation as resolved.
Show resolved Hide resolved
}