MrRefactoring / jira.js

A JavaScript/TypeScript wrapper for the JIRA Cloud, Service Desk and Agile REST API
https://mrrefactoring.github.io/jira.js/
MIT License
349 stars 46 forks source link

Webhook and header types #294

Open NatoBoram opened 6 months ago

NatoBoram commented 6 months ago

I realize this might be out of scope for this library, but I would like for this library to expose types for Jira's webhooks and the headers that come with it.

For example, here's a Jira headers, taken from https://developer.atlassian.com/cloud/jira/software/webhooks/

export interface JiraHeaders {
    /** Every webhook contains the `X-Atlassian-Webhook-Identifier` header that
     * provides an identifier for a webhook. This identifier is unique within a
     * Jira Cloud tenant and is the same across retries. After you have processed
     * a webhook, you can use the identifier to filter out retries. */
    readonly "X-Atlassian-Webhook-Identifier": string
    /** The `X-Atlassian-Webhook-Retry` header with the current retry count is
     * included with webhooks that have been retried. Monitor this header and
     * cross-reference it with the callback server logs to stay on top of any
     * unexpected reliability problems. */
    readonly "X-Atlassian-Webhook-Retry": number
    /** Each webhook contains `X-Atlassian-Webhook-Flow` header with `"Primary"`
     * or `"Secondary"` value.
     *
     * All Primary webhooks should be delivered within 30 seconds.
     *
     * All Secondary webhooks are a result of long-lasting bulk or cascade
     * operation (bulk issue update, project deletion, issue deletion etc.).
     * those webhooks, the expected delivery time requirements are relaxed,
     * the delivery should take no more than 15 minutes.
     *
     * Note, in those cases, the top level webhook is transferred via Primary
     * flow, and all dependent webhooks are transferred using the Secondary flow.
     * For example, when deleting an issue, the issue_deleted event is transferred
     * as Primary but all dependent `commend_deleted`, `attachment_deleted`
     * `issuelink_deleted` etc. are Secondary. */
    readonly "X-Atlassian-Webhook-Flow": "Primary" | "Secondary"
    /** To trace the origin of a webhook, Connect apps can attach the additional
     * `X-Atlassian-Webhook-Trace` HTTP header with any value consisting of a
     * string of up to 1024 printable ASCII characters to a REST API request.
     *
     * The header and its value are then attached to every webhook sent from Jira for the REST API request.
     *
     * The app can use the webhook trace header to, for example:
     *
     * * Differentiate between webhooks triggered by various REST API requests.
     * * Track a webhook’s delivery.
     * * Attach other data for use when a webhook arrives.
     */
    readonly "X-Atlassian-Webhook-Trace": string
}
MrRefactoring commented 6 months ago

Hello @NatoBoram! Could you please define which exact endpoints should to have these headers?

NatoBoram commented 6 months ago

If you go to https://webhook.site then create a WebHook in Jira at https://${USER}.atlassian.net/plugins/servlet/webhooks pointing to your custom listener, you can receive these headers.

For example, I made a test board with a test issue and I received these headers:

