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

Jeff schneider add powercomman sdkv3 #62

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
21 changes: 21 additions & 0 deletions javascript/powercommand_using_SDKv3/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Asana

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
54 changes: 54 additions & 0 deletions javascript/powercommand_using_SDKv3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# powercommands
Command-line interface for power-user operations over the Asana API using the Asana Node SDK ^3.0.1

To install and run:
- Install node
- Clone the repository
- Inside this folder, run `npm install`
- Run `npm link`
- Run `asana-power -h` to see all help options, then go from there!

## Examples

Configure your connection settings and save them to your keychain:
- `asana-power --token <your_personal_access_token> --project <your_project_url> --save`

The project can be provided as either a URL or an ID:
- `asana-power --project https://app.asana.com/0/1148349108183377/list --save`
- `asana-power --project 1148349108183377 --save`

List all of the tasks in the target project:
- `asana-power list --all`

List all of the tasks which contain some text in its notes:
- `asana-power list --notes "some text"`

Complete all of the tasks which match some assignee:
- `asana-power complete --assignee "Kenan Kigunda"`

You can mix and match filters. For example, we can go back and incomplete the subset of tasks which match some assignee and have some text:
- `asana-power incomplete --assignee "Kenan Kigunda" --notes "some text"`

Add a comment on a task with a particular name:
- `asana-power comment --name "The lights are out" --message "We tried to replace the bulb today but the new one didn't fit :("`

At any point, you can run a command on a project or with a token different than the one saved in the keychain by passing the corresponding parameters:
- `asana-power list --all --project <your_other_project_url>`
- `asana-power comment --name "The lights are out" --message "We'll order another one next week" --token <token_for_the_vendor>`

Each of the options has an alias. For example, you can call:
- `asana-power list -a` (all)
- `asana-power list -c` (completed)
- `asana-power list -i` (incompleted)
- `asana-power complete -@ Kenan -n lights` (assignee, name)
- `asana-power comment -o workshop -m "Is the workshop still happening?"` (notes, message)

## Code references

To see how we read tasks, see `task_provider.js`

To see how we filter tasks, see `task_client_filter.js`

To see how we run commands on each task, see `command_runner.js`

The main file, `asana_power.js`, orchestrates these modules together
89 changes: 89 additions & 0 deletions javascript/powercommand_using_SDKv3/asana_power.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env node

const argv = require('yargs')
.usage('Usage: $0 <command> [options]')
.command('list', 'List all of the matching tasks')
.command('complete', 'Complete all of the matching tasks')
.command('incomplete', 'Incomplete all of the matching tasks')
.command('comment', 'Add a comment with a message on all of the matching tasks')
.alias('t', 'token')
.nargs('t', 1)
.describe('t', 'The access token used to authenticate to Asana')
.alias('p', 'project')
.describe('p', 'The project that we should operate on. Can be provided either as an ID or a link')
.alias('s', 'save')
.describe(
's',
'Indicates that we should save the provided token and project parameters to the keychain for future requests'
)
.alias('m', 'message')
.describe('m', "The message used in commands such as 'comment'")
.alias('a', 'all')
.describe(
'a',
'Indicates that we should operate over all tasks in the target project. Not compatible with other filters'
)
.alias('n', 'name')
.describe(
'name',
'Indicates that we should operate only on the tasks whose name contains this value. Compatible with other filters'
)
.alias('o', 'notes')
.describe(
'notes',
'Indicates that we should operate only on the tasks whose notes (the task text) contains this value. Compatible with other filters'
)
.alias('@', 'assignee')
.describe(
'assignee',
'Indicates that we should operate only on the tasks whose assignee\'s name contains this value. Compatible with other filters'
)
.alias('c', 'completed')
.describe(
'completed',
'Indicates that we should operate only on the tasks which are completed. Compatible with other filters'
)
.alias('i', 'incomplete')
.describe(
'incomplete',
'Indicates that we should operate only on the tasks which are incomplete. Compatible with other filters'
)
.alias('h', 'help')
.help('h')
.alias('v', 'version').argv;
const Asana = require('asana');
const connectionSettings = require('./connection_settings.js');
const taskProvider = require('./task_provider.js');
const taskFilterer = require('./task_client_filter.js');
const commandRunner = require('./command_runner.js');

const run = async () => {
try {
const projectId = await connectionSettings.getProjectId(argv.project, argv.save);
const PAT = await connectionSettings.getToken(argv.token, argv.save);
const command = commandRunner.getCommand(argv);
if (!command) return;
const filters = taskFilterer.getFilters(argv);

// Create Asana ApiInstances for tasks and stories using node SDK v3
// SDK repo for more information: https://github.com/Asana/node-asana/
const client = Asana.ApiClient.instance;
const token = client.authentications['token'];
token.accessToken = PAT;
const tasksApiInstance = new Asana.TasksApi();
const storiesApiInstance = new Asana.StoriesApi();

const tasks = await taskProvider.getTaskRefs(tasksApiInstance, projectId);

for(let task of tasks.data) {
if (filters.some(filter => !filter.matchesTask(task))) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably be a continue rather than return it doesn't make sense to return in this for loop. You want to continue the loop if the task does not match the filter.

The return makes sense in the previous code since it's a tasks.stream().on(...) takes in a function https://github.com/Asana/devrel-examples/blob/master/javascript/powercommands/asana_power.js#L69-L73

Copy link
Contributor

@jv-asana jv-asana Feb 3, 2024

Choose a reason for hiding this comment

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

If you try to run asana-power complete --assignee "Kenan Kigunda" this won't work because the tasks being returned is missing the assignee.name. even though you requested for assignee in opt_fields in your task_provider.jshttps://github.com/Asana/devrel-examples/pull/62/files#diff-c55db41f9d5a798d2282e92481cd6de3642c3ebb4d25c7bb1d48de8ae51315a7R4

The Asana API does not return the full assignee object so it'll error when it tries to filter. Pretty sure this was broken in the v1 as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added the extra check. It could be worth coming back in the future if we get meaningful usage to add another request to fetch each task with getTask to get the assignee name. Could also maybe do it with assignee gid.

await commandRunner.runCommand(tasksApiInstance, storiesApiInstance, command, task, argv);
}

} catch (err) {
console.log(err);
process.exit(1);
}
};

