import { Url , parse } from "url" ; import * as http from "http" ; import * as https from "https" ; import { RedmineProject } from "./redmine-project" ; import { IssueStatus , Membership , QuickUpdate , QuickUpdateResult , } from "../controllers/domain" ; import { TimeEntryActivity } from "./models/time-entry-activity" ; import { Project } from "./models/project" ; import { TimeEntry } from "./models/time-entry" ; import { Issue } from "./models/issue" ; import { IssueStatus as RedmineIssueStatus } from "./models/issue-status" ; import { Membership as RedmineMembership } from "./models/membership" ; import { isEqual } from "lodash" ; type HttpMethods = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" ; const REDMINE_API_KEY_HEADER_NAME = "X-Redmine-API-Key" ; export interface RedmineServerConnectionOptions { example example example example address : string ; example key : string ; default rejectUnauthorized ? : boolean ; example additionalHeaders ? : { [ key ] : string ; } ; } interface RedmineServerOptions extends RedmineServerConnectionOptions { url : Url ; } export class RedmineOptionsError extends Error { name = "RedmineOptionsError" ; } export class RedmineServer { options : RedmineServerOptions ; private timeEntryActivities : TimeEntryActivity [ ] = null ; get request ( ) { return this . options . url . protocol === "https:" ? https . request : http . request ; } private validateOptions ( options ) { if ( ! options . address ) { throw new RedmineOptionsError ( "Address cannot be empty!" ) ; } if ( ! options . key ) { throw new RedmineOptionsError ( "Key cannot be empty!" ) ; } const url = parse ( options . address ) ; if ( [ "https:" , "http:" ] . indexOf ( url . protocol ) === - 1 ) { throw new RedmineOptionsError ( "Address must have supported protocol (http/https)" ) ; } } private setOptions ( options ) { this . options = { ... options , url : parse ( options . address ) , } ; if ( this . options . additionalHeaders == null ) { this . options . additionalHeaders = { } ; } } constructor ( options ) { this . validateOptions ( options ) ; this . setOptions ( options ) ; } doRequest < T > ( path , method , data ? ) { const { url , key , additionalHeaders , rejectUnauthorized } = this . options ; const options = { hostname : url . hostname , port : url . port , headers : { [ REDMINE_API_KEY_HEADER_NAME ] : key , ... additionalHeaders , } , rejectUnauthorized : rejectUnauthorized , path : `${ url . pathname }${ path }` , method , } ; if ( data ) { options . headers [ "Content-Length" ] = data . length ; options . headers [ "Content-Type" ] = "application/json" ; } return new Promise ( ( resolve , reject ) => { let incomingBuffer = Buffer . from ( "" ) ; const handleData = ( _ ) => ( incoming ) => { incomingBuffer = Buffer . concat ( [ incomingBuffer , incoming ] ) ; } ; const handleEnd = ( clientResponse ) => ( ) => { const { statusCode , statusMessage } = clientResponse ; console . log ( "end" , statusCode ) ; if ( statusCode == 401 ) { reject ( "Server returned 401 (perhaps your API Key is not valid, or your server has additional authentication methods?)" ) ; return ; } if ( statusCode == 403 ) { reject ( "Server returned 403 (perhaps you haven't got permissions?)" ) ; return ; } if ( statusCode == 404 ) { reject ( "Resource doesn't exsist" ) ; return ; } if ( statusCode >= 400 ) { reject ( statusMessage ) ; return ; } console . log ( incomingBuffer ) ; if ( incomingBuffer . length > 0 ) { try { const object = JSON . parse ( incomingBuffer . toString ( "utf8" ) ) ; console . log ( incomingBuffer , object ) ; resolve ( object ) ; } catch ( e ) { console . warn ( "Couldn't parse response as JSON..." ) ; resolve ( null ) ; } return ; } resolve ( null ) ; } ; const clientRequest = this . request ( options , ( incoming ) => { incoming . on ( "data" , handleData ( incoming ) ) ; incoming . on ( "end" , handleEnd ( incoming ) ) ; } ) ; const handleError = ( error ) => { console . error ( error ) ; reject ( `NodeJS Request Error (${ error . name }): ${ error . message }` ) ; } ; clientRequest . on ( "error" , handleError ) ; clientRequest . end ( data ) ; } ) ; } async getProjects ( ) { const req = async ( offset = 0 , limit = 50 , count = null , accumulator = [ ] ) => { if ( count && count <= offset ) { return accumulator ; } const [ totalCount , result ] = await this . doRequest < { projects : Project [ ] ; total_count : number ; } > ( `/projects.json?limit=${ limit }&offset=${ offset }` , "GET" ) . then ( ( { total_count , projects } ) => [ total_count , projects . map ( ( proj ) => new RedmineProject ( this , { ... proj , id : `${ proj . id }` , parent : proj . parent ? { ... proj . parent , id : `${ proj . parent . id }` , } : undefined , } ) ) , ] ) ; return req ( offset + limit , limit , totalCount , accumulator . concat ( result ) ) ; } ; return req ( ) ; } getTimeEntryActivities ( ) { if ( this . timeEntryActivities ) { return Promise . resolve ( { time_entry_activities : this . timeEntryActivities , } ) ; } return this . doRequest < { time_entry_activities : TimeEntryActivity [ ] ; } > ( `/enumerations/time_entry_activities.json` , "GET" ) . then ( ( response ) => { if ( response ) { this . timeEntryActivities = response . time_entry_activities ; } return response ; } ) ; } addTimeEntry ( issueId , activityId , hours , message ) { return this . doRequest < { time_entry : TimeEntry ; } > ( `/time_entries.json` , "POST" , Buffer . from ( JSON . stringify ( { time_entry : < TimeEntry > { issue_id : issueId , activity_id : activityId , hours , comments : message , } , } ) ) ) ; } param issueId getIssueById ( issueId ) { return this . doRequest ( `/issues/${ issueId }.json` , "GET" ) ; } /**
     * Returns promise, that resolves, when issue status is set
     */ setIssueStatus ( issue , statusId ) { return this . doRequest < { issue : Issue ; } > ( `/issues/${ issue . id }.json` , "PUT" , new Buffer ( JSON . stringify ( { issue : { status_id : statusId , } , } ) ) ) ; } issueStatuses : { issue_statuses : RedmineIssueStatus [ ] ; } = null ; /**
     * Returns promise, that resolves to list of issue statuses in provided redmine server
     */ getIssueStatuses ( ) { if ( this . issueStatuses == null ) { return this . doRequest < { issue_statuses : RedmineIssueStatus [ ] ; } > ( "/issue_statuses.json" , "GET" ) . then ( ( obj ) => { if ( obj ) { this . issueStatuses = obj ; } return obj ; } ) ; } else { return Promise . resolve ( this . issueStatuses ) ; } } async getIssueStatusesTyped ( ) { const statuses = await this . getIssueStatuses ( ) ; return statuses . issue_statuses . map ( ( s ) => new IssueStatus ( s . id , s . name ) ) ; } async getMemberships ( projectId ) { const membershipsResponse = await this . doRequest < { memberships : RedmineMembership [ ] ; } > ( `/projects/${ projectId }/memberships.json` , "GET" ) ; return membershipsResponse . memberships . filter ( ( m ) => m . user ) . map ( ( m ) => new Membership ( m . user . id , m . user . name ) ) ; } async applyQuickUpdate ( quickUpdate ) { await this . doRequest < { issue : Issue ; } > ( `/issues/${ quickUpdate . issueId }.json` , "PUT" , new Buffer ( JSON . stringify ( { issue : { status_id : quickUpdate . status . statusId , assigned_to_id : quickUpdate . assignee . userId , notes : quickUpdate . message , } , } ) ) ) ; const issueRequest = await this . getIssueById ( quickUpdate . issueId ) ; const issue = issueRequest . issue ; const updateResult = new QuickUpdateResult ( ) ; if ( issue . assigned_to . id != quickUpdate . assignee . userId ) { updateResult . addDifference ( "Couldn't assign user" ) ; } if ( issue . status . id != quickUpdate . status . statusId ) { updateResult . addDifference ( "Couldn't update status" ) ; } return updateResult ; } /**
     * Returns promise, that resolves to list of issues assigned to api key owner
     */ getIssuesAssignedToMe ( ) { return this . doRequest < { issues : Issue [ ] ; } > ( "/issues.json?status_id=open&assigned_to_id=me" , "GET" ) ; } /**
     * Returns promise, that resolves to list of open issues for project
     */ getOpenIssuesForProject ( project_id , include_subproject = true ) { if ( include_subproject ) { return this . doRequest < { issues : Issue [ ] ; } > ( `/issues.json?status_id=open&project_id=${ project_id }&subproject_id=!*` , "GET" ) ; } else { return this . doRequest < { issues : Issue [ ] ; } > ( `/issues.json?status_id=open&project_id=${ project_id }` , "GET" ) ; } } compare ( other ) { return ( this . options . address === other . options . address && this . options . key === other . options . key && this . options . rejectUnauthorized === other . options . rejectUnauthorized && isEqual ( this . options . additionalHeaders , other . options . additionalHeaders ) ) ; } }