connection: close
accept-encoding: gzip,deflate
user-agent: Atlassian Webhook HTTP Client
host: webhook.site
content-type: application/json; charset=UTF-8
content-length: 7157
x-b3-sampled: 0
x-b3-spanid: 6349452bd099e495
x-b3-traceid: a4ecf355b72258e87c3b2215d1628c5c
accept: */*
x-atlassian-webhook-flow: Primary
x-atlassian-webhook-identifier: 69312270414585119

I also got this request body:

{
    "timestamp": 1704303069063,
    "webhookEvent": "jira:issue_created",
    "issue_event_type_name": "issue_created",
    "user": {
        "self": "https://natoboram.atlassian.net/rest/api/2/user?accountId=5c19e3e3f9cb4734ae1b918e",
        "accountId": "5c19e3e3f9cb4734ae1b918e",
        "avatarUrls": {
            "48x48": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
            "24x24": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
            "16x16": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
            "32x32": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png"
        },
        "displayName": "Nato Boram",
        "active": true,
        "timeZone": "America/Toronto",
        "accountType": "atlassian"
    },
    "issue": {
        "id": "10000",
        "self": "https://natoboram.atlassian.net/rest/api/2/10000",
        "key": "TEST-1",
        "fields": {
            "statuscategorychangedate": "2024-01-03T12:31:09.209-0500",
            "issuetype": {
                "self": "https://natoboram.atlassian.net/rest/api/2/issuetype/10001",
                "id": "10001",
                "description": "Tasks track small, distinct pieces of work.",
                "iconUrl": "https://natoboram.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium",
                "name": "Task",
                "subtask": false,
                "avatarId": 10318,
                "entityId": "9baea660-ceab-4eaa-8046-9f53e3558981",
                "hierarchyLevel": 0
            },
            "timespent": null,
            "customfield_10030": null,
            "customfield_10031": null,
            "project": {
                "self": "https://natoboram.atlassian.net/rest/api/2/project/10000",
                "id": "10000",
                "key": "TEST",
                "name": "Test Project",
                "projectTypeKey": "software",
                "simplified": true,
                "avatarUrls": {
                    "48x48": "https://natoboram.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10423",
                    "24x24": "https://natoboram.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10423?size=small",
                    "16x16": "https://natoboram.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10423?size=xsmall",
                    "32x32": "https://natoboram.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10423?size=medium"
                }
            },
            "customfield_10032": null,
            "fixVersions": [],
            "aggregatetimespent": null,
            "resolution": null,
            "customfield_10027": null,
            "customfield_10028": null,
            "customfield_10029": null,
            "resolutiondate": null,
            "workratio": -1,
            "lastViewed": null,
            "watches": {
                "self": "https://natoboram.atlassian.net/rest/api/2/issue/TEST-1/watchers",
                "watchCount": 0,
                "isWatching": true
            },
            "issuerestriction": { "issuerestrictions": {}, "shouldDisplay": true },
            "created": "2024-01-03T12:31:08.939-0500",
            "customfield_10020": null,
            "customfield_10021": null,
            "customfield_10022": null,
            "customfield_10023": null,
            "priority": {
                "self": "https://natoboram.atlassian.net/rest/api/2/priority/3",
                "iconUrl": "https://natoboram.atlassian.net/images/icons/priorities/medium.svg",
                "name": "Medium",
                "id": "3"
            },
            "customfield_10024": null,
            "customfield_10025": null,
            "customfield_10026": null,
            "labels": [],
            "customfield_10016": null,
            "customfield_10017": null,
            "customfield_10018": {
                "hasEpicLinkFieldDependency": false,
                "showField": false,
                "nonEditableReason": {
                    "reason": "PLUGIN_LICENSE_ERROR",
                    "message": "The Parent Link is only available to Jira Premium users."
                }
            },
            "customfield_10019": "0|hzzzzz:",
            "aggregatetimeoriginalestimate": null,
            "timeestimate": null,
            "versions": [],
            "issuelinks": [],
            "assignee": null,
            "updated": "2024-01-03T12:31:08.939-0500",
            "status": {
                "self": "https://natoboram.atlassian.net/rest/api/2/status/10000",
                "description": "",
                "iconUrl": "https://natoboram.atlassian.net/",
                "name": "To Do",
                "id": "10000",
                "statusCategory": {
                    "self": "https://natoboram.atlassian.net/rest/api/2/statuscategory/2",
                    "id": 2,
                    "key": "new",
                    "colorName": "blue-gray",
                    "name": "New"
                }
            },
            "components": [],
            "timeoriginalestimate": null,
            "description": null,
            "customfield_10010": null,
            "customfield_10014": null,
            "timetracking": {},
            "customfield_10015": null,
            "customfield_10005": null,
            "customfield_10006": null,
            "security": null,
            "customfield_10007": null,
            "customfield_10008": null,
            "aggregatetimeestimate": null,
            "attachment": [],
            "customfield_10009": null,
            "summary": "This is a new issue, created in Jira.",
            "creator": {
                "self": "https://natoboram.atlassian.net/rest/api/2/user?accountId=5c19e3e3f9cb4734ae1b918e",
                "accountId": "5c19e3e3f9cb4734ae1b918e",
                "avatarUrls": {
                    "48x48": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
                    "24x24": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
                    "16x16": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
                    "32x32": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png"
                },
                "displayName": "Nato Boram",
                "active": true,
                "timeZone": "America/Toronto",
                "accountType": "atlassian"
            },
            "subtasks": [],
            "reporter": {
                "self": "https://natoboram.atlassian.net/rest/api/2/user?accountId=5c19e3e3f9cb4734ae1b918e",
                "accountId": "5c19e3e3f9cb4734ae1b918e",
                "avatarUrls": {
                    "48x48": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
                    "24x24": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
                    "16x16": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png",
                    "32x32": "https://secure.gravatar.com/avatar/bdc89ce7df863d04f08e3b1c980938d3?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNB-1.png"
                },
                "displayName": "Nato Boram",
                "active": true,
                "timeZone": "America/Toronto",
                "accountType": "atlassian"
            },
            "aggregateprogress": { "progress": 0, "total": 0 },
            "customfield_10001": null,
            "customfield_10002": null,
            "customfield_10003": null,
            "customfield_10004": null,
            "environment": null,
            "duedate": null,
            "progress": { "progress": 0, "total": 0 },
            "votes": {
                "self": "https://natoboram.atlassian.net/rest/api/2/issue/TEST-1/votes",
                "votes": 0,
                "hasVoted": false
            }
        }
    },
    "changelog": {
        "id": "10000",
        "items": [
            {
                "field": "priority",
                "fieldtype": "jira",
                "fieldId": "priority",
                "from": null,
                "fromString": null,
                "to": "3",
                "toString": "Medium"
            },
            {
                "field": "reporter",
                "fieldtype": "jira",
                "fieldId": "reporter",
                "from": null,
                "fromString": null,
                "to": "5c19e3e3f9cb4734ae1b918e",
                "toString": "Nato Boram",
                "tmpFromAccountId": null,
                "tmpToAccountId": "5c19e3e3f9cb4734ae1b918e"
            },
            {
                "field": "Status",
                "fieldtype": "jira",
                "fieldId": "status",
                "from": null,
                "fromString": null,
                "to": "10000",
                "toString": "To Do"
            },
            {
                "field": "summary",
                "fieldtype": "jira",
                "fieldId": "summary",
                "from": null,
                "fromString": null,
                "to": null,
                "toString": "This is a new issue, created in Jira."
            }
        ]
    }
}
MrRefactoring commented 6 months ago

Screenshot 2024-01-04 at 00 00 37 I see few endpoints. For which one I should to add headers?

https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-webhooks/#api-rest-api-3-webhook-get

NatoBoram commented 6 months ago

It's not actually for an endpoint on Jira's side, it's for requests that the Jira server itself sends when you add webhooks at https://natoboram.atlassian.net/plugins/servlet/webhooks.

Example:

image

When you receive these events, it looks like this:

image

For example, I'm making a program that interacts with the API to add comments to issues. This can be done with Jira.js. However, this program needs to know when an issue is created, so I'm registering a webhook on Jira's side so Jira can tell me when there's a new issue. This would require additional types that I haven't seen in Jira.js.

MrRefactoring commented 6 months ago

Oh okay I catch it. You wanna extract headers from response

NatoBoram commented 6 months ago

Yep, both headers and body for WebHook types

MrRefactoring commented 5 months ago

How you imagine library can handle this webhook response?

NatoBoram commented 5 months ago

I don't think this library should "handle" them, but rather, expose the types for these. Something like this:

// Example enum for webhook events
const webhookEvents = {
    issue_created: "jira:issue_created",
    issue_updated: "jira:issue_updated",
} as const
type WebhookEvent = (typeof webhookEvents)[keyof typeof webhookEvents]

const issueEventTypeNames = {
    issue_created: "issue_created",
    issue_updated: "issue_updated",
} as const
type IssueEventTypeName =
    (typeof issueEventTypeNames)[keyof typeof issueEventTypeNames]

// Example interfaces for webhook bodies
interface JiraWebHookBase {
    readonly webhookEvent: WebhookEvent
    readonly issue_event_type_name: IssueEventTypeName
}

interface JiraWebHookIssueCreated extends JiraWebHookBase {
    readonly webhookEvent: typeof webhookEvents.issue_created
    readonly issue_event_type_name: typeof issueEventTypeNames.issue_created
}

interface JiraWebHookIssueUpdated extends JiraWebHookBase {
    readonly webhookEvent: typeof webhookEvents.issue_updated
    readonly issue_event_type_name: typeof issueEventTypeNames.issue_updated
}

type JiraWebHook = JiraWebHookIssueCreated | JiraWebHookIssueUpdated

// Example headers for webhooks
interface JiraHeaders {
    readonly "x-atlassian-webhook-flow": "Primary"
}

Someone using this library would just have to import and use these types with whatever router they are using.

const router = express()
router.post("/jira", (req: Request<undefined, undefined, JiraWebHook>, res) => {
    switch (req.body.webhookEvent) {
        case webhookEvents.issue_created:
            // `req.body` is now narrowed to `JiraWebHookIssueCreated`
            console.log("req.body", req.body)
            return res.sendStatus(200)

        case webhookEvents.issue_updated:
            // `req.body` is now narrowed to `JiraWebHookIssueUpdated`
            console.log("req.body", req.body)
            return res.sendStatus(200)

        default:
            // `req.body` is now narrowed to `never`
            console.log("req.body", req.body)
            return res.sendStatus(400)
    }
})
cenobitedk commented 2 weeks ago

Hi @MrRefactoring and @NatoBoram I would like this as well. Currently there's no way to tell what props are available on the incoming webhook body. This library is already great for using the API (thanks @MrRefactoring 🙏) and it would make sense to have it in the same package. Besides I haven't found any other package that provides the types.

There haven't been any comments on this since January, but the issue is still open. Is it still a feature request, or something you are actually working on? :-)