run();
61 changes: 61 additions & 0 deletions javascript/powercommand_using_SDKv3/command_runner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const commands = {
list: 'list',
complete: 'complete',
incomplete: 'incomplete',
comment: 'comment',
};

const getCommand = argv => {
if (argv._.length == 0 && argv.save) {
console.log('No command specified, so configured connection settings without executing command');
return;
} else if (argv._.length !== 1) {
throw new Error(`Specifify exactly one command from the set: ${Object.keys(commands).join(', ')}`);
}
const command = argv._[0];
if (!(command in commands)) {
throw new Error(`Command not recognized: ${command}. Specifify exactly one command from the set: ${Object.keys(commands).join(', ')}`);
}
console.log('Selected command:', command);
if (command in validateArgs) validateArgs[command](argv);
return command;
};

const validateArgs = {
comment: argv => {
if (!argv.message) throw new Error(`Command 'comment' requires a 'message' argument`);
},
};

const runCommand = async (tasksApiInstance, storiesApiInstance, command, task, argv) => {
switch (command) {
case commands.list:
console.log('Listing task:', task);
return;
case commands.complete:
console.log('Completing task:', task);
const completedTask = await tasksApiInstance.updateTask({"data": {"completed": "true"}},
task.gid);
console.log('Completed task:', completedTask);
return;
case commands.incomplete:
console.log('Incompleting task:', task);
const incompletedTask = await tasksApiInstance.updateTask({"data": {"completed": "false"}},
task.gid);
console.log('Incompleted task:', incompletedTask);
return;
case commands.comment:
console.log('Commenting on task', task, 'message', argv.message);
const comment = await storiesApiInstance.createStoryForTask({"data": {"text": argv.message}},
task.gid);
console.log('Commented on task', comment);
return;
default:
throw new Error(`Command not recognized: ${command}. Specifify exactly one command from the set: ${Object.keys(commands).join(', ')}`);
}
};

module.exports = {
getCommand,
runCommand,
};
104 changes: 104 additions & 0 deletions javascript/powercommand_using_SDKv3/connection_settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
const keychain = require('keychain');

const tokenKeychainSettings = {
service: 'asana-power cli access-token',
account: 'default',
};

const projectKeychainSettings = {
service: 'asana-power cli project-id',
account: 'default',
};

const getToken = function(tokenInput, shouldSave) {
if (tokenInput) {
console.log('Using provided access token');
return shouldSave ? saveTokenToKeychain(tokenInput) : useTokenDirectly(tokenInput);
} else {
return shouldSave ? saveTokenError() : restoreTokenFromKeychain();
}
};

const useTokenDirectly = token => Promise.resolve(token);

const saveTokenError = () =>
Promise.reject('Error: You indicated that we should save a token, but no token was provided');

const saveTokenToKeychain = token => {
console.log('Saving access token to keychain using settings', tokenKeychainSettings);
return new Promise((resolve, reject) =>
keychain.setPassword(
{
...tokenKeychainSettings,
password: token,
},
err => {
if (err) return reject(err);
console.log('Successfully saved access token to keychain!');
resolve(token);
}
)
);
};

const restoreTokenFromKeychain = () => {
console.log('Restoring access token from keychain using settings', tokenKeychainSettings);
return new Promise((resolve, reject) => {
keychain.getPassword(tokenKeychainSettings, (err, token) => {
if (err) return reject(err);
console.log('Successfully restored access token from keychain!');
resolve(token);
});
});
};

const getProjectId = async (projectInput, shouldSave) => {
if (projectInput) {
console.log('Using provided project');
const projectId = parseProjectId(projectInput);
return shouldSave ? saveProjectIdToKeychain(projectId) : projectId;
} else {
return restoreProjectIdFromKeychain();
}
};

const parseProjectId = projectInput => {
const matchProjectInput = /[0-9]{2,}/.exec(projectInput);
if (!matchProjectInput) throw new Error(`Cannot determine project ID from input '${projectInput}'`);
if (matchProjectInput.length > 1)
console.log('Warning: Found more than one potential project ID; using the first match');
return matchProjectInput[0];
};

const saveProjectIdToKeychain = projectId => {
console.log('Saving project ID to keychain using settings', projectKeychainSettings);
return new Promise((resolve, reject) =>
keychain.setPassword(
{
...projectKeychainSettings,
password: projectId,
},
err => {
if (err) return reject(err);
console.log('Successfully saved project ID ${projectId} to keychain!');
resolve(projectId);
}
)
);
};

const restoreProjectIdFromKeychain = () => {
console.log('Restoring project ID from keychain using settings', projectKeychainSettings);
return new Promise((resolve, reject) => {
keychain.getPassword(projectKeychainSettings, (err, projectId) => {
if (err) return reject(err);
console.log('Successfully restored project ID from keychain!');
resolve(projectId);
});
});
};

module.exports = {
getToken,
getProjectId,
};
Loading