@rawdash/connector-jira 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # @rawdash/connector-jira
2
+
3
+ Rawdash connector for [Jira Cloud](https://www.atlassian.com/software/jira) — syncs issues, issue status-change events, sprints, projects, and users into the six-shape storage model. Extends the engineering vertical beyond Linear-native shops.
4
+
5
+ > **Cloud only.** This connector targets the Jira Cloud REST API v3 and Agile API. Jira Data Center / Server are out of scope for v0.1.
6
+
7
+ ## Auth setup
8
+
9
+ The connector authenticates with **Basic auth** using an Atlassian account email and an API token (Atlassian Cloud's recommended pattern for server-to-server access).
10
+
11
+ 1. Sign in to [id.atlassian.com](https://id.atlassian.com) with the account that should own the integration (a service account is recommended so the sync survives staff changes).
12
+ 2. Go to **Security → API tokens → Create API token**.
13
+ 3. Give it a label (e.g. `rawdash`) and copy the token — you won't see it again.
14
+ 4. Store the token wherever your rawdash deployment resolves secrets from (e.g. the `JIRA_API_TOKEN` env var).
15
+
16
+ The account only needs read access (Browse Projects permission) to the projects you want to sync. No webhook or app installation is required.
17
+
18
+ > The token's permissions cap what the connector can see. To sync sprints you also need the account to have access to the relevant Scrum boards.
19
+
20
+ ## Configuration
21
+
22
+ ```ts
23
+ import { secret } from '@rawdash/core';
24
+
25
+ const jira = {
26
+ name: 'jira',
27
+ connectorId: 'jira',
28
+ config: {
29
+ email: 'bot@yourorg.com',
30
+ apiToken: secret('JIRA_API_TOKEN'),
31
+ host: 'yourorg.atlassian.net', // no protocol, no trailing slash
32
+ // projectKeys: ['ENG', 'OPS'], // optional — restrict to specific project keys
33
+ // resources: ['issues', 'issue_events'], // optional — defaults to all five
34
+ // storyPointsField: 'customfield_10016', // optional — story points custom field id
35
+ // sprintField: 'customfield_10020', // optional — sprint custom field id
36
+ },
37
+ };
38
+ ```
39
+
40
+ Register the connector class when mounting the engine:
41
+
42
+ ```ts
43
+ import { JiraConnector } from '@rawdash/connector-jira';
44
+ import { mountEngine } from '@rawdash/hono';
45
+
46
+ mountEngine(config, { connectorRegistry: { jira: JiraConnector } });
47
+ ```
48
+
49
+ ### Custom field IDs vary per site
50
+
51
+ Story points and the sprint association live on **custom fields** whose IDs differ between Jira sites. The defaults (`customfield_10016` for story points, `customfield_10020` for sprint) match most company-managed Scrum projects, but if your numbers come up empty, find the right IDs at **`https://<host>/rest/api/3/field`** and set `storyPointsField` / `sprintField`.
52
+
53
+ ### Choosing resources
54
+
55
+ The connector exposes five resources, written across four internal sync phases:
56
+
57
+ | Resource | Phase | What gets written |
58
+ | -------------- | -------- | -------------------------------------------------------------------------------------- |
59
+ | `projects` | projects | `jira_project` entities, one per project |
60
+ | `users` | users | `jira_user` entities, one per Atlassian account |
61
+ | `sprints` | sprints | `jira_sprint` entities, one per sprint across every Scrum board the account can see |
62
+ | `issues` | issues | `jira_issue` entities, one per issue |
63
+ | `issue_events` | issues | `jira_issue_status_change` events, one per status transition in each issue's changelog |
64
+
65
+ `issue_events` shares the `issues` phase because the events are derived from the changelog returned alongside each issue (`expand=changelog`). Enabling `issue_events` without `issues` still runs the issue search (so the events have data) but skips writing the issue entities themselves.
66
+
67
+ ### Example dashboard
68
+
69
+ ```ts
70
+ import { defineConfig, defineDashboard, defineMetric } from '@rawdash/core';
71
+
72
+ export default defineConfig({
73
+ connectors: [jira],
74
+ dashboards: {
75
+ delivery: defineDashboard({
76
+ widgets: {
77
+ completed_7d: {
78
+ kind: 'stat',
79
+ title: 'Issues completed (7d)',
80
+ metric: defineMetric({
81
+ connector: jira,
82
+ shape: 'event',
83
+ name: 'jira_issue_status_change',
84
+ field: 'start_ts',
85
+ fn: 'count',
86
+ window: '7d',
87
+ filter: [{ field: 'toStatus', op: 'eq', value: 'Done' }],
88
+ }),
89
+ },
90
+ issues_by_status: {
91
+ kind: 'distribution',
92
+ title: 'Open issues by status',
93
+ metric: defineMetric({
94
+ connector: jira,
95
+ shape: 'entity',
96
+ type: 'jira_issue',
97
+ fn: 'count',
98
+ groupBy: { field: 'statusName' },
99
+ }),
100
+ },
101
+ transitions_per_day: {
102
+ kind: 'timeseries',
103
+ title: 'Status transitions per day',
104
+ window: '14d',
105
+ metric: defineMetric({
106
+ connector: jira,
107
+ shape: 'event',
108
+ name: 'jira_issue_status_change',
109
+ field: 'start_ts',
110
+ fn: 'count',
111
+ window: '14d',
112
+ groupBy: { field: 'start_ts', granularity: 'day' },
113
+ }),
114
+ },
115
+ },
116
+ }),
117
+ },
118
+ });
119
+ ```
120
+
121
+ ## Data model
122
+
123
+ | Storage shape | Entity/event type | Key attributes |
124
+ | ------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
125
+ | entity | `jira_project` | key, name, projectTypeKey, leadAccountId, leadDisplayName |
126
+ | entity | `jira_user` | displayName, emailAddress, accountType, active |
127
+ | entity | `jira_sprint` | name, state, boardId, originBoardId, startDate, endDate, completeDate |
128
+ | entity | `jira_issue` | key, summary, statusName, statusCategory, priority, issueType, assigneeId, reporterId, projectKey, sprintId, storyPoints, createdAt, resolvedAt |
129
+ | event | `jira_issue_status_change` | historyId, issueId, issueKey, projectKey, authorId, fromStatus, toStatus. `start_ts` = changelog entry time, `end_ts` = null. |
130
+
131
+ Timestamps are stored as Unix epoch milliseconds. `sprintId` is taken from the most recent sprint on the issue's sprint custom field. `jira_project` / `jira_user` have no source-side update timestamp, so their `updated_at` is the sync time.
132
+
133
+ ## Schemas
134
+
135
+ `JiraConnector.schemas` declares the Zod schema for each resource's raw API response. Used by the cloud shape-drift pipeline to populate `connector_baselines`, and by the package's property tests.
136
+
137
+ | Resource | Represents |
138
+ | ---------- | ---------------------------------------------- |
139
+ | `projects` | `GET /rest/api/3/project/search` page |
140
+ | `users` | `GET /rest/api/3/users/search` page |
141
+ | `sprints` | `GET /rest/agile/1.0/board/{id}/sprint` values |
142
+ | `issues` | `GET /rest/api/3/search/jql` page |
143
+
144
+ ## Sync behaviour
145
+
146
+ - **Backfill** (`mode: 'full'`): paginates each phase and clears the phase's entity/event scope on the first page so deletions in Jira converge.
147
+ - **projects** — `GET /rest/api/3/project/search?expand=lead`, `startAt` pagination via the response's `isLast` flag.
148
+ - **users** — `GET /rest/api/3/users/search`, `startAt` pagination (terminates when a short page is returned).
149
+ - **sprints** — lists Scrum boards via `GET /rest/agile/1.0/board`, then pulls each board's sprints from `GET /rest/agile/1.0/board/{id}/sprint`. Non-Scrum boards (kanban) are skipped because they don't support sprints.
150
+ - **issues** — `GET /rest/api/3/search/jql` with `expand=changelog`, paginated via the response's `nextPageToken`.
151
+ - **Incremental** (`mode: 'latest'`): the issues query adds a `updated >= "<UTC yyyy-MM-dd HH:mm>"` JQL clause so only issues changed since the last sync are pulled, ordered by `updated ASC`. Changelog entries at or before the `since` cutoff are dropped so status-change events aren't re-emitted. Projects, users, and sprints are small, so they are fully refreshed on every sync.
152
+ - **Rate limits**: Jira applies cost-based rate limiting and returns `429` with a `Retry-After` header — the shared HTTP client surfaces these as `RateLimitError` and the host backs off.
153
+ - **Resumable**: every paginated phase yields a `{ phase, page }` cursor (`ChunkedSyncCursor`). For `startAt`-based phases `page` is the numeric offset; for issues it is the opaque `nextPageToken`.
154
+
155
+ ## Errors
156
+
157
+ `@rawdash/connector-shared` maps Jira's HTTP responses to typed errors automatically:
158
+
159
+ - `401` / `403` → `AuthError` — host stops syncing until the credentials are replaced.
160
+ - `429` → `RateLimitError` — host backs off and reschedules.
161
+ - `5xx` → `TransientError` — host retries on the next tick.
162
+
163
+ ## Out of scope (post-v0.1)
164
+
165
+ - **Jira Data Center / Server** — Cloud only for v0.1.
166
+ - **Confluence** — a separate connector if needed.
167
+ - **Worklogs / time tracking** — not dashboard-shaped for the initial release.
168
+
169
+ ## Property tests
170
+
171
+ Resources in this connector have fast-check property tests under `src/property.test.ts` that:
172
+
173
+ 1. Generate synthetic API payloads from a Zod schema mirroring Jira's response shape.
174
+ 2. Pipe them through `connector.sync()` against an `InMemoryStorage` instance.
175
+ 3. Assert universal invariants — non-empty entity ids, finite event timestamps, no `undefined` reaching storage, no thrown errors on any valid input — plus per-resource counts.
176
+
177
+ The helper lives in `@rawdash/connector-test-utils`. When adding a new resource, add a Zod schema for its payload and a test wired up via `runPropertySyncTest`.
@@ -0,0 +1,171 @@
1
+ import { BaseConnector, ConnectorContext, SyncOptions, StorageHandle, SyncResult } from '@rawdash/core';
2
+ import { z } from 'zod';
3
+
4
+ declare const configFields: z.ZodObject<{
5
+ email: z.ZodString;
6
+ apiToken: z.ZodObject<{
7
+ $secret: z.ZodString;
8
+ }, z.core.$strip>;
9
+ host: z.ZodString;
10
+ projectKeys: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
+ resources: z.ZodOptional<z.ZodArray<z.ZodEnum<{
12
+ projects: "projects";
13
+ users: "users";
14
+ sprints: "sprints";
15
+ issues: "issues";
16
+ issue_events: "issue_events";
17
+ }>>>;
18
+ storyPointsField: z.ZodOptional<z.ZodString>;
19
+ sprintField: z.ZodOptional<z.ZodString>;
20
+ }, z.core.$strip>;
21
+ type JiraResource = 'projects' | 'users' | 'sprints' | 'issues' | 'issue_events';
22
+ interface JiraSettings {
23
+ host: string;
24
+ projectKeys?: readonly string[];
25
+ resources?: readonly JiraResource[];
26
+ storyPointsField?: string;
27
+ sprintField?: string;
28
+ }
29
+ declare const jiraCredentials: {
30
+ email: {
31
+ description: string;
32
+ auth: "required";
33
+ };
34
+ apiToken: {
35
+ description: string;
36
+ auth: "required";
37
+ };
38
+ };
39
+ type JiraCredentials = typeof jiraCredentials;
40
+ declare class JiraConnector extends BaseConnector<JiraSettings, JiraCredentials> {
41
+ static readonly id = "jira";
42
+ static readonly schemas: {
43
+ readonly projects: z.ZodObject<{
44
+ values: z.ZodArray<z.ZodObject<{
45
+ id: z.ZodString;
46
+ key: z.ZodString;
47
+ name: z.ZodString;
48
+ projectTypeKey: z.ZodOptional<z.ZodNullable<z.ZodString>>;
49
+ lead: z.ZodOptional<z.ZodNullable<z.ZodObject<{
50
+ accountId: z.ZodString;
51
+ displayName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
52
+ }, z.core.$strip>>>;
53
+ }, z.core.$strip>>;
54
+ isLast: z.ZodBoolean;
55
+ startAt: z.ZodNumber;
56
+ maxResults: z.ZodNumber;
57
+ total: z.ZodNumber;
58
+ }, z.core.$strip>;
59
+ readonly users: z.ZodArray<z.ZodObject<{
60
+ accountId: z.ZodString;
61
+ displayName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
62
+ emailAddress: z.ZodOptional<z.ZodNullable<z.ZodString>>;
63
+ accountType: z.ZodOptional<z.ZodNullable<z.ZodString>>;
64
+ active: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
65
+ }, z.core.$strip>>;
66
+ readonly sprints: z.ZodArray<z.ZodObject<{
67
+ id: z.ZodNumber;
68
+ name: z.ZodString;
69
+ state: z.ZodEnum<{
70
+ active: "active";
71
+ closed: "closed";
72
+ future: "future";
73
+ }>;
74
+ startDate: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
75
+ endDate: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
76
+ completeDate: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
77
+ originBoardId: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
78
+ }, z.core.$strip>>;
79
+ readonly issues: z.ZodObject<{
80
+ issues: z.ZodArray<z.ZodObject<{
81
+ id: z.ZodString;
82
+ key: z.ZodString;
83
+ fields: z.ZodObject<{
84
+ summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
85
+ status: z.ZodOptional<z.ZodNullable<z.ZodObject<{
86
+ name: z.ZodString;
87
+ statusCategory: z.ZodOptional<z.ZodNullable<z.ZodObject<{
88
+ key: z.ZodString;
89
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
90
+ }, z.core.$strip>>>;
91
+ }, z.core.$strip>>>;
92
+ priority: z.ZodOptional<z.ZodNullable<z.ZodObject<{
93
+ name: z.ZodString;
94
+ }, z.core.$strip>>>;
95
+ issuetype: z.ZodOptional<z.ZodNullable<z.ZodObject<{
96
+ name: z.ZodString;
97
+ }, z.core.$strip>>>;
98
+ assignee: z.ZodOptional<z.ZodNullable<z.ZodObject<{
99
+ accountId: z.ZodString;
100
+ displayName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
101
+ }, z.core.$strip>>>;
102
+ reporter: z.ZodOptional<z.ZodNullable<z.ZodObject<{
103
+ accountId: z.ZodString;
104
+ displayName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
105
+ }, z.core.$strip>>>;
106
+ project: z.ZodNullable<z.ZodObject<{
107
+ id: z.ZodString;
108
+ key: z.ZodString;
109
+ }, z.core.$strip>>;
110
+ created: z.ZodISODateTime;
111
+ updated: z.ZodISODateTime;
112
+ resolutiondate: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
113
+ customfield_10016: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
114
+ customfield_10020: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
115
+ id: z.ZodNumber;
116
+ name: z.ZodString;
117
+ }, z.core.$strip>>>>;
118
+ }, z.core.$strip>;
119
+ changelog: z.ZodObject<{
120
+ histories: z.ZodArray<z.ZodObject<{
121
+ id: z.ZodString;
122
+ created: z.ZodISODateTime;
123
+ author: z.ZodOptional<z.ZodNullable<z.ZodObject<{
124
+ accountId: z.ZodString;
125
+ displayName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
126
+ }, z.core.$strip>>>;
127
+ items: z.ZodArray<z.ZodObject<{
128
+ field: z.ZodString;
129
+ fromString: z.ZodOptional<z.ZodNullable<z.ZodString>>;
130
+ toString: z.ZodOptional<z.ZodNullable<z.ZodString>>;
131
+ }, z.core.$strip>>;
132
+ }, z.core.$strip>>;
133
+ }, z.core.$strip>;
134
+ }, z.core.$strip>>;
135
+ nextPageToken: z.ZodOptional<z.ZodNullable<z.ZodString>>;
136
+ isLast: z.ZodOptional<z.ZodBoolean>;
137
+ }, z.core.$strip>;
138
+ };
139
+ static create(input: unknown, ctx?: ConnectorContext): JiraConnector;
140
+ readonly id = "jira";
141
+ readonly credentials: {
142
+ email: {
143
+ description: string;
144
+ auth: "required";
145
+ };
146
+ apiToken: {
147
+ description: string;
148
+ auth: "required";
149
+ };
150
+ };
151
+ private get baseUrl();
152
+ private get storyPointsField();
153
+ private get sprintField();
154
+ private buildHeaders;
155
+ private fetch;
156
+ private activePhases;
157
+ private buildJql;
158
+ private fetchProjectsPage;
159
+ private fetchUsersPage;
160
+ private fetchBoardsPage;
161
+ private fetchSprintsForBoard;
162
+ private fetchSprintsPage;
163
+ private fetchIssuesPage;
164
+ private writeProjects;
165
+ private writeUsers;
166
+ private writeSprints;
167
+ private writeIssues;
168
+ sync(options: SyncOptions, storage: StorageHandle, signal?: AbortSignal): Promise<SyncResult>;
169
+ }
170
+
171
+ export { JiraConnector, type JiraResource, type JiraSettings, configFields, JiraConnector as default };
package/dist/index.js ADDED
@@ -0,0 +1,617 @@
1
+ // ../../connector-shared/dist/index.js
2
+ var HTTP_CLIENT_VERSION = "0.0.0";
3
+ var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
4
+ function connectorUserAgent(connectorId) {
5
+ return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
6
+ }
7
+ function parseEpoch(value, unit) {
8
+ if (value === null || value === void 0) {
9
+ return null;
10
+ }
11
+ if (unit === "iso") {
12
+ if (typeof value !== "string") {
13
+ return null;
14
+ }
15
+ const ms = new Date(value).getTime();
16
+ return Number.isFinite(ms) ? ms : null;
17
+ }
18
+ if (typeof value === "string" && value.trim() === "") {
19
+ return null;
20
+ }
21
+ const n = typeof value === "number" ? value : Number(value);
22
+ if (!Number.isFinite(n)) {
23
+ return null;
24
+ }
25
+ const result = unit === "s" ? n * 1e3 : n;
26
+ return Number.isFinite(result) ? result : null;
27
+ }
28
+
29
+ // src/jira.ts
30
+ import {
31
+ BaseConnector,
32
+ defineConfigFields,
33
+ makeChunkedCursorGuard,
34
+ paginateChunked,
35
+ selectActivePhases
36
+ } from "@rawdash/core";
37
+ import { z } from "zod";
38
+ var configFields = defineConfigFields(
39
+ z.object({
40
+ email: z.string().min(1).meta({
41
+ label: "Account email",
42
+ description: "Atlassian account email paired with the API token for Basic auth.",
43
+ placeholder: "you@yourorg.com"
44
+ }),
45
+ apiToken: z.object({ $secret: z.string() }).meta({
46
+ label: "API Token",
47
+ description: "Atlassian API token. Create one at id.atlassian.com \u2192 Security \u2192 API tokens.",
48
+ placeholder: "ATATT...",
49
+ secret: true
50
+ }),
51
+ host: z.string().min(1).regex(
52
+ /^[^/\s:?#]+$/,
53
+ "Use host only (no protocol, port, path, or query)."
54
+ ).meta({
55
+ label: "Site host",
56
+ description: "Your Jira Cloud host, e.g. yourorg.atlassian.net (no protocol, no trailing slash).",
57
+ placeholder: "yourorg.atlassian.net"
58
+ }),
59
+ projectKeys: z.array(z.string().min(1)).nonempty().optional().meta({
60
+ label: "Project keys (optional)",
61
+ description: "Restrict the sync to specific Jira project keys (e.g. ENG, OPS). Omit to sync every project the account can see."
62
+ }),
63
+ resources: z.array(z.enum(["projects", "users", "sprints", "issues", "issue_events"])).nonempty().optional().meta({
64
+ label: "Resources",
65
+ description: "Which Jira resources to sync. Omit to sync all of them. 'issue_events' shares the issues query \u2014 enabling it without 'issues' still fetches issues (with changelog) but skips writing issue entities."
66
+ }),
67
+ storyPointsField: z.string().min(1).optional().meta({
68
+ label: "Story points field ID",
69
+ description: "Custom field ID holding story points (varies per site). Defaults to customfield_10016.",
70
+ placeholder: "customfield_10016"
71
+ }),
72
+ sprintField: z.string().min(1).optional().meta({
73
+ label: "Sprint field ID",
74
+ description: "Custom field ID holding the sprint association on issues. Defaults to customfield_10020.",
75
+ placeholder: "customfield_10020"
76
+ })
77
+ })
78
+ );
79
+ var jiraCredentials = {
80
+ email: {
81
+ description: "Atlassian account email",
82
+ auth: "required"
83
+ },
84
+ apiToken: {
85
+ description: "Atlassian API token",
86
+ auth: "required"
87
+ }
88
+ };
89
+ var PHASE_ORDER = ["projects", "users", "sprints", "issues"];
90
+ var isJiraSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
91
+ var idString = z.string().min(1);
92
+ var nonNegInt = z.number().int().nonnegative();
93
+ var accountRefSchema = z.object({
94
+ accountId: idString,
95
+ displayName: z.string().nullable().optional()
96
+ });
97
+ var projectSchema = z.object({
98
+ id: idString,
99
+ key: z.string().min(1),
100
+ name: z.string(),
101
+ projectTypeKey: z.string().nullable().optional(),
102
+ lead: accountRefSchema.nullable().optional()
103
+ });
104
+ var projectsResponseSchema = z.object({
105
+ values: z.array(projectSchema),
106
+ isLast: z.boolean(),
107
+ startAt: nonNegInt,
108
+ maxResults: nonNegInt,
109
+ total: nonNegInt
110
+ });
111
+ var usersResponseSchema = z.array(
112
+ z.object({
113
+ accountId: idString,
114
+ displayName: z.string().nullable().optional(),
115
+ emailAddress: z.string().nullable().optional(),
116
+ accountType: z.string().nullable().optional(),
117
+ active: z.boolean().nullable().optional()
118
+ })
119
+ );
120
+ var sprintsResponseSchema = z.array(
121
+ z.object({
122
+ id: nonNegInt,
123
+ name: z.string(),
124
+ state: z.enum(["active", "closed", "future"]),
125
+ startDate: z.iso.datetime().nullable().optional(),
126
+ endDate: z.iso.datetime().nullable().optional(),
127
+ completeDate: z.iso.datetime().nullable().optional(),
128
+ originBoardId: nonNegInt.nullable().optional()
129
+ })
130
+ );
131
+ var changelogHistorySchema = z.object({
132
+ id: idString,
133
+ created: z.iso.datetime(),
134
+ author: accountRefSchema.nullable().optional(),
135
+ items: z.array(
136
+ z.object({
137
+ field: z.string(),
138
+ fromString: z.string().nullable().optional(),
139
+ toString: z.string().nullable().optional()
140
+ })
141
+ )
142
+ });
143
+ var issueSchema = z.object({
144
+ id: idString,
145
+ key: z.string().min(1),
146
+ fields: z.object({
147
+ summary: z.string().nullable().optional(),
148
+ status: z.object({
149
+ name: z.string(),
150
+ statusCategory: z.object({ key: z.string(), name: z.string().nullable().optional() }).nullable().optional()
151
+ }).nullable().optional(),
152
+ priority: z.object({ name: z.string() }).nullable().optional(),
153
+ issuetype: z.object({ name: z.string() }).nullable().optional(),
154
+ assignee: accountRefSchema.nullable().optional(),
155
+ reporter: accountRefSchema.nullable().optional(),
156
+ project: z.object({ id: idString, key: z.string().min(1) }).nullable(),
157
+ created: z.iso.datetime(),
158
+ updated: z.iso.datetime(),
159
+ resolutiondate: z.iso.datetime().nullable().optional(),
160
+ customfield_10016: z.number().nullable().optional(),
161
+ customfield_10020: z.array(z.object({ id: nonNegInt, name: z.string() })).nullable().optional()
162
+ }),
163
+ changelog: z.object({ histories: z.array(changelogHistorySchema) })
164
+ });
165
+ var issuesResponseSchema = z.object({
166
+ issues: z.array(issueSchema),
167
+ nextPageToken: z.string().nullable().optional(),
168
+ isLast: z.boolean().optional()
169
+ });
170
+ var PROJECTS_PAGE_SIZE = 50;
171
+ var USERS_PAGE_SIZE = 50;
172
+ var BOARDS_PAGE_SIZE = 50;
173
+ var SPRINTS_PAGE_SIZE = 50;
174
+ var ISSUES_PAGE_SIZE = 100;
175
+ var DEFAULT_STORY_POINTS_FIELD = "customfield_10016";
176
+ var DEFAULT_SPRINT_FIELD = "customfield_10020";
177
+ var ISSUE_FIELDS = [
178
+ "summary",
179
+ "status",
180
+ "priority",
181
+ "issuetype",
182
+ "assignee",
183
+ "reporter",
184
+ "project",
185
+ "created",
186
+ "updated",
187
+ "resolutiondate"
188
+ ];
189
+ function parseOffset(page) {
190
+ if (page === null) {
191
+ return 0;
192
+ }
193
+ const n = Number.parseInt(page, 10);
194
+ return Number.isFinite(n) && n >= 0 ? n : 0;
195
+ }
196
+ function extractSprintId(value) {
197
+ if (!Array.isArray(value) || value.length === 0) {
198
+ return null;
199
+ }
200
+ const last = value[value.length - 1];
201
+ if (last !== null && typeof last === "object" && "id" in last) {
202
+ const id = last.id;
203
+ return id === null || id === void 0 ? null : String(id);
204
+ }
205
+ if (typeof last === "number" || typeof last === "string") {
206
+ return String(last);
207
+ }
208
+ return null;
209
+ }
210
+ function formatJqlDate(iso) {
211
+ const ms = parseEpoch(iso, "iso");
212
+ if (ms === null) {
213
+ return null;
214
+ }
215
+ const d = new Date(ms);
216
+ const pad = (n) => String(n).padStart(2, "0");
217
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
218
+ }
219
+ var JiraConnector = class _JiraConnector extends BaseConnector {
220
+ static id = "jira";
221
+ static schemas = {
222
+ projects: projectsResponseSchema,
223
+ users: usersResponseSchema,
224
+ sprints: sprintsResponseSchema,
225
+ issues: issuesResponseSchema
226
+ };
227
+ static create(input, ctx) {
228
+ const parsed = configFields.parse(input);
229
+ return new _JiraConnector(
230
+ {
231
+ host: parsed.host,
232
+ projectKeys: parsed.projectKeys,
233
+ resources: parsed.resources,
234
+ storyPointsField: parsed.storyPointsField,
235
+ sprintField: parsed.sprintField
236
+ },
237
+ { email: parsed.email, apiToken: parsed.apiToken },
238
+ ctx
239
+ );
240
+ }
241
+ id = "jira";
242
+ credentials = jiraCredentials;
243
+ get baseUrl() {
244
+ const host = this.settings.host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
245
+ return `https://${host}`;
246
+ }
247
+ get storyPointsField() {
248
+ return this.settings.storyPointsField ?? DEFAULT_STORY_POINTS_FIELD;
249
+ }
250
+ get sprintField() {
251
+ return this.settings.sprintField ?? DEFAULT_SPRINT_FIELD;
252
+ }
253
+ buildHeaders() {
254
+ const basic = btoa(`${this.creds.email}:${this.creds.apiToken}`);
255
+ return {
256
+ Authorization: `Basic ${basic}`,
257
+ Accept: "application/json",
258
+ "User-Agent": connectorUserAgent("jira")
259
+ };
260
+ }
261
+ fetch(url, resource, signal) {
262
+ return this.get(url, {
263
+ resource,
264
+ headers: this.buildHeaders(),
265
+ signal
266
+ });
267
+ }
268
+ // -------------------------------------------------------------------------
269
+ // Resource enablement
270
+ // -------------------------------------------------------------------------
271
+ activePhases() {
272
+ return selectActivePhases(
273
+ (r) => {
274
+ switch (r) {
275
+ case "projects":
276
+ return "projects";
277
+ case "users":
278
+ return "users";
279
+ case "sprints":
280
+ return "sprints";
281
+ case "issues":
282
+ case "issue_events":
283
+ return "issues";
284
+ }
285
+ },
286
+ PHASE_ORDER,
287
+ this.settings.resources
288
+ );
289
+ }
290
+ // -------------------------------------------------------------------------
291
+ // JQL
292
+ // -------------------------------------------------------------------------
293
+ buildJql(options) {
294
+ const clauses = [];
295
+ const keys = this.settings.projectKeys;
296
+ if (keys && keys.length > 0) {
297
+ const quoted = keys.map(
298
+ (k) => `"${k.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
299
+ );
300
+ clauses.push(`project in (${quoted.join(",")})`);
301
+ }
302
+ if (options.mode === "latest" && options.since) {
303
+ const formatted = formatJqlDate(options.since);
304
+ if (formatted !== null) {
305
+ clauses.push(`updated >= "${formatted}"`);
306
+ }
307
+ }
308
+ const where = clauses.join(" AND ");
309
+ return where.length > 0 ? `${where} ORDER BY updated ASC` : "ORDER BY updated ASC";
310
+ }
311
+ // -------------------------------------------------------------------------
312
+ // Fetchers
313
+ // -------------------------------------------------------------------------
314
+ async fetchProjectsPage(page, signal) {
315
+ const startAt = parseOffset(page);
316
+ const u = new URL(`${this.baseUrl}/rest/api/3/project/search`);
317
+ u.searchParams.set("startAt", String(startAt));
318
+ u.searchParams.set("maxResults", String(PROJECTS_PAGE_SIZE));
319
+ u.searchParams.set("expand", "lead");
320
+ const res = await this.fetch(
321
+ u.toString(),
322
+ "projects",
323
+ signal
324
+ );
325
+ const values = res.body.values;
326
+ const next = res.body.isLast || values.length === 0 ? null : String(startAt + values.length);
327
+ return { items: values, next };
328
+ }
329
+ async fetchUsersPage(page, signal) {
330
+ const startAt = parseOffset(page);
331
+ const u = new URL(`${this.baseUrl}/rest/api/3/users/search`);
332
+ u.searchParams.set("startAt", String(startAt));
333
+ u.searchParams.set("maxResults", String(USERS_PAGE_SIZE));
334
+ const res = await this.fetch(u.toString(), "users", signal);
335
+ const users = res.body;
336
+ const next = users.length < USERS_PAGE_SIZE ? null : String(startAt + users.length);
337
+ return { items: users, next };
338
+ }
339
+ async fetchBoardsPage(startAt, signal) {
340
+ const u = new URL(`${this.baseUrl}/rest/agile/1.0/board`);
341
+ u.searchParams.set("startAt", String(startAt));
342
+ u.searchParams.set("maxResults", String(BOARDS_PAGE_SIZE));
343
+ const res = await this.fetch(
344
+ u.toString(),
345
+ "sprints",
346
+ signal
347
+ );
348
+ return res.body;
349
+ }
350
+ async fetchSprintsForBoard(boardId, signal) {
351
+ const out = [];
352
+ let startAt = 0;
353
+ while (true) {
354
+ signal?.throwIfAborted();
355
+ const u = new URL(
356
+ `${this.baseUrl}/rest/agile/1.0/board/${boardId}/sprint`
357
+ );
358
+ u.searchParams.set("startAt", String(startAt));
359
+ u.searchParams.set("maxResults", String(SPRINTS_PAGE_SIZE));
360
+ const res = await this.fetch(
361
+ u.toString(),
362
+ "sprints",
363
+ signal
364
+ );
365
+ const values = res.body.values;
366
+ out.push(...values);
367
+ const isLast = res.body.isLast ?? values.length < SPRINTS_PAGE_SIZE;
368
+ if (isLast || values.length === 0) {
369
+ break;
370
+ }
371
+ startAt += values.length;
372
+ }
373
+ return out;
374
+ }
375
+ async fetchSprintsPage(page, signal) {
376
+ const startAt = parseOffset(page);
377
+ const boardsPage = await this.fetchBoardsPage(startAt, signal);
378
+ const boards = boardsPage.values;
379
+ const sprints = [];
380
+ for (const board of boards) {
381
+ if (board.type !== "scrum") {
382
+ continue;
383
+ }
384
+ const boardSprints = await this.fetchSprintsForBoard(board.id, signal);
385
+ for (const s of boardSprints) {
386
+ sprints.push({ ...s, boardId: board.id });
387
+ }
388
+ }
389
+ const isLast = boardsPage.isLast ?? boards.length < BOARDS_PAGE_SIZE;
390
+ const next = isLast ? null : String(startAt + boards.length);
391
+ return { items: sprints, next };
392
+ }
393
+ async fetchIssuesPage(page, options, signal) {
394
+ const u = new URL(`${this.baseUrl}/rest/api/3/search/jql`);
395
+ u.searchParams.set("jql", this.buildJql(options));
396
+ u.searchParams.set("maxResults", String(ISSUES_PAGE_SIZE));
397
+ u.searchParams.set(
398
+ "fields",
399
+ [...ISSUE_FIELDS, this.storyPointsField, this.sprintField].join(",")
400
+ );
401
+ u.searchParams.set("expand", "changelog");
402
+ if (page !== null) {
403
+ u.searchParams.set("nextPageToken", page);
404
+ }
405
+ const res = await this.fetch(
406
+ u.toString(),
407
+ "issues",
408
+ signal
409
+ );
410
+ const token = res.body.nextPageToken ?? null;
411
+ const next = res.body.isLast === true || token === null ? null : token;
412
+ return { items: res.body.issues, next };
413
+ }
414
+ // -------------------------------------------------------------------------
415
+ // Writers
416
+ // -------------------------------------------------------------------------
417
+ async writeProjects(storage, projects) {
418
+ const now = Date.now();
419
+ for (const p of projects) {
420
+ await storage.entity({
421
+ type: "jira_project",
422
+ id: p.id,
423
+ attributes: {
424
+ key: p.key,
425
+ name: p.name,
426
+ projectTypeKey: p.projectTypeKey ?? null,
427
+ leadAccountId: p.lead?.accountId ?? null,
428
+ leadDisplayName: p.lead?.displayName ?? null
429
+ },
430
+ updated_at: now
431
+ });
432
+ }
433
+ }
434
+ async writeUsers(storage, users) {
435
+ const now = Date.now();
436
+ for (const u of users) {
437
+ if (!u.accountId) {
438
+ continue;
439
+ }
440
+ await storage.entity({
441
+ type: "jira_user",
442
+ id: u.accountId,
443
+ attributes: {
444
+ displayName: u.displayName ?? null,
445
+ emailAddress: u.emailAddress ?? null,
446
+ accountType: u.accountType ?? null,
447
+ active: u.active ?? null
448
+ },
449
+ updated_at: now
450
+ });
451
+ }
452
+ }
453
+ async writeSprints(storage, sprints) {
454
+ const now = Date.now();
455
+ for (const s of sprints) {
456
+ const startMs = parseEpoch(s.startDate ?? null, "iso");
457
+ const endMs = parseEpoch(s.endDate ?? null, "iso");
458
+ const completeMs = parseEpoch(s.completeDate ?? null, "iso");
459
+ await storage.entity({
460
+ type: "jira_sprint",
461
+ id: String(s.id),
462
+ attributes: {
463
+ name: s.name,
464
+ state: s.state,
465
+ boardId: s.boardId,
466
+ originBoardId: s.originBoardId ?? null,
467
+ startDate: startMs,
468
+ endDate: endMs,
469
+ completeDate: completeMs
470
+ },
471
+ updated_at: completeMs ?? endMs ?? startMs ?? now
472
+ });
473
+ }
474
+ }
475
+ async writeIssues(storage, issues, sinceMs) {
476
+ const writeEntities = this.isResourceEnabled("issues");
477
+ const writeEvents = this.isResourceEnabled("issue_events");
478
+ for (const issue of issues) {
479
+ const f = issue.fields;
480
+ const createdMs = parseEpoch(f.created, "iso");
481
+ const updatedMs = parseEpoch(f.updated, "iso");
482
+ if (createdMs === null || updatedMs === null) {
483
+ console.warn(
484
+ `[connector-jira] skipping issue ${issue.key} with unparseable created/updated`
485
+ );
486
+ continue;
487
+ }
488
+ const projectKey = f.project?.key ?? null;
489
+ if (writeEntities) {
490
+ const rawPoints = f[this.storyPointsField];
491
+ const storyPoints = typeof rawPoints === "number" && Number.isFinite(rawPoints) ? rawPoints : null;
492
+ await storage.entity({
493
+ type: "jira_issue",
494
+ id: issue.id,
495
+ attributes: {
496
+ key: issue.key,
497
+ summary: f.summary ?? null,
498
+ statusName: f.status?.name ?? null,
499
+ statusCategory: f.status?.statusCategory?.key ?? null,
500
+ priority: f.priority?.name ?? null,
501
+ issueType: f.issuetype?.name ?? null,
502
+ assigneeId: f.assignee?.accountId ?? null,
503
+ reporterId: f.reporter?.accountId ?? null,
504
+ projectKey,
505
+ sprintId: extractSprintId(f[this.sprintField]),
506
+ storyPoints,
507
+ createdAt: createdMs,
508
+ resolvedAt: parseEpoch(f.resolutiondate ?? null, "iso")
509
+ },
510
+ updated_at: updatedMs
511
+ });
512
+ }
513
+ if (writeEvents) {
514
+ const histories = issue.changelog?.histories ?? [];
515
+ for (const h of histories) {
516
+ const ts = parseEpoch(h.created, "iso");
517
+ if (ts === null) {
518
+ continue;
519
+ }
520
+ if (sinceMs !== null && ts <= sinceMs) {
521
+ continue;
522
+ }
523
+ for (const item of h.items) {
524
+ if (item.field !== "status") {
525
+ continue;
526
+ }
527
+ const attributes = {
528
+ historyId: h.id,
529
+ issueId: issue.id,
530
+ issueKey: issue.key,
531
+ projectKey,
532
+ authorId: h.author?.accountId ?? null,
533
+ fromStatus: item.fromString ?? null,
534
+ toStatus: item.toString ?? null
535
+ };
536
+ await storage.event({
537
+ name: "jira_issue_status_change",
538
+ start_ts: ts,
539
+ end_ts: null,
540
+ attributes
541
+ });
542
+ }
543
+ }
544
+ }
545
+ }
546
+ }
547
+ // -------------------------------------------------------------------------
548
+ // sync
549
+ // -------------------------------------------------------------------------
550
+ async sync(options, storage, signal) {
551
+ const cursor = isJiraSyncCursor(options.cursor) ? options.cursor : void 0;
552
+ const isFull = options.mode === "full";
553
+ const sinceMs = options.since ? parseEpoch(options.since, "iso") : null;
554
+ const phases = this.activePhases();
555
+ return paginateChunked({
556
+ phases,
557
+ cursor,
558
+ signal,
559
+ fetchPage: async (phase, page, sig) => {
560
+ switch (phase) {
561
+ case "projects":
562
+ return this.fetchProjectsPage(page, sig);
563
+ case "users":
564
+ return this.fetchUsersPage(page, sig);
565
+ case "sprints":
566
+ return this.fetchSprintsPage(page, sig);
567
+ case "issues":
568
+ return this.fetchIssuesPage(page, options, sig);
569
+ }
570
+ },
571
+ writeBatch: async (phase, items, page) => {
572
+ if (isFull && page === null) {
573
+ switch (phase) {
574
+ case "projects":
575
+ await storage.entities([], { types: ["jira_project"] });
576
+ break;
577
+ case "users":
578
+ await storage.entities([], { types: ["jira_user"] });
579
+ break;
580
+ case "sprints":
581
+ await storage.entities([], { types: ["jira_sprint"] });
582
+ break;
583
+ case "issues":
584
+ if (this.isResourceEnabled("issues")) {
585
+ await storage.entities([], { types: ["jira_issue"] });
586
+ }
587
+ if (this.isResourceEnabled("issue_events")) {
588
+ await storage.events([], {
589
+ names: ["jira_issue_status_change"]
590
+ });
591
+ }
592
+ break;
593
+ }
594
+ }
595
+ switch (phase) {
596
+ case "projects":
597
+ return this.writeProjects(storage, items);
598
+ case "users":
599
+ return this.writeUsers(storage, items);
600
+ case "sprints":
601
+ return this.writeSprints(storage, items);
602
+ case "issues":
603
+ return this.writeIssues(storage, items, sinceMs);
604
+ }
605
+ }
606
+ });
607
+ }
608
+ };
609
+
610
+ // src/index.ts
611
+ var index_default = JiraConnector;
612
+ export {
613
+ JiraConnector,
614
+ configFields,
615
+ index_default as default
616
+ };
617
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../src/jira.ts","../src/index.ts"],"sourcesContent":["import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","import {\n type HttpResponse,\n connectorUserAgent,\n parseEpoch,\n} from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ConnectorContext,\n type CredentialsSchema,\n type JSONValue,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n makeChunkedCursorGuard,\n paginateChunked,\n selectActivePhases,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\n// ---------------------------------------------------------------------------\n// configFields\n// ---------------------------------------------------------------------------\n\nexport const configFields = defineConfigFields(\n z.object({\n email: z.string().min(1).meta({\n label: 'Account email',\n description:\n 'Atlassian account email paired with the API token for Basic auth.',\n placeholder: 'you@yourorg.com',\n }),\n apiToken: z.object({ $secret: z.string() }).meta({\n label: 'API Token',\n description:\n 'Atlassian API token. Create one at id.atlassian.com → Security → API tokens.',\n placeholder: 'ATATT...',\n secret: true,\n }),\n host: z\n .string()\n .min(1)\n .regex(\n /^[^/\\s:?#]+$/,\n 'Use host only (no protocol, port, path, or query).',\n )\n .meta({\n label: 'Site host',\n description:\n 'Your Jira Cloud host, e.g. yourorg.atlassian.net (no protocol, no trailing slash).',\n placeholder: 'yourorg.atlassian.net',\n }),\n projectKeys: z.array(z.string().min(1)).nonempty().optional().meta({\n label: 'Project keys (optional)',\n description:\n 'Restrict the sync to specific Jira project keys (e.g. ENG, OPS). Omit to sync every project the account can see.',\n }),\n resources: z\n .array(z.enum(['projects', 'users', 'sprints', 'issues', 'issue_events']))\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n \"Which Jira resources to sync. Omit to sync all of them. 'issue_events' shares the issues query — enabling it without 'issues' still fetches issues (with changelog) but skips writing issue entities.\",\n }),\n storyPointsField: z.string().min(1).optional().meta({\n label: 'Story points field ID',\n description:\n 'Custom field ID holding story points (varies per site). Defaults to customfield_10016.',\n placeholder: 'customfield_10016',\n }),\n sprintField: z.string().min(1).optional().meta({\n label: 'Sprint field ID',\n description:\n 'Custom field ID holding the sprint association on issues. Defaults to customfield_10020.',\n placeholder: 'customfield_10020',\n }),\n }),\n);\n\nexport type JiraResource =\n | 'projects'\n | 'users'\n | 'sprints'\n | 'issues'\n | 'issue_events';\n\nexport interface JiraSettings {\n host: string;\n projectKeys?: readonly string[];\n resources?: readonly JiraResource[];\n storyPointsField?: string;\n sprintField?: string;\n}\n\nconst jiraCredentials = {\n email: {\n description: 'Atlassian account email',\n auth: 'required' as const,\n },\n apiToken: {\n description: 'Atlassian API token',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype JiraCredentials = typeof jiraCredentials;\n\n// ---------------------------------------------------------------------------\n// Sync phases + cursor\n// ---------------------------------------------------------------------------\n\nconst PHASE_ORDER = ['projects', 'users', 'sprints', 'issues'] as const;\n\ntype JiraPhase = (typeof PHASE_ORDER)[number];\n\nconst isJiraSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\n// ---------------------------------------------------------------------------\n// Jira API types\n// ---------------------------------------------------------------------------\n\ninterface JiraAccountRef {\n accountId: string;\n displayName?: string | null;\n}\n\ninterface JiraStatusCategory {\n key: string;\n name?: string | null;\n}\n\ninterface JiraStatus {\n name: string;\n statusCategory?: JiraStatusCategory | null;\n}\n\ninterface JiraNamed {\n name: string;\n}\n\ninterface JiraProjectRef {\n id: string;\n key: string;\n}\n\ninterface JiraIssueFields {\n summary?: string | null;\n status?: JiraStatus | null;\n priority?: JiraNamed | null;\n issuetype?: JiraNamed | null;\n assignee?: JiraAccountRef | null;\n reporter?: JiraAccountRef | null;\n project?: JiraProjectRef | null;\n created: string;\n updated: string;\n resolutiondate?: string | null;\n [key: string]: unknown;\n}\n\ninterface JiraChangelogItem {\n field: string;\n fromString?: string | null;\n toString?: string | null;\n}\n\ninterface JiraChangelogHistory {\n id: string;\n created: string;\n author?: JiraAccountRef | null;\n items: JiraChangelogItem[];\n}\n\ninterface JiraIssue {\n id: string;\n key: string;\n fields: JiraIssueFields;\n changelog?: { histories: JiraChangelogHistory[] } | null;\n}\n\ninterface JiraSearchResponse {\n issues: JiraIssue[];\n nextPageToken?: string | null;\n isLast?: boolean;\n}\n\ninterface JiraProject {\n id: string;\n key: string;\n name: string;\n projectTypeKey?: string | null;\n lead?: JiraAccountRef | null;\n}\n\ninterface JiraProjectPage {\n values: JiraProject[];\n isLast: boolean;\n startAt: number;\n maxResults: number;\n total: number;\n}\n\ninterface JiraUser {\n accountId: string;\n displayName?: string | null;\n emailAddress?: string | null;\n accountType?: string | null;\n active?: boolean | null;\n}\n\ninterface JiraSprint {\n id: number;\n name: string;\n state: 'active' | 'closed' | 'future';\n startDate?: string | null;\n endDate?: string | null;\n completeDate?: string | null;\n originBoardId?: number | null;\n}\n\ninterface JiraSprintWithBoard extends JiraSprint {\n boardId: number;\n}\n\ninterface JiraBoard {\n id: number;\n name: string;\n type: string;\n}\n\ninterface JiraAgilePage<T> {\n values: T[];\n isLast?: boolean;\n startAt: number;\n maxResults: number;\n}\n\n// ---------------------------------------------------------------------------\n// Schemas — describe the per-resource API response shape consumed by request()\n// ---------------------------------------------------------------------------\n\nconst idString = z.string().min(1);\nconst nonNegInt = z.number().int().nonnegative();\n\nconst accountRefSchema = z.object({\n accountId: idString,\n displayName: z.string().nullable().optional(),\n});\n\nconst projectSchema = z.object({\n id: idString,\n key: z.string().min(1),\n name: z.string(),\n projectTypeKey: z.string().nullable().optional(),\n lead: accountRefSchema.nullable().optional(),\n});\n\nconst projectsResponseSchema = z.object({\n values: z.array(projectSchema),\n isLast: z.boolean(),\n startAt: nonNegInt,\n maxResults: nonNegInt,\n total: nonNegInt,\n});\n\nconst usersResponseSchema = z.array(\n z.object({\n accountId: idString,\n displayName: z.string().nullable().optional(),\n emailAddress: z.string().nullable().optional(),\n accountType: z.string().nullable().optional(),\n active: z.boolean().nullable().optional(),\n }),\n);\n\nconst sprintsResponseSchema = z.array(\n z.object({\n id: nonNegInt,\n name: z.string(),\n state: z.enum(['active', 'closed', 'future']),\n startDate: z.iso.datetime().nullable().optional(),\n endDate: z.iso.datetime().nullable().optional(),\n completeDate: z.iso.datetime().nullable().optional(),\n originBoardId: nonNegInt.nullable().optional(),\n }),\n);\n\nconst changelogHistorySchema = z.object({\n id: idString,\n created: z.iso.datetime(),\n author: accountRefSchema.nullable().optional(),\n items: z.array(\n z.object({\n field: z.string(),\n fromString: z.string().nullable().optional(),\n toString: z.string().nullable().optional(),\n }),\n ),\n});\n\nconst issueSchema = z.object({\n id: idString,\n key: z.string().min(1),\n fields: z.object({\n summary: z.string().nullable().optional(),\n status: z\n .object({\n name: z.string(),\n statusCategory: z\n .object({ key: z.string(), name: z.string().nullable().optional() })\n .nullable()\n .optional(),\n })\n .nullable()\n .optional(),\n priority: z.object({ name: z.string() }).nullable().optional(),\n issuetype: z.object({ name: z.string() }).nullable().optional(),\n assignee: accountRefSchema.nullable().optional(),\n reporter: accountRefSchema.nullable().optional(),\n project: z.object({ id: idString, key: z.string().min(1) }).nullable(),\n created: z.iso.datetime(),\n updated: z.iso.datetime(),\n resolutiondate: z.iso.datetime().nullable().optional(),\n customfield_10016: z.number().nullable().optional(),\n customfield_10020: z\n .array(z.object({ id: nonNegInt, name: z.string() }))\n .nullable()\n .optional(),\n }),\n changelog: z.object({ histories: z.array(changelogHistorySchema) }),\n});\n\nconst issuesResponseSchema = z.object({\n issues: z.array(issueSchema),\n nextPageToken: z.string().nullable().optional(),\n isLast: z.boolean().optional(),\n});\n\n// ---------------------------------------------------------------------------\n// JiraConnector\n// ---------------------------------------------------------------------------\n\nconst PROJECTS_PAGE_SIZE = 50;\nconst USERS_PAGE_SIZE = 50;\nconst BOARDS_PAGE_SIZE = 50;\nconst SPRINTS_PAGE_SIZE = 50;\nconst ISSUES_PAGE_SIZE = 100;\nconst DEFAULT_STORY_POINTS_FIELD = 'customfield_10016';\nconst DEFAULT_SPRINT_FIELD = 'customfield_10020';\n\nconst ISSUE_FIELDS = [\n 'summary',\n 'status',\n 'priority',\n 'issuetype',\n 'assignee',\n 'reporter',\n 'project',\n 'created',\n 'updated',\n 'resolutiondate',\n] as const;\n\nfunction parseOffset(page: string | null): number {\n if (page === null) {\n return 0;\n }\n const n = Number.parseInt(page, 10);\n return Number.isFinite(n) && n >= 0 ? n : 0;\n}\n\nfunction extractSprintId(value: unknown): string | null {\n if (!Array.isArray(value) || value.length === 0) {\n return null;\n }\n const last = value[value.length - 1];\n if (last !== null && typeof last === 'object' && 'id' in last) {\n const id = (last as { id: unknown }).id;\n return id === null || id === undefined ? null : String(id);\n }\n if (typeof last === 'number' || typeof last === 'string') {\n return String(last);\n }\n return null;\n}\n\nfunction formatJqlDate(iso: string): string | null {\n const ms = parseEpoch(iso, 'iso');\n if (ms === null) {\n return null;\n }\n const d = new Date(ms);\n const pad = (n: number) => String(n).padStart(2, '0');\n return (\n `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ` +\n `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`\n );\n}\n\nexport class JiraConnector extends BaseConnector<\n JiraSettings,\n JiraCredentials\n> {\n static readonly id = 'jira';\n\n static readonly schemas = {\n projects: projectsResponseSchema,\n users: usersResponseSchema,\n sprints: sprintsResponseSchema,\n issues: issuesResponseSchema,\n } as const;\n\n static create(input: unknown, ctx?: ConnectorContext): JiraConnector {\n const parsed = configFields.parse(input);\n return new JiraConnector(\n {\n host: parsed.host,\n projectKeys: parsed.projectKeys,\n resources: parsed.resources,\n storyPointsField: parsed.storyPointsField,\n sprintField: parsed.sprintField,\n },\n { email: parsed.email, apiToken: parsed.apiToken },\n ctx,\n );\n }\n\n readonly id = 'jira';\n override readonly credentials = jiraCredentials;\n\n private get baseUrl(): string {\n const host = this.settings.host\n .replace(/^https?:\\/\\//, '')\n .replace(/\\/+$/, '');\n return `https://${host}`;\n }\n\n private get storyPointsField(): string {\n return this.settings.storyPointsField ?? DEFAULT_STORY_POINTS_FIELD;\n }\n\n private get sprintField(): string {\n return this.settings.sprintField ?? DEFAULT_SPRINT_FIELD;\n }\n\n private buildHeaders(): Record<string, string> {\n const basic = btoa(`${this.creds.email}:${this.creds.apiToken}`);\n return {\n Authorization: `Basic ${basic}`,\n Accept: 'application/json',\n 'User-Agent': connectorUserAgent('jira'),\n };\n }\n\n private fetch<T>(\n url: string,\n resource: string,\n signal?: AbortSignal,\n ): Promise<HttpResponse<T>> {\n return this.get<T>(url, {\n resource,\n headers: this.buildHeaders(),\n signal,\n });\n }\n\n // -------------------------------------------------------------------------\n // Resource enablement\n // -------------------------------------------------------------------------\n\n private activePhases(): JiraPhase[] {\n return selectActivePhases<JiraResource, JiraPhase>(\n (r) => {\n switch (r) {\n case 'projects':\n return 'projects';\n case 'users':\n return 'users';\n case 'sprints':\n return 'sprints';\n case 'issues':\n case 'issue_events':\n return 'issues';\n }\n },\n PHASE_ORDER,\n this.settings.resources,\n );\n }\n\n // -------------------------------------------------------------------------\n // JQL\n // -------------------------------------------------------------------------\n\n private buildJql(options: SyncOptions): string {\n const clauses: string[] = [];\n const keys = this.settings.projectKeys;\n if (keys && keys.length > 0) {\n const quoted = keys.map(\n (k) => `\"${k.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`,\n );\n clauses.push(`project in (${quoted.join(',')})`);\n }\n if (options.mode === 'latest' && options.since) {\n const formatted = formatJqlDate(options.since);\n if (formatted !== null) {\n clauses.push(`updated >= \"${formatted}\"`);\n }\n }\n const where = clauses.join(' AND ');\n return where.length > 0\n ? `${where} ORDER BY updated ASC`\n : 'ORDER BY updated ASC';\n }\n\n // -------------------------------------------------------------------------\n // Fetchers\n // -------------------------------------------------------------------------\n\n private async fetchProjectsPage(\n page: string | null,\n signal: AbortSignal | undefined,\n ): Promise<{ items: JiraProject[]; next: string | null }> {\n const startAt = parseOffset(page);\n const u = new URL(`${this.baseUrl}/rest/api/3/project/search`);\n u.searchParams.set('startAt', String(startAt));\n u.searchParams.set('maxResults', String(PROJECTS_PAGE_SIZE));\n u.searchParams.set('expand', 'lead');\n const res = await this.fetch<JiraProjectPage>(\n u.toString(),\n 'projects',\n signal,\n );\n const values = res.body.values;\n const next =\n res.body.isLast || values.length === 0\n ? null\n : String(startAt + values.length);\n return { items: values, next };\n }\n\n private async fetchUsersPage(\n page: string | null,\n signal: AbortSignal | undefined,\n ): Promise<{ items: JiraUser[]; next: string | null }> {\n const startAt = parseOffset(page);\n const u = new URL(`${this.baseUrl}/rest/api/3/users/search`);\n u.searchParams.set('startAt', String(startAt));\n u.searchParams.set('maxResults', String(USERS_PAGE_SIZE));\n const res = await this.fetch<JiraUser[]>(u.toString(), 'users', signal);\n const users = res.body;\n const next =\n users.length < USERS_PAGE_SIZE ? null : String(startAt + users.length);\n return { items: users, next };\n }\n\n private async fetchBoardsPage(\n startAt: number,\n signal: AbortSignal | undefined,\n ): Promise<JiraAgilePage<JiraBoard>> {\n const u = new URL(`${this.baseUrl}/rest/agile/1.0/board`);\n u.searchParams.set('startAt', String(startAt));\n u.searchParams.set('maxResults', String(BOARDS_PAGE_SIZE));\n const res = await this.fetch<JiraAgilePage<JiraBoard>>(\n u.toString(),\n 'sprints',\n signal,\n );\n return res.body;\n }\n\n private async fetchSprintsForBoard(\n boardId: number,\n signal: AbortSignal | undefined,\n ): Promise<JiraSprint[]> {\n const out: JiraSprint[] = [];\n let startAt = 0;\n while (true) {\n signal?.throwIfAborted();\n const u = new URL(\n `${this.baseUrl}/rest/agile/1.0/board/${boardId}/sprint`,\n );\n u.searchParams.set('startAt', String(startAt));\n u.searchParams.set('maxResults', String(SPRINTS_PAGE_SIZE));\n const res = await this.fetch<JiraAgilePage<JiraSprint>>(\n u.toString(),\n 'sprints',\n signal,\n );\n const values = res.body.values;\n out.push(...values);\n const isLast = res.body.isLast ?? values.length < SPRINTS_PAGE_SIZE;\n if (isLast || values.length === 0) {\n break;\n }\n startAt += values.length;\n }\n return out;\n }\n\n private async fetchSprintsPage(\n page: string | null,\n signal: AbortSignal | undefined,\n ): Promise<{ items: JiraSprintWithBoard[]; next: string | null }> {\n const startAt = parseOffset(page);\n const boardsPage = await this.fetchBoardsPage(startAt, signal);\n const boards = boardsPage.values;\n const sprints: JiraSprintWithBoard[] = [];\n for (const board of boards) {\n if (board.type !== 'scrum') {\n continue;\n }\n const boardSprints = await this.fetchSprintsForBoard(board.id, signal);\n for (const s of boardSprints) {\n sprints.push({ ...s, boardId: board.id });\n }\n }\n const isLast = boardsPage.isLast ?? boards.length < BOARDS_PAGE_SIZE;\n const next = isLast ? null : String(startAt + boards.length);\n return { items: sprints, next };\n }\n\n private async fetchIssuesPage(\n page: string | null,\n options: SyncOptions,\n signal: AbortSignal | undefined,\n ): Promise<{ items: JiraIssue[]; next: string | null }> {\n const u = new URL(`${this.baseUrl}/rest/api/3/search/jql`);\n u.searchParams.set('jql', this.buildJql(options));\n u.searchParams.set('maxResults', String(ISSUES_PAGE_SIZE));\n u.searchParams.set(\n 'fields',\n [...ISSUE_FIELDS, this.storyPointsField, this.sprintField].join(','),\n );\n u.searchParams.set('expand', 'changelog');\n if (page !== null) {\n u.searchParams.set('nextPageToken', page);\n }\n const res = await this.fetch<JiraSearchResponse>(\n u.toString(),\n 'issues',\n signal,\n );\n const token = res.body.nextPageToken ?? null;\n const next = res.body.isLast === true || token === null ? null : token;\n return { items: res.body.issues, next };\n }\n\n // -------------------------------------------------------------------------\n // Writers\n // -------------------------------------------------------------------------\n\n private async writeProjects(\n storage: StorageHandle,\n projects: JiraProject[],\n ): Promise<void> {\n const now = Date.now();\n for (const p of projects) {\n await storage.entity({\n type: 'jira_project',\n id: p.id,\n attributes: {\n key: p.key,\n name: p.name,\n projectTypeKey: p.projectTypeKey ?? null,\n leadAccountId: p.lead?.accountId ?? null,\n leadDisplayName: p.lead?.displayName ?? null,\n },\n updated_at: now,\n });\n }\n }\n\n private async writeUsers(\n storage: StorageHandle,\n users: JiraUser[],\n ): Promise<void> {\n const now = Date.now();\n for (const u of users) {\n if (!u.accountId) {\n continue;\n }\n await storage.entity({\n type: 'jira_user',\n id: u.accountId,\n attributes: {\n displayName: u.displayName ?? null,\n emailAddress: u.emailAddress ?? null,\n accountType: u.accountType ?? null,\n active: u.active ?? null,\n },\n updated_at: now,\n });\n }\n }\n\n private async writeSprints(\n storage: StorageHandle,\n sprints: JiraSprintWithBoard[],\n ): Promise<void> {\n const now = Date.now();\n for (const s of sprints) {\n const startMs = parseEpoch(s.startDate ?? null, 'iso');\n const endMs = parseEpoch(s.endDate ?? null, 'iso');\n const completeMs = parseEpoch(s.completeDate ?? null, 'iso');\n await storage.entity({\n type: 'jira_sprint',\n id: String(s.id),\n attributes: {\n name: s.name,\n state: s.state,\n boardId: s.boardId,\n originBoardId: s.originBoardId ?? null,\n startDate: startMs,\n endDate: endMs,\n completeDate: completeMs,\n },\n updated_at: completeMs ?? endMs ?? startMs ?? now,\n });\n }\n }\n\n private async writeIssues(\n storage: StorageHandle,\n issues: JiraIssue[],\n sinceMs: number | null,\n ): Promise<void> {\n const writeEntities = this.isResourceEnabled('issues');\n const writeEvents = this.isResourceEnabled('issue_events');\n\n for (const issue of issues) {\n const f = issue.fields;\n const createdMs = parseEpoch(f.created, 'iso');\n const updatedMs = parseEpoch(f.updated, 'iso');\n if (createdMs === null || updatedMs === null) {\n console.warn(\n `[connector-jira] skipping issue ${issue.key} with unparseable created/updated`,\n );\n continue;\n }\n const projectKey = f.project?.key ?? null;\n\n if (writeEntities) {\n const rawPoints = f[this.storyPointsField];\n const storyPoints =\n typeof rawPoints === 'number' && Number.isFinite(rawPoints)\n ? rawPoints\n : null;\n await storage.entity({\n type: 'jira_issue',\n id: issue.id,\n attributes: {\n key: issue.key,\n summary: f.summary ?? null,\n statusName: f.status?.name ?? null,\n statusCategory: f.status?.statusCategory?.key ?? null,\n priority: f.priority?.name ?? null,\n issueType: f.issuetype?.name ?? null,\n assigneeId: f.assignee?.accountId ?? null,\n reporterId: f.reporter?.accountId ?? null,\n projectKey,\n sprintId: extractSprintId(f[this.sprintField]),\n storyPoints,\n createdAt: createdMs,\n resolvedAt: parseEpoch(f.resolutiondate ?? null, 'iso'),\n },\n updated_at: updatedMs,\n });\n }\n\n if (writeEvents) {\n const histories = issue.changelog?.histories ?? [];\n for (const h of histories) {\n const ts = parseEpoch(h.created, 'iso');\n if (ts === null) {\n continue;\n }\n if (sinceMs !== null && ts <= sinceMs) {\n continue;\n }\n for (const item of h.items) {\n if (item.field !== 'status') {\n continue;\n }\n const attributes: Record<string, JSONValue> = {\n historyId: h.id,\n issueId: issue.id,\n issueKey: issue.key,\n projectKey,\n authorId: h.author?.accountId ?? null,\n fromStatus: item.fromString ?? null,\n toStatus: item.toString ?? null,\n };\n await storage.event({\n name: 'jira_issue_status_change',\n start_ts: ts,\n end_ts: null,\n attributes,\n });\n }\n }\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // sync\n // -------------------------------------------------------------------------\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const cursor = isJiraSyncCursor(options.cursor)\n ? options.cursor\n : undefined;\n const isFull = options.mode === 'full';\n const sinceMs = options.since ? parseEpoch(options.since, 'iso') : null;\n const phases = this.activePhases();\n\n return paginateChunked<JiraPhase, string>({\n phases,\n cursor,\n signal,\n fetchPage: async (phase, page, sig) => {\n switch (phase) {\n case 'projects':\n return this.fetchProjectsPage(page, sig);\n case 'users':\n return this.fetchUsersPage(page, sig);\n case 'sprints':\n return this.fetchSprintsPage(page, sig);\n case 'issues':\n return this.fetchIssuesPage(page, options, sig);\n }\n },\n writeBatch: async (phase, items, page) => {\n if (isFull && page === null) {\n switch (phase) {\n case 'projects':\n await storage.entities([], { types: ['jira_project'] });\n break;\n case 'users':\n await storage.entities([], { types: ['jira_user'] });\n break;\n case 'sprints':\n await storage.entities([], { types: ['jira_sprint'] });\n break;\n case 'issues':\n if (this.isResourceEnabled('issues')) {\n await storage.entities([], { types: ['jira_issue'] });\n }\n if (this.isResourceEnabled('issue_events')) {\n await storage.events([], {\n names: ['jira_issue_status_change'],\n });\n }\n break;\n }\n }\n switch (phase) {\n case 'projects':\n return this.writeProjects(storage, items as JiraProject[]);\n case 'users':\n return this.writeUsers(storage, items as JiraUser[]);\n case 'sprints':\n return this.writeSprints(storage, items as JiraSprintWithBoard[]);\n case 'issues':\n return this.writeIssues(storage, items as JiraIssue[], sinceMs);\n }\n },\n });\n }\n}\n","import { JiraConnector } from './jira';\n\nexport { configFields, JiraConnector } from './jira';\nexport type { JiraSettings, JiraResource } from './jira';\nexport default JiraConnector;\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@rawdash/connector-jira",
3
+ "version": "0.0.1",
4
+ "description": "Rawdash connector for Jira Cloud — issues, status-change events, sprints, projects, and users",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/rawdash/rawdash.git",
10
+ "directory": "packages/connectors/jira"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "exports": {
18
+ ".": {
19
+ "@rawdash/source": "./src/index.ts",
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "typecheck": "tsc --noEmit",
27
+ "lint": "eslint src",
28
+ "test": "vitest run"
29
+ },
30
+ "dependencies": {
31
+ "@rawdash/core": "workspace:*",
32
+ "zod": "^4.4.3"
33
+ },
34
+ "devDependencies": {
35
+ "@rawdash/connector-shared": "workspace:*",
36
+ "@rawdash/connector-test-utils": "workspace:*",
37
+ "fast-check": "^4.8.0",
38
+ "tsup": "^8.0.0",
39
+ "typescript": "^5.7.2",
40
+ "vitest": "^4.1.4"
41
+ }
42
+ }