diff --git a/src/handlers/work/issues/_lib/add-lib.mjs b/src/handlers/work/issues/_lib/add-lib.mjs new file mode 100644 index 0000000..17c9744 --- /dev/null +++ b/src/handlers/work/issues/_lib/add-lib.mjs @@ -0,0 +1,78 @@ +import { claimIssues, verifyIssuesAvailable } from '@liquid-labs/github-toolkit' +import { httpSmartResponse } from '@liquid-labs/http-smart-response' +import { CredentialsDB, purposes } from '@liquid-labs/liq-credentials-db' +import { Octocache } from '@liquid-labs/octocache' + +import { commonAssignParameters } from '../../_lib/common-assign-parameters' +import { WorkDB } from '../../_lib/work-db' + +const doAddIssues = async({ app, cache, reporter, req, res, workKey }) => { + reporter = reporter.isolate() + + let { assignee, comment, issues, noAutoAssign } = req.vars + + const credDB = new CredentialsDB({ app, cache, reporter }) + const authToken = credDB.getToken(purposes.GITHUB_API) + const workDB = new WorkDB({ app, authToken, reporter }) + + // normalize the issue spec; add default project to issues + const workData = workDB.requireData(workKey) + // normalize the issue references + const primaryProject = workData.projects[0].name + issues = issues.map((i) => i.match(/^\d+$/) ? primaryProject + '/' + i : i) + + await verifyIssuesAvailable({ authToken, issues, noAutoAssign, notClosed : true, reporter }) + await claimIssues({ assignee, authToken, comment, issues, reporter }) + + const updatedWorkData = await workDB.addIssues({ issues, workKey }) + + httpSmartResponse({ + data : updatedWorkData, + msg : `Added '${issues.join("', '")}' to unit of work '${workKey}'.`, + req, + res + }) +} + +const getIssuesAddEndpointParameters = ({ workDesc }) => { + const help = { + name : 'Work issues add', + summary : `Add issues to the ${workDesc} unit of work.`, + description : `Adds one or more issues to the ${workDesc} unit of work.` + } + + const method = 'put' + + const parameters = [...commonAssignParameters()] + + parameters.find((p) => p.name === 'issues').optionsFunc = issueOptionsFunc + Object.freeze(parameters) + + return { help, method, parameters } +} + +const issueOptionsFunc = async({ app, cache, workKey }) => { + const credDB = new CredentialsDB({ app, cache }) + const authToken = credDB.getToken(purposes.GITHUB_API) + const octocache = new Octocache({ authToken }) + + const workDB = new WorkDB({ app }) + + const projects = workDB.getData(workKey).projects?.map((p) => p.name) + if (projects === undefined) return [] + + const options = [] + for (const project of projects) { + const issues = await octocache.paginate(`GET /repos/${project}/issues`, { state : 'open' }) + // TODO: use constant for 'assigned' + options.push(...issues + .filter((i) => !i.labels.some((l) => l.name === 'assigned')) + // eslint-disable-next-line prefer-regex-literals + .map((i) => i.url.replace(new RegExp('.+/repos/([^/]+)/([^/]+)/issues/(\\d+).*'), '$1/$2/$3')) + ) + } + + return options +} + +export { doAddIssues, getIssuesAddEndpointParameters } diff --git a/src/handlers/work/issues/_lib/list-lib.mjs b/src/handlers/work/issues/_lib/list-lib.mjs new file mode 100644 index 0000000..c8d26ef --- /dev/null +++ b/src/handlers/work/issues/_lib/list-lib.mjs @@ -0,0 +1,67 @@ +import { commonOutputParams, formatOutput } from '@liquid-labs/liq-handlers-lib' +import { tryExec } from '@liquid-labs/shell-toolkit' + +import { WorkDB } from '../../_lib/work-db' + +const getIssuesListEndpointParameters = ({ workDesc }) => { + const parameters = [ + { + name : 'browseEach', + isBoolean : true, + description : 'Will attempt to open a browser window for each issues in the list.' + }, + ...commonOutputParams() + ] + Object.freeze(parameters) + + return { + help : { + name : 'Work projects list', + summary : `List the ${workDesc} work issues.`, + description : `Lists the issues associated with the ${workDesc} unit of work.` + }, + method : 'get', + parameters + } +} + +const allFields = ['name', 'private'] +const defaultFields = allFields + +const mdFormatter = (issues, title) => `# ${title}\n\n${issues.map((i) => `- __${i.name}__:\n - private: ${i.private}`).join('\n')}\n` + +const terminalFormatter = (issues) => issues.map((i) => `${i.name}:\n - private: ${i.private}`).join('\n') + +const textFormatter = (issues) => issues.map((i) => `${i.name}:\n - private: ${i.private}`).join('\n') + +const doListIssues = async({ app, cache, reporter, req, res, workKey }) => { + reporter = reporter.isolate() + + const { browseEach = false } = req.vars + + const workDB = new WorkDB({ app }) + const workData = await workDB.getData(workKey) + + if (browseEach === true) { + for (const issue of workData.projects) { + const projectFQN = issue.name + tryExec(`open 'https://github.com/${projectFQN}'`) + } + } + + formatOutput({ + basicTitle : `${workKey} Projects`, + data : workData.projects, + allFields, + defaultFields, + mdFormatter, + terminalFormatter, + textFormatter, + reporter, + req, + res, + ...req.vars + }) +} + +export { doListIssues, getIssuesListEndpointParameters } diff --git a/src/handlers/work/issues/_lib/remove-lib.mjs b/src/handlers/work/issues/_lib/remove-lib.mjs new file mode 100644 index 0000000..da3d030 --- /dev/null +++ b/src/handlers/work/issues/_lib/remove-lib.mjs @@ -0,0 +1,77 @@ +import createError from 'http-errors' + +import { releaseIssues } from '@liquid-labs/github-toolkit' +import { httpSmartResponse } from '@liquid-labs/http-smart-response' +import { CredentialsDB, purposes } from '@liquid-labs/liq-credentials-db' + +import { commonIssuesParameters } from '../../_lib/common-issues-parameters' +import { WorkDB } from '../../_lib/work-db' + +const doRemoveIssues = async({ app, cache, reporter, req, res, workKey }) => { + reporter = reporter.isolate() + + let { comment, issues, noUnassign, noUnlabel } = req.vars + + const workDB = new WorkDB({ app, reporter }) + const workData = workDB.getData(workKey) + if (workData === undefined) { + throw createError.NotFound(`No such active unit of work '${workKey}'.`) + } + + // normalize the issue spec; add default project to issues + const primaryProject = workData.projects[0].name + issues = issues.map((i) => i.match(/^\d+$/) ? primaryProject + '/' + i : i) + + const credDB = new CredentialsDB({ app, cache, reporter }) + const authToken = credDB.getToken(purposes.GITHUB_API) + + await releaseIssues({ authToken, comment, issues, noUnassign, noUnlabel, reporter }) + + const updatedWorkData = workDB.removeIssues({ workKey, issues }) + + httpSmartResponse({ + data : updatedWorkData, + msg : `Removed issues '${issues.join("', '")}' from unit of work '${workKey}'.`, + req, + res + }) +} + +const getIssuesRemoveEndpointParameters = ({ workDesc }) => { + const parameters = [ + { + name : 'comment', + description : 'Comment to add to the issues as they are removed. A default comment will be generated if none is provided. Pass an empty string to suppress leaving a comment.' + }, + { + name : 'noUnassign', + isBoolean : true, + description : 'Setting `noUnassign` to true maintains the issue assignments rather than the default behavior of unassigning the issue.' + }, + { + name : 'noUnlabel', + isBoolean : true, + description : "Setting `noUnlabel` to true keeps the 'claim label' on the issue rather than the default behavior of removing it." + }, + ...commonIssuesParameters + ] + parameters.find((p) => p.name === 'issues').optionsFunc = issueOptionsFunc + Object.freeze(parameters) + + return { + help : { + name : 'Work issues remove', + summary : `Remove issues from the ${workDesc} unit of work.`, + description : `Removes issues from the ${workDesc} unit of work.` + }, + method : 'delete', + parameters + } +} + +const issueOptionsFunc = ({ app, workKey }) => { + const workDB = new WorkDB({ app }) + return workDB.getIssueKeys(workKey) +} + +export { doRemoveIssues, getIssuesRemoveEndpointParameters } diff --git a/src/handlers/work/issues/add-implied.mjs b/src/handlers/work/issues/add-implied.mjs new file mode 100644 index 0000000..b288acc --- /dev/null +++ b/src/handlers/work/issues/add-implied.mjs @@ -0,0 +1,22 @@ +import createError from 'http-errors' + +import { determineCurrentBranch } from '@liquid-labs/git-toolkit' + +import { doAddIssues, getIssuesAddEndpointParameters } from './_lib/add-lib' + +const { help, method, parameters } = getIssuesAddEndpointParameters({ workDesc : 'current' }) + +const path = ['work', 'issues', 'add'] + +const func = ({ app, cache, model, reporter }) => async(req, res) => { + const currDir = req.get('X-CWD') + if (currDir === undefined) { + throw createError.BadRequest('Called \'work issues add\' with implied work, but \'X-CWD\' header not found.') + } + + const workKey = await determineCurrentBranch({ projectPath : currDir, reporter }) + + await doAddIssues({ app, cache, reporter, req, res, workKey }) +} + +export { func, help, method, parameters, path } diff --git a/src/handlers/work/issues/add.js b/src/handlers/work/issues/add.js index 2cc5ebd..0469cd5 100644 --- a/src/handlers/work/issues/add.js +++ b/src/handlers/work/issues/add.js @@ -1,79 +1,13 @@ -import createError from 'http-errors' +import { doAddIssues, getIssuesAddEndpointParameters } from './_lib/add-lib' -import { claimIssues, verifyIssuesAvailable } from '@liquid-labs/github-toolkit' -import { httpSmartResponse } from '@liquid-labs/http-smart-response' -import { CredentialsDB, purposes } from '@liquid-labs/liq-credentials-db' -import { Octocache } from '@liquid-labs/octocache' +const { help, method, parameters } = getIssuesAddEndpointParameters({ workDesc : 'named' }) -import { commonAssignParameters } from '../_lib/common-assign-parameters' -import { WorkDB } from '../_lib/work-db' - -const help = { - name : 'Work issues add', - summary : 'Add issues to a unit of work.', - description : 'Adds one or more issues to a unit of work.' -} - -const method = 'put' const path = ['work', ':workKey', 'issues', 'add'] -const parameters = [ - ...commonAssignParameters() -] -const issueOptionsFunc = async({ app, cache, workKey }) => { - const credDB = new CredentialsDB({ app, cache }) - const authToken = credDB.getToken(purposes.GITHUB_API) - const octocache = new Octocache({ authToken }) - - const workDB = new WorkDB({ app }) - - const projects = workDB.getData(workKey).projects?.map((p) => p.name) - if (projects === undefined) return [] - - const options = [] - for (const project of projects) { - const issues = await octocache.paginate(`GET /repos/${project}/issues`, { state : 'open' }) - // TODO: use constant for 'assigned' - options.push(...issues - .filter((i) => !i.labels.some((l) => l.name === 'assigned')) - // eslint-disable-next-line prefer-regex-literals - .map((i) => i.url.replace(new RegExp('.+/repos/([^/]+)/([^/]+)/issues/(\\d+).*'), '$1/$2/$3')) - ) - } - - return options -} -parameters.find((p) => p.name === 'issues').optionsFunc = issueOptionsFunc -Object.freeze(parameters) - const func = ({ app, cache, model, reporter }) => async(req, res) => { - reporter = reporter.isolate() - - let { assignee, comment, issues, noAutoAssign, workKey } = req.vars - - const credDB = new CredentialsDB({ app, cache, reporter }) - const authToken = credDB.getToken(purposes.GITHUB_API) - const workDB = new WorkDB({ app, authToken, reporter }) - - // normalize the issue spec; add default project to issues - const workData = workDB.getData(workKey) - if (workData === undefined) { - throw createError.NotFound(`No such active unit of work '${workKey}'.`) - } - const primaryProject = workData.projects[0].name - issues = issues.map((i) => i.match(/^\d+$/) ? primaryProject + '/' + i : i) - - await verifyIssuesAvailable({ authToken, issues, noAutoAssign, notClosed : true, reporter }) - await claimIssues({ assignee, authToken, comment, issues, reporter }) - - const updatedWorkData = await workDB.addIssues({ issues, workKey }) + const { workKey } = req.vars - httpSmartResponse({ - data : updatedWorkData, - msg : `Added '${issues.join("', '")}' to unit of work '${workKey}'.`, - req, - res - }) + await doAddIssues({ app, cache, reporter, req, res, workKey }) } -export { func, help, parameters, path, method } +export { func, help, method, parameters, path } diff --git a/src/handlers/work/issues/index.js b/src/handlers/work/issues/index.js index e3f2755..3e72e45 100644 --- a/src/handlers/work/issues/index.js +++ b/src/handlers/work/issues/index.js @@ -1,7 +1,10 @@ import * as addHandler from './add' +import * as addImpliedHandler from './add-implied' import * as listHandler from './list' +import * as listImpliedHandler from './list-implied' import * as removeHandler from './remove' +import * as removeImpliedHandler from './remove-implied' -const handlers = [addHandler, listHandler, removeHandler] +const handlers = [addHandler, addImpliedHandler, listHandler, listImpliedHandler, removeHandler, removeImpliedHandler] export { handlers } diff --git a/src/handlers/work/issues/list-implied.mjs b/src/handlers/work/issues/list-implied.mjs new file mode 100644 index 0000000..6d8a1ec --- /dev/null +++ b/src/handlers/work/issues/list-implied.mjs @@ -0,0 +1,22 @@ +import createError from 'http-errors' + +import { determineCurrentBranch } from '@liquid-labs/git-toolkit' + +import { doListIssues, getIssuesListEndpointParameters } from './_lib/list-lib' + +const { help, method, parameters } = getIssuesListEndpointParameters({ workDesc : 'current' }) + +const path = ['work', 'issues', 'list'] + +const func = ({ app, cache, model, reporter }) => async(req, res) => { + const currDir = req.get('X-CWD') + if (currDir === undefined) { + throw createError.BadRequest('Called \'work issues list\' with implied work, but \'X-CWD\' header not found.') + } + + const workKey = await determineCurrentBranch({ projectPath : currDir, reporter }) + + await doListIssues({ app, cache, reporter, req, res, workKey }) +} + +export { func, help, method, parameters, path } diff --git a/src/handlers/work/issues/list.mjs b/src/handlers/work/issues/list.mjs index 2a81182..20b4872 100644 --- a/src/handlers/work/issues/list.mjs +++ b/src/handlers/work/issues/list.mjs @@ -1,63 +1,13 @@ -import { commonOutputParams, formatOutput } from '@liquid-labs/liq-handlers-lib' -import { tryExec } from '@liquid-labs/shell-toolkit' +import { doListIssues, getIssuesListEndpointParameters } from './_lib/list-lib' -import { WorkDB } from '../_lib/work-db' +const { help, method, parameters } = getIssuesListEndpointParameters({ workDesc : 'named' }) -const help = { - name : 'Work issues list', - summary : 'List work issues.', - description : 'Lists the issues associated with the indicated unit of work.' -} - -const method = 'get' const path = ['work', ':workKey', 'issues', 'list'] -const parameters = [ - { - name : 'browseEach', - isBoolean : true, - description : 'Will attempt to open a browser window for each issues in the list.' - }, - ...commonOutputParams() -] -Object.freeze(parameters) - -const allFields = ['id', 'summary'] -const defaultFields = allFields - -const mdFormatter = (issues, title) => `# ${title}\n\n${issues.map((i) => `* __${i.id}__: ${i.descirption}`).join('\n')}\n` - -const terminalFormatter = (issues) => issues.map((i) => `${i.id}: ${i.summary}`).join('\n') - -const textFormatter = (issues) => issues.map((i) => `${i.id}: ${i.summary}`).join('\n') const func = ({ app, cache, model, reporter }) => async(req, res) => { - reporter = reporter.isolate() - - const { browseEach = false, workKey } = req.vars - - const workDB = new WorkDB({ app }) - const workData = await workDB.getData(workKey) - - if (browseEach === true) { - for (const issue of workData.issues) { - const [org, project, number] = issue.id.split('/') - tryExec(`open 'https://github.com/${org}/${project}/issues/${number}'`) - } - } + const { workKey } = req.vars - formatOutput({ - basicTitle : `${workKey} Issues`, - data : workData.issues, - allFields, - defaultFields, - mdFormatter, - terminalFormatter, - textFormatter, - reporter, - req, - res, - ...req.vars - }) + await doListIssues({ app, cache, reporter, req, res, workKey }) } -export { func, help, parameters, path, method } +export { func, help, method, parameters, path } diff --git a/src/handlers/work/issues/remove-implied.mjs b/src/handlers/work/issues/remove-implied.mjs new file mode 100644 index 0000000..0f0e1da --- /dev/null +++ b/src/handlers/work/issues/remove-implied.mjs @@ -0,0 +1,22 @@ +import createError from 'http-errors' + +import { determineCurrentBranch } from '@liquid-labs/git-toolkit' + +import { doRemoveIssues, getIssuesRemoveEndpointParameters } from './_lib/remove-lib' + +const { help, method, parameters } = getIssuesRemoveEndpointParameters({ workDesc : 'current' }) + +const path = ['work', 'issues', 'remove'] + +const func = ({ app, cache, model, reporter }) => async(req, res) => { + const currDir = req.get('X-CWD') + if (currDir === undefined) { + throw createError.BadRequest('Called \'work issues list\' with implied work, but \'X-CWD\' header not found.') + } + + const workKey = await determineCurrentBranch({ projectPath : currDir, reporter }) + + await doRemoveIssues({ app, cache, reporter, req, res, workKey }) +} + +export { func, help, method, parameters, path } diff --git a/src/handlers/work/issues/remove.js b/src/handlers/work/issues/remove.js index 17685f2..8afd386 100644 --- a/src/handlers/work/issues/remove.js +++ b/src/handlers/work/issues/remove.js @@ -1,73 +1,13 @@ -import createError from 'http-errors' +import { doRemoveIssues, getIssuesRemoveEndpointParameters } from './_lib/remove-lib' -import { releaseIssues } from '@liquid-labs/github-toolkit' -import { httpSmartResponse } from '@liquid-labs/http-smart-response' -import { CredentialsDB, purposes } from '@liquid-labs/liq-credentials-db' +const { help, method, parameters } = getIssuesRemoveEndpointParameters({ workDesc : 'named' }) -import { commonIssuesParameters } from '../_lib/common-issues-parameters' -import { WorkDB } from '../_lib/work-db' - -const help = { - name : 'Work issues remove', - summary : 'Remove issues from a unit of work.', - description : 'Removes issues from the indicated unit of work.' -} - -const method = 'delete' const path = ['work', ':workKey', 'issues', 'remove'] -const parameters = [ - { - name : 'comment', - description : 'Comment to add to the issues as they are removed. A default comment will be generated if none is provided. Pass an empty string to suppress leaving a comment.' - }, - { - name : 'noUnassign', - isBoolean : true, - description : 'Setting `noUnassign` to true maintains the issue assignments rather than the default behavior of unassigning the issue.' - }, - { - name : 'noUnlabel', - isBoolean : true, - description : "Setting `noUnlabel` to true keeps the 'claim label' on the issue rather than the default behavior of removing it." - }, - ...commonIssuesParameters -] -const issueOptionsFunc = ({ app, workKey }) => { - const workDB = new WorkDB({ app }) - return workDB.getIssueKeys(workKey) -} -parameters.find((p) => p.name === 'issues').optionsFunc = issueOptionsFunc -Object.freeze(parameters) - const func = ({ app, cache, model, reporter }) => async(req, res) => { - reporter = reporter.isolate() - - let { comment, issues, noUnassign, noUnlabel, workKey } = req.vars - - const workDB = new WorkDB({ app, reporter }) - const workData = workDB.getData(workKey) - if (workData === undefined) { - throw createError.NotFound(`No such active unit of work '${workKey}'.`) - } - - // normalize the issue spec; add default project to issues - const primaryProject = workData.projects[0].name - issues = issues.map((i) => i.match(/^\d+$/) ? primaryProject + '/' + i : i) - - const credDB = new CredentialsDB({ app, cache, reporter }) - const authToken = credDB.getToken(purposes.GITHUB_API) - - await releaseIssues({ authToken, comment, issues, noUnassign, noUnlabel, reporter }) - - const updatedWorkData = workDB.removeIssues({ workKey, issues }) + const { workKey } = req.vars - httpSmartResponse({ - data : updatedWorkData, - msg : `Removed issues '${issues.join("', '")}' from unit of work '${workKey}'.`, - req, - res - }) + await doRemoveIssues({ app, cache, reporter, req, res, workKey }) } -export { func, help, parameters, path, method } +export { func, help, method, parameters, path }