Skip to content

Commit 5b4bb92

Browse files
committed
Add tool to delete member labels
1 parent 17eb60f commit 5b4bb92

File tree

9 files changed

+249
-3
lines changed

9 files changed

+249
-3
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,18 @@ gctools delete-tags --help
290290
gctools delete-tags <apiURL> <adminAPIKey> --tags hash-gctools, test 1
291291
```
292292

293+
### delete-labels
294+
295+
Delete member labels
296+
297+
```sh
298+
# See all available options
299+
gctools delete-labels --help
300+
301+
# Delete a specific tag or multiple labels
302+
gctools delete-labels <apiURL> <adminAPIKey> --labels 'First' 'Second'
303+
```
304+
293305

294306
### delete-empty-tags
295307

bin/cli.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import deleteUnusedTagsCommands from '../commands/delete-unused-tags.js';
2121
import deletePagesCommands from '../commands/delete-pages.js';
2222
import deletePostsCommands from '../commands/delete-posts.js';
2323
import deleteTagsCommands from '../commands/delete-tags.js';
24+
import deleteLabelsCommands from '../commands/delete-labels.js';
2425
import fetchAssetsCommands from '../commands/fetch-assets.js';
2526
import findReplaceCommands from '../commands/find-replace.js';
2627
import interactiveCommands from '../commands/interactive.js';
@@ -55,6 +56,7 @@ prettyCLI.command(deleteUnusedTagsCommands);
5556
prettyCLI.command(deletePagesCommands);
5657
prettyCLI.command(deletePostsCommands);
5758
prettyCLI.command(deleteTagsCommands);
59+
prettyCLI.command(deleteLabelsCommands);
5860
prettyCLI.command(fetchAssetsCommands);
5961
prettyCLI.command(findReplaceCommands);
6062
prettyCLI.command(interactiveCommands);

commands/delete-labels.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {ui} from '@tryghost/pretty-cli';
2+
import deleteLabels from '../tasks/delete-labels.js';
3+
4+
// Internal ID in case we need one.
5+
const id = 'delete-labels';
6+
7+
const group = 'Members:';
8+
9+
// The command to run and any params
10+
const flags = 'delete-labels <apiURL> <adminAPIKey>';
11+
12+
// Description for the top level command
13+
const desc = 'Delete labels in Ghost';
14+
15+
// Descriptions for the individual params
16+
const paramsDesc = [
17+
'URL to your Ghost API',
18+
'Admin API key'
19+
];
20+
21+
// Configure all the options
22+
const setup = (sywac) => {
23+
sywac.boolean('-V --verbose', {
24+
defaultValue: false,
25+
desc: 'Show verbose output'
26+
});
27+
sywac.array('--labels', {
28+
defaultValue: null,
29+
desc: 'Name of the label to delete, in the format \'First\' \'Second\''
30+
});
31+
sywac.number('--delayBetweenCalls', {
32+
defaultValue: 50,
33+
desc: 'The delay between API calls, in ms'
34+
});
35+
};
36+
37+
// What to do when this command is executed
38+
const run = async (argv) => {
39+
let timer = Date.now();
40+
let context = {errors: []};
41+
42+
try {
43+
// Fetch the tasks, configured correctly according to the options passed in
44+
let runner = deleteLabels.getTaskRunner(argv);
45+
46+
// Run the migration
47+
await runner.run(context);
48+
} catch (error) {
49+
ui.log.error('Done with errors', context.errors);
50+
}
51+
52+
// Report success
53+
ui.log.ok(`Successfully deleted ${context.deleted.length} labels in ${Date.now() - timer}ms.`);
54+
};
55+
56+
export default {
57+
id,
58+
group,
59+
flags,
60+
desc,
61+
paramsDesc,
62+
setup,
63+
run
64+
};

commands/interactive.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ const run = async () => {
121121
name: tasks.dedupeMembersCsv.choice.name,
122122
value: tasks.dedupeMembersCsv.choice.value
123123
},
124+
{
125+
name: tasks.deleteLabels.choice.name,
126+
value: tasks.deleteLabels.choice.value
127+
},
124128
new inquirer.Separator('--- Dangerous API Utilities -------'),
125129
{
126130
name: tasks.deletePosts.choice.name,

lib/admin-api-call.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const getMemberLabels = async (options) => {
7171
};
7272

7373
export {
74+
apiAuthTokenHeaders,
7475
getTiers,
7576
getMemberLabels
7677
};

lib/ghost-api-choices.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,11 @@ const getAPIRolesObj = async () => {
134134
];
135135
};
136136

137-
const getAPIMemberLabels = async ({returnKey = 'slug'}) => {
137+
const getAPIMemberLabels = async ({returnKey = 'slug', options}) => {
138138
let labels = [];
139139

140-
const url = process.env.GC_TOOLS_apiURL;
141-
const key = process.env.GC_TOOLS_adminAPIKey;
140+
const url = options?.apiURL ?? process.env.GC_TOOLS_apiURL;
141+
const key = options?.adminAPIKey ?? process.env.GC_TOOLS_adminAPIKey;
142142

143143
let labelsResponse = await getMemberLabels({
144144
adminAPIKey: key,

prompts/delete-labels.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import inquirer from 'inquirer';
2+
import inquirerSearchCheckbox from 'inquirer-search-checkbox';
3+
inquirer.registerPrompt('search-checkbox', inquirerSearchCheckbox);
4+
import {ui} from '@tryghost/pretty-cli';
5+
import chalk from 'chalk';
6+
import deleteLabels from '../tasks/delete-labels.js';
7+
import {getAPIMemberLabels} from '../lib/ghost-api-choices.js';
8+
import ghostAPICreds from '../lib/ghost-api-creds.js';
9+
10+
const choice = {
11+
name: 'Delete labels',
12+
value: 'deleteLabels'
13+
};
14+
15+
const options = [
16+
...ghostAPICreds,
17+
{
18+
type: 'search-checkbox',
19+
name: 'labels',
20+
message: `Select labels: ${chalk.yellow('[Type to search]')}`,
21+
pageSize: 20,
22+
choices: function () {
23+
return getAPIMemberLabels({returnKey: false});
24+
}
25+
}
26+
];
27+
28+
async function run() {
29+
await inquirer.prompt(options).then(async (answers) => {
30+
let timer = Date.now();
31+
let context = {errors: []};
32+
33+
try {
34+
let runner = deleteLabels.getTaskRunner(answers);
35+
await runner.run(context);
36+
ui.log.ok(`Successfully deleted ${context.deleted.length} labels in ${Date.now() - timer}ms.`);
37+
} catch (error) {
38+
ui.log.error('Done with errors', context.errors);
39+
}
40+
});
41+
}
42+
43+
export default {
44+
choice,
45+
options,
46+
run
47+
};

prompts/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import addTags from './add-tags.js';
1111
import combineTags from './combine-tags.js';
1212
import addPreview from './add-preview.js';
1313
import deleteTags from './delete-tags.js';
14+
import deleteLabels from './delete-labels.js';
1415
import deleteUnusedTags from './delete-unused-tags.js';
1516
import findReplace from './find-replace.js';
1617
import changeAuthor from './change-author.js';
@@ -40,6 +41,7 @@ export default {
4041
combineTags,
4142
addPreview,
4243
deleteTags,
44+
deleteLabels,
4345
deleteUnusedTags,
4446
findReplace,
4547
changeAuthor,

tasks/delete-labels.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import Promise from 'bluebird';
2+
import axios from 'axios';
3+
import GhostAdminAPI from '@tryghost/admin-api';
4+
import {makeTaskRunner} from '@tryghost/listr-smart-renderer';
5+
import _ from 'lodash';
6+
import {apiAuthTokenHeaders} from '../lib/admin-api-call.js';
7+
import {getAPIMemberLabels} from '../lib/ghost-api-choices.js';
8+
9+
const initialise = (options) => {
10+
return {
11+
title: 'Initialising API connection',
12+
task: (ctx, task) => {
13+
let defaults = {
14+
verbose: false,
15+
labels: false,
16+
delayBetweenCalls: 50
17+
};
18+
19+
const url = options.apiURL.replace(/\/$/, '');
20+
const key = options.adminAPIKey;
21+
const api = new GhostAdminAPI({
22+
url: url.replace('localhost', '127.0.0.1'),
23+
key,
24+
version: 'v5.0'
25+
});
26+
27+
ctx.args = _.mergeWith(defaults, options);
28+
ctx.api = api;
29+
ctx.labels = [];
30+
ctx.deleted = [];
31+
32+
task.output = `Initialised API connection for ${options.apiURL}`;
33+
}
34+
};
35+
};
36+
37+
const getFullTaskList = (options) => {
38+
return [
39+
initialise(options),
40+
{
41+
title: 'Fetch labels from Ghost API',
42+
task: async (ctx) => {
43+
if (ctx.args.labels[0].id) {
44+
ctx.labels = ctx.args.labels;
45+
} else {
46+
const allLabels = await getAPIMemberLabels({returnKey: false, options});
47+
48+
ctx.args.labels.forEach((label) => {
49+
let labelsObject = allLabels.find((labelObj) => {
50+
return labelObj.value.name === label;
51+
});
52+
53+
if (labelsObject) {
54+
ctx.labels.push(labelsObject.value);
55+
} else {
56+
ctx.errors.push(`No label found for "${label}"`);
57+
}
58+
});
59+
}
60+
}
61+
},
62+
{
63+
title: 'Deleting labels from Ghost',
64+
skip: (ctx) => {
65+
return ctx.labels.length === 0;
66+
},
67+
task: async (ctx) => {
68+
let tasks = [];
69+
70+
const headers = apiAuthTokenHeaders(options);
71+
72+
await Promise.mapSeries(ctx.labels, async (label) => {
73+
tasks.push({
74+
title: `Delete label ${label.name}`,
75+
task: async () => {
76+
let urlString = `${options.apiURL}/ghost/api/admin/labels/${label.id}/`;
77+
78+
try {
79+
let result = await axios.delete(urlString, {headers});
80+
ctx.deleted.push(label.name);
81+
return Promise.delay(options.delayBetweenCalls).return(result);
82+
} catch (error) {
83+
error.resource = {
84+
name: label.name
85+
};
86+
error.object = label;
87+
ctx.errors.push(error);
88+
throw error;
89+
}
90+
}
91+
});
92+
});
93+
94+
let taskOptions = options;
95+
taskOptions.concurrent = 3;
96+
return makeTaskRunner(tasks, taskOptions);
97+
}
98+
}
99+
];
100+
};
101+
102+
const getTaskRunner = (options) => {
103+
let tasks = [];
104+
105+
tasks = getFullTaskList(options);
106+
107+
return makeTaskRunner(tasks, Object.assign({topLevel: true}, {...options, verbose: true}));
108+
};
109+
110+
export default {
111+
initialise,
112+
getFullTaskList,
113+
getTaskRunner
114+
};

0 commit comments

Comments
 (0)