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

Improve error handling #85

Merged
merged 1 commit into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading