-
Notifications
You must be signed in to change notification settings - Fork 27
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
jjschnei
wants to merge
9
commits into
master
Choose a base branch
from
jeff-schneider-add-powercomman-sdkv3
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
27ce1b5
Create asanapower sdk v3
jjschnei 10f67e8
Add files via upload
jjschnei 9fac3eb
Remove log from testing
jjschnei 672850d
Change return to continue for filter check
jjschnei f0e3286
add another check for assignee having a name
jjschnei 0cbfd03
Add comment about using pagination for projects with more than 1000 t…
jjschnei cbe5bd7
fix logging
jjschnei cd73500
Update opt_fields in task_provider.js
jjschnei 55c3258
Add s to powercommand in folder name
jjschnei File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
await commandRunner.runCommand(tasksApiInstance, storiesApiInstance, command, task, argv); | ||
} | ||
|
||
} catch (err) { | ||
console.log(err); | ||
process.exit(1); | ||
} | ||
}; | ||
|
||
run(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
104
javascript/powercommand_using_SDKv3/connection_settings.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 thanreturn
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
There was a problem hiding this comment.
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 theassignee.name
. even though you requested forassignee
inopt_fields
in yourtask_provider.js
https://github.com/Asana/devrel-examples/pull/62/files#diff-c55db41f9d5a798d2282e92481cd6de3642c3ebb4d25c7bb1d48de8ae51315a7R4The 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.
There was a problem hiding this comment.
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.