Skip to content

Commit

Permalink
Merge pull request #85 from gov4git/service-response
Browse files Browse the repository at this point in the history
Improve error handling
  • Loading branch information
dworthen authored Feb 5, 2024
2 parents cd44d10 + 0e6cdda commit ec76292
Show file tree
Hide file tree
Showing 32 changed files with 684 additions and 263 deletions.
5 changes: 5 additions & 0 deletions .changeset/calm-taxis-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'gov4git-desktop-app': patch
---

Improve error handling
6 changes: 3 additions & 3 deletions src/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ async function setup(): Promise<void> {
})
services.register('github', gitHubService)

const gov4GitService = new Gov4GitService(services)
services.register('gov4git', gov4GitService)

const settingsService = new SettingsService({
services,
})
services.register('settings', settingsService)

const gov4GitService = new Gov4GitService(services)
services.register('gov4git', gov4GitService)

const userService = new UserService({
services,
identityRepoName: COMMUNITY_REPO_NAME,
Expand Down
19 changes: 8 additions & 11 deletions src/electron/services/CommunityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,7 @@ ${user.memberPublicBranch}`

private getCommunityMembers = async (community: Community) => {
const command = ['group', 'list', '--name', 'everybody']
return await this.govService.mustRun<string[]>(
command,
community.configPath,
)
return await this.govService.mustRun<string[]>(command, community)
}

private isCommunityMember = async (
Expand Down Expand Up @@ -252,7 +249,7 @@ ${user.memberPublicBranch}`
null,
]
} catch (ex: any) {
return [null, ex.message]
return [null, `${ex}`]
}
}

Expand Down Expand Up @@ -324,7 +321,10 @@ ${user.memberPublicBranch}`
await this.selectCommunity(community.url)
}

await this.settingsService.generateConfig(user, currentCommunity)
const initializationResults = await this.govService.initId()
if (!initializationResults.ok) {
return [initializationResults.error]
}

const syncedCommunity = await this.syncCommunity(user, currentCommunity)

Expand Down Expand Up @@ -400,10 +400,7 @@ ${user.memberPublicBranch}`
'--asset',
'plural',
]
const credits = await this.govService.mustRun<number>(
command,
community.configPath,
)
const credits = await this.govService.mustRun<number>(command, community)
return { username, credits }
}

Expand Down Expand Up @@ -450,7 +447,7 @@ ${user.memberPublicBranch}`
'--quantity',
credits,
]
await this.govService.mustRun(command, community.configPath)
await this.govService.mustRun(command, community)
}

public getCommunityIssues = async (communityUrl: string) => {
Expand Down
74 changes: 50 additions & 24 deletions src/electron/services/GitHubService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { request } from '@octokit/request'

import { retryAsync } from '../../shared/index.js'
import { LogService } from './LogService.js'
import { Services } from './Services.js'

export type GitHubServiceOptions = {
Expand Down Expand Up @@ -103,13 +104,15 @@ export type GetOrgReposArgs = {

export class GitHubService {
protected declare readonly services: Services
protected declare readonly log: LogService
protected declare readonly authenticate: OAuthAppAuthInterface
protected declare authVerification: Verification | null
protected declare oauthTokenInfo: Promise<OAuthAppAuthentication> | null
protected declare clientId: string

constructor({ services, clientId }: GitHubServiceOptions) {
this.services = services
this.log = this.services.load<LogService>('log')
this.clientId = clientId
}

Expand Down Expand Up @@ -162,7 +165,7 @@ export class GitHubService {
try {
tokenInfo = await this.oauthTokenInfo
} catch (ex: any) {
return [null, [`Failed to log in. ${JSON.stringify(ex.response.data)}`]]
return [null, [`Failed to log in. ${JSON.stringify(ex, undefined, 2)}`]]
}

const scopes = tokenInfo.scopes[0]!.split(',')
Expand Down Expand Up @@ -192,20 +195,26 @@ export class GitHubService {
) as typeof request
}

public getAuthenticatedUser = async (token: string) => {
return await this.reqWithAuth(token)('GET /user')
}

public run = async (
req: typeof request,
expectedStatus: number,
...args: Parameters<typeof request>
): ReturnType<typeof request> => {
const response = await req(...args)
if (response.status !== expectedStatus) {
throw response
try {
const response = await req(...args)
if (response.status !== expectedStatus) {
throw new Error(JSON.stringify(response, undefined, 2))
}
this.log.info('GitHub Request:')
this.log.info(JSON.stringify(response, undefined, 2))
return response
} catch (ex) {
throw new Error(JSON.stringify(ex, undefined, 2))
}
return response
}

public getAuthenticatedUser = async (token: string) => {
return await this.run(this.reqWithAuth(token), 200, 'GET /user')
}

public getRepoInfo = async ({
Expand All @@ -229,20 +238,28 @@ export class GitHubService {
const response = await this.getRepoInfo(args)
return response.data.default_branch as string
} catch (ex: any) {
if (ex.status === 404) {
throw new Error(
`${args.repoName} does not exist on GitHub. Please verify that the repo exists and is public.`,
)
} else if (ex.status === 403) {
throw new Error(
`Unauthorized. Insufficient privileges to access ${args.repoName}.`,
)
if (ex instanceof Error) {
try {
const error = JSON.parse(ex.message)
if ('status' in error) {
if (error.status === 404) {
throw new Error(
`${args.repoName} does not exist on GitHub. Please verify that the repo exists and is public.`,
)
} else if (error.status === 403) {
throw new Error(
`Unauthorized. Insufficient privileges to access ${args.repoName}.`,
)
}
}
throw new Error(
`Failed to retrieve default branch for ${args.repoName}. ${ex}`,
)
} catch (error) {
throw ex
}
} else {
throw new Error(
`Failed to retrieve default branch for ${
args.repoName
}. ${JSON.stringify(ex)}`,
)
throw new Error(`${ex}`)
}
}
}
Expand Down Expand Up @@ -381,8 +398,17 @@ export class GitHubService {
},
)
} catch (ex: any) {
if (ex.status !== 404) {
throw ex
if (ex instanceof Error) {
try {
const error = JSON.parse(ex.message)
if ('status' in error && error.status !== 404) {
throw ex
}
} catch (error) {
throw ex
}
} else {
throw new Error(`${ex}`)
}
}
}
Expand Down
149 changes: 111 additions & 38 deletions src/electron/services/Gov4GitService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,71 +2,97 @@ import { runGov4Git } from '@gov4git/js-client'
import { eq } from 'drizzle-orm'
import { existsSync } from 'fs'

import { retryAsync } from '../../shared/index.js'
import { retryAsync, ServiceResponse } from '../../shared/index.js'
import { DB } from '../db/db.js'
import { communities } from '../db/schema.js'
import { communities, type Community, type User, users } from '../db/schema.js'
import { urlToRepoSegments } from '../lib/index.js'
import { parseStdout } from '../lib/stdout.js'
import { GitHubService } from './GitHubService.js'
import { LogService } from './LogService.js'
import { Services } from './Services.js'
import { SettingsService } from './SettingsService.js'

export class Gov4GitService {
protected declare readonly services: Services
protected declare readonly log: LogService
protected declare readonly db: DB
private declare readonly services: Services
private declare readonly log: LogService
private declare readonly db: DB
private declare readonly settingsService: SettingsService
private declare readonly gitHubService: GitHubService

constructor(services: Services) {
this.services = services
this.log = this.services.load<LogService>('log')
this.db = this.services.load<DB>('db')
this.settingsService = this.services.load<SettingsService>('settings')
this.gitHubService = this.services.load<GitHubService>('github')
}

private checkConfigPath = (configPath: string) => {
if (!existsSync(configPath)) {
throw new Error(
`Unable to run Gov4Git command as config ${configPath} does not exist`,
)
}
private getUser = async (): Promise<User | null> => {
return (await this.db.select().from(users).limit(1))[0] ?? null
}

private getCommunity = async (): Promise<Community | null> => {
return (
(
await this.db
.select()
.from(communities)
.where(eq(communities.selected, true))
.limit(1)
)[0] ?? null
)
}

private getConfigPath = async (): Promise<string> => {
const selectedCommunity = (
await this.db
.select()
.from(communities)
.where(eq(communities.selected, true))
.limit(1)
)[0]
private loadConfigPath = async (
community?: Community,
): Promise<ServiceResponse<string>> => {
const selectedCommunity = community ?? (await this.getCommunity())

if (selectedCommunity == null) {
throw new Error(
`Unable to run Gov4Git command as config is not provided.`,
)
return {
ok: false,
statusCode: 401,
error: `Failed to load config path. Please select a community.`,
}
}

this.checkConfigPath(selectedCommunity.configPath)
if (!existsSync(selectedCommunity?.configPath)) {
const user = await this.getUser()
if (user == null) {
return {
ok: false,
statusCode: 401,
error: `Unauthenticated. Failed to load config path. Please login.`,
}
}
const response = await this.settingsService.generateConfig(
user,
selectedCommunity,
)
if (!response.ok) {
return response
}
}

return selectedCommunity.configPath
return {
ok: true,
statusCode: 200,
data: selectedCommunity.configPath,
}
}

public mustRun = async <T>(
command: string[],
configPath?: string,
community?: Community,
skipConfig = false,
): Promise<T> => {
try {
if (configPath != null) {
this.checkConfigPath(configPath)
} else if (!skipConfig) {
configPath = await this.getConfigPath()
}
} catch (ex) {
throw new Error(
`Failed to run Gov4Git command ${command.join(' ')}. ${ex}`,
)
}

if (!skipConfig) {
command.push('--config', configPath!)
const configPathResponse = await this.loadConfigPath(community)
if (!configPathResponse.ok) {
throw new Error(`${configPathResponse.error}`)
} else {
command.push('--config', configPathResponse.data)
}
}

command.push('-v')
Expand All @@ -91,4 +117,51 @@ export class Gov4GitService {
throw ex
}
}

public initId = async (): Promise<ServiceResponse<null>> => {
const user = await this.getUser()
if (user == null) {
return {
ok: false,
statusCode: 401,
error: `Unauthenticed. Failed to initialize Gov4Git identity repos. Please login.`,
}
}
const publicRepoSegments = urlToRepoSegments(user.memberPublicUrl)
const isPublicEmpty = !(await this.gitHubService.hasCommits({
repoName: publicRepoSegments.repo,
username: publicRepoSegments.owner,
token: user.pat,
}))

const privateRepoSegments = urlToRepoSegments(user.memberPrivateUrl)
const isPrivateEmpty = !(await this.gitHubService.hasCommits({
repoName: privateRepoSegments.repo,
username: privateRepoSegments.owner,
token: user.pat,
}))

if (isPublicEmpty || isPrivateEmpty) {
try {
await this.mustRun(['init-id'])
return {
ok: true,
statusCode: 201,
data: null,
}
} catch (ex) {
return {
ok: false,
statusCode: 500,
error: `Failed to initialize Gov4Git identity repos. ${ex}`,
}
}
}

return {
ok: true,
statusCode: 200,
data: null,
}
}
}
Loading

0 comments on commit ec76292

Please sign in to comment.