Skip to content
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
100 changes: 100 additions & 0 deletions api/jira-provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { assertEquals } from '@std/assert'
import { describe, it } from '@std/testing/bdd'
import { createJiraProvider } from './jira-provider.ts'

const issue = {
id: '10001',
key: 'TNT-393',
Project: {
id: '10017',
key: 'TNT',
name: 'Tournament',
},
Summary: 'Hide settings for no admin tournament user',
Status: {
name: 'Done',
},
Updated: '2026-01-02T12:00:00.000Z',
Created: '2026-01-01T12:00:00.000Z',
Description: 'The settings tab should not be visible to non-admin users.',
Assignee: {
accountId: 'jira-account-1',
emailAddress: 'ada@example.com',
displayName: 'Ada Lovelace',
},
Comment: {
comments: [
{
body: 'Implemented in the tournament frontend.',
author: {
accountId: 'jira-account-2',
emailAddress: 'grace@example.com',
displayName: 'Grace Hopper',
},
created: '2026-01-02T10:00:00.000Z',
},
],
},
}

describe('jiraProvider', () => {
it('lists issues scoped by Jira project and maps the assignee', async () => {
let receivedParams: { q?: string } = {}
const provider = createJiraProvider((_path, params) => {
receivedParams = params ?? {}
return Promise.resolve([issue])
})

const items = await provider.list({ jiraProjectKey: 'TNT' })

assertEquals(receivedParams.q, `select(.Project.key == "TNT") | {
id,
key,
Summary,
Status: {
name: .Status.name,
iconUrl: .Status.iconUrl
},
Updated,
Assignee: {
accountId: .Assignee.accountId,
emailAddress: .Assignee.emailAddress
}
}`)
assertEquals(items.length, 1)
assertEquals(items[0].externalId, '10001')
assertEquals(items[0].title, issue.Summary)
assertEquals(items[0].status, issue.Status.name)
assertEquals(items[0].assigneeRefs, [{
jiraAccountId: 'jira-account-1',
email: 'ada@example.com',
}])
})

it('returns no records when the project scope is absent', async () => {
let called = false
const provider = createJiraProvider(() => {
called = true
return Promise.resolve([issue])
})

assertEquals(await provider.list({}), [])
assertEquals(called, false)
})

it('returns description first, followed by Jira comments', async () => {
const provider = createJiraProvider(() => Promise.resolve([issue]))
const [item] = await provider.list({ jiraProjectKey: 'TNT' })
const discussion = await provider.comments(item)

assertEquals(discussion.map((comment) => comment.body), [
issue.Description,
'Implemented in the tournament frontend.',
])
assertEquals(discussion[1].author, 'grace@example.com')
assertEquals(
discussion[1].createdAt,
Date.parse('2026-01-02T10:00:00.000Z'),
)
})
})
174 changes: 174 additions & 0 deletions api/jira-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { get } from './lmdb-store.ts'
import type {
Comment,
PersonRef,
ProjectScope,
Provider,
WorkItem,
} from './tickets.ts'

type JiraUser = {
accountId?: string
emailAddress?: string | null
}

type JiraStatus = {
name?: string
iconUrl?: string
} | null

type JiraText = string | Record<string, unknown>

type JiraComment = {
body?: JiraText
author?: JiraUser | null
created?: string
}

type JiraIssue = {
id?: string
key?: string
Project?: {
key?: string
} | null

Summary?: string | null
Status?: JiraStatus
Updated?: string
Created?: string
Description?: JiraText | null
Assignee?: JiraUser | null
Comment?: {
comments?: JiraComment[]
} | null
}

type StoreGet = (
path: string,
params?: { q?: string },
) => Promise<JiraIssue[]>

const jiraUserRef = (
user: JiraUser | null | undefined,
): PersonRef | undefined => {
if (!user?.accountId && !user?.emailAddress) return undefined
return {
jiraAccountId: user.accountId,
email: user.emailAddress ?? undefined,
}
}

const toWorkItem = (issue: JiraIssue): WorkItem | undefined => {
if (!issue.id || !issue.key) return undefined

const assignee = jiraUserRef(issue.Assignee)
return {
source: 'jira',
externalId: issue.id,
externalUrl: issue.Status?.iconUrl
? `${new URL(issue.Status.iconUrl).origin}/browse/${
encodeURIComponent(issue.key)
}`
: `/browse/${encodeURIComponent(issue.key)}`,
title: issue.Summary ?? issue.key,
status: typeof issue.Status === 'string'
? issue.Status
: issue.Status?.name,
updatedAt: issue.Updated ? Date.parse(issue.Updated) : undefined,
assigneeRefs: assignee ? [assignee] : [],
raw: issue,
}
}

const toComment = (
item: WorkItem,
body: JiraText | null | undefined,
author: JiraUser | null | undefined,
created: string | undefined,
): Comment | undefined => {
if (typeof body !== 'string' || !body) return undefined
return {
source: 'jira',
author: author?.emailAddress ?? undefined,
body,
url: item.externalUrl,
createdAt: created ? Date.parse(created) : undefined,
}
}

export const createJiraProvider = (
storeGet: StoreGet = (path, params) => get<JiraIssue[]>(path, params),
): Provider => ({
id: 'jira',

async list(scope: ProjectScope): Promise<WorkItem[]> {
if (!scope.jiraProjectKey) return []
const issues = await storeGet('jira/issue', {
q: `select(.Project.key == "${scope.jiraProjectKey}") | {
id,
key,
Summary,
Status: {
name: .Status.name,
iconUrl: .Status.iconUrl
},
Updated,
Assignee: {
accountId: .Assignee.accountId,
emailAddress: .Assignee.emailAddress
}
}`,
})
const items: WorkItem[] = []
for (const issue of issues) {
const item = toWorkItem(issue)
if (item) items.push(item)
}
return items
},

async comments(item: WorkItem): Promise<Comment[]> {
const issues = await storeGet('jira/issue', {
q: `select(.id == "${item.externalId}") | {
id,
key,
Summary,
Created,
Description,
Comment: {
comments: ((.Comment.comments // []) | map({
body,
created,
author: { emailAddress: .author.emailAddress }
}))
}
}`,
})

if (issues.length === 0 || !issues[0]) return []
const issue = issues[0]

const comments: Comment[] = []
const description = toComment(
item,
issue.Description,
undefined,
issue.Created,
)
if (description) comments.push(description)

for (const record of issue.Comment?.comments ?? []) {
const comment = toComment(
item,
record.body,
record.author,
record.created,
)
if (comment) comments.push(comment)
}

return comments
},
})

export const jiraProvider = createJiraProvider()
Loading