@felixgeelhaar/jira-sdk 0.2.0 → 1.0.0
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 +214 -374
- package/dist/auth/index.cjs +400 -0
- package/dist/auth/index.cjs.map +1 -0
- package/dist/auth/index.d.cts +192 -0
- package/dist/auth/index.d.ts +192 -0
- package/dist/auth/index.js +386 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/errors/index.cjs +272 -0
- package/dist/errors/index.cjs.map +1 -0
- package/dist/errors/index.d.cts +203 -0
- package/dist/errors/index.d.ts +203 -0
- package/dist/errors/index.js +254 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/http-client-BSzRYQZa.d.cts +317 -0
- package/dist/http-client-erRvYNs-.d.ts +317 -0
- package/dist/index.cjs +9582 -13466
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1242 -76
- package/dist/index.d.ts +1242 -76
- package/dist/index.js +9166 -13318
- package/dist/index.js.map +1 -1
- package/dist/schemas/index.cjs +2481 -13092
- package/dist/schemas/index.cjs.map +1 -1
- package/dist/schemas/index.d.cts +335 -207
- package/dist/schemas/index.d.ts +335 -207
- package/dist/schemas/index.js +2138 -13047
- package/dist/schemas/index.js.map +1 -1
- package/dist/services/index.cjs +6375 -13323
- package/dist/services/index.cjs.map +1 -1
- package/dist/services/index.d.cts +3852 -299
- package/dist/services/index.d.ts +3852 -299
- package/dist/services/index.js +6350 -13321
- package/dist/services/index.js.map +1 -1
- package/dist/transport/index.cjs +1016 -0
- package/dist/transport/index.cjs.map +1 -0
- package/dist/transport/index.d.cts +372 -0
- package/dist/transport/index.d.ts +372 -0
- package/dist/transport/index.js +997 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/types-E6djPHpW.d.cts +95 -0
- package/dist/types-E6djPHpW.d.ts +95 -0
- package/dist/webhook-Bn8gme6Y.d.cts +6959 -0
- package/dist/webhook-Bn8gme6Y.d.ts +6959 -0
- package/package.json +73 -27
- package/dist/project-BtUx-eSv.d.cts +0 -1480
- package/dist/project-BtUx-eSv.d.ts +0 -1480
package/README.md
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Jira SDK TypeScript
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/felixgeelhaar/jirasdk-ts)
|
|
4
|
+
|
|
5
|
+
A type-safe, production-ready SDK for the Jira REST API with built-in resilience patterns.
|
|
4
6
|
|
|
5
7
|
## Features
|
|
6
8
|
|
|
7
|
-
- **Type-Safe**: Full TypeScript support with strict mode
|
|
9
|
+
- **Type-Safe**: Full TypeScript support with strict mode enabled
|
|
8
10
|
- **Runtime Validation**: Zod schemas for request/response validation
|
|
9
|
-
- **Resilience Built-In**: Circuit breaker, retry, rate limiting
|
|
10
|
-
- **Multiple Auth Methods**: API Token,
|
|
11
|
-
- **Middleware Architecture**: Extensible request/response pipeline
|
|
11
|
+
- **Resilience Built-In**: Circuit breaker, retry with exponential backoff, rate limiting
|
|
12
|
+
- **Multiple Auth Methods**: API Token, Personal Access Token, Basic Auth, OAuth 2.0
|
|
13
|
+
- **Middleware Architecture**: Extensible request/response processing pipeline
|
|
12
14
|
- **Tree-Shakeable**: ESM and CJS builds with proper exports
|
|
13
15
|
|
|
14
|
-
##
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### Installation
|
|
15
19
|
|
|
16
20
|
```bash
|
|
17
21
|
npm install @felixgeelhaar/jira-sdk
|
|
@@ -21,7 +25,7 @@ pnpm add @felixgeelhaar/jira-sdk
|
|
|
21
25
|
yarn add @felixgeelhaar/jira-sdk
|
|
22
26
|
```
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
### Basic Usage
|
|
25
29
|
|
|
26
30
|
```typescript
|
|
27
31
|
import { createJiraClient, createApiTokenAuth } from '@felixgeelhaar/jira-sdk';
|
|
@@ -29,7 +33,7 @@ import { createJiraClient, createApiTokenAuth } from '@felixgeelhaar/jira-sdk';
|
|
|
29
33
|
// Create auth provider
|
|
30
34
|
const auth = createApiTokenAuth({
|
|
31
35
|
email: 'your-email@example.com',
|
|
32
|
-
apiToken: 'your-api-token',
|
|
36
|
+
apiToken: 'your-api-token',
|
|
33
37
|
});
|
|
34
38
|
|
|
35
39
|
// Create client
|
|
@@ -47,262 +51,235 @@ const results = await client.search.jql('project = PROJ AND status = Open');
|
|
|
47
51
|
for (const issue of results.issues) {
|
|
48
52
|
console.log(issue.key, issue.fields.summary);
|
|
49
53
|
}
|
|
50
|
-
```
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
// Create an issue
|
|
56
|
+
const newIssue = await client.issues.create({
|
|
57
|
+
fields: {
|
|
58
|
+
project: { key: 'PROJ' },
|
|
59
|
+
summary: 'New issue from SDK',
|
|
60
|
+
issuetype: { name: 'Task' },
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
```
|
|
53
64
|
|
|
54
|
-
###
|
|
65
|
+
### From environment variables
|
|
55
66
|
|
|
56
|
-
|
|
67
|
+
The SDK reads the same variables as the Go SDK, so both can share a deployment's
|
|
68
|
+
configuration:
|
|
57
69
|
|
|
58
70
|
```typescript
|
|
59
|
-
import {
|
|
71
|
+
import { createJiraClientFromEnv } from '@felixgeelhaar/jira-sdk';
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
// JIRA_BASE_URL=https://your-domain.atlassian.net
|
|
74
|
+
// JIRA_EMAIL=you@example.com
|
|
75
|
+
// JIRA_API_TOKEN=...
|
|
76
|
+
const client = createJiraClientFromEnv();
|
|
65
77
|
```
|
|
66
78
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
79
|
+
Credentials resolve in this order: `JIRA_EMAIL` + `JIRA_API_TOKEN`, then
|
|
80
|
+
`JIRA_PAT`, then `JIRA_USERNAME` + `JIRA_PASSWORD`, then `JIRA_OAUTH_CLIENT_ID` +
|
|
81
|
+
`JIRA_OAUTH_CLIENT_SECRET`. Also read: `JIRA_TIMEOUT` (seconds), `JIRA_MAX_RETRIES`,
|
|
82
|
+
`JIRA_USER_AGENT`.
|
|
70
83
|
|
|
71
|
-
|
|
84
|
+
### With Resilience
|
|
72
85
|
|
|
73
86
|
```typescript
|
|
74
|
-
import {
|
|
87
|
+
import {
|
|
88
|
+
createJiraClient,
|
|
89
|
+
createApiTokenAuth,
|
|
90
|
+
createResilienceMiddleware,
|
|
91
|
+
} from '@felixgeelhaar/jira-sdk';
|
|
75
92
|
|
|
76
|
-
const auth =
|
|
77
|
-
|
|
93
|
+
const auth = createApiTokenAuth({
|
|
94
|
+
email: 'your-email@example.com',
|
|
95
|
+
apiToken: 'your-api-token',
|
|
78
96
|
});
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### Basic Auth
|
|
82
97
|
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
// Create resilience middleware with circuit breaker, retry, and rate limiting
|
|
99
|
+
const { middleware, circuitBreaker } = createResilienceMiddleware({
|
|
100
|
+
retry: {
|
|
101
|
+
maxRetries: 3,
|
|
102
|
+
initialDelayMs: 1000,
|
|
103
|
+
maxDelayMs: 30000,
|
|
104
|
+
},
|
|
105
|
+
rateLimit: {
|
|
106
|
+
maxRequests: 90,
|
|
107
|
+
windowMs: 60000,
|
|
108
|
+
},
|
|
109
|
+
circuitBreaker: {
|
|
110
|
+
failureThreshold: 5,
|
|
111
|
+
resetTimeoutMs: 30000,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
85
114
|
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
115
|
+
const client = createJiraClient({
|
|
116
|
+
host: 'https://your-domain.atlassian.net',
|
|
117
|
+
auth,
|
|
118
|
+
middleware: [middleware],
|
|
89
119
|
});
|
|
90
|
-
```
|
|
91
120
|
|
|
92
|
-
|
|
121
|
+
// Monitor circuit breaker state
|
|
122
|
+
console.log(circuitBreaker?.getStats());
|
|
123
|
+
```
|
|
93
124
|
|
|
94
|
-
|
|
125
|
+
Or apply the whole stack as a client option, which also exposes the breaker on
|
|
126
|
+
the client:
|
|
95
127
|
|
|
96
128
|
```typescript
|
|
97
|
-
import {
|
|
129
|
+
import { createJiraClient, withResilience } from '@felixgeelhaar/jira-sdk';
|
|
98
130
|
|
|
99
|
-
const
|
|
100
|
-
clientId: 'your-client-id',
|
|
101
|
-
clientSecret: 'your-client-secret',
|
|
102
|
-
accessToken: 'current-access-token',
|
|
103
|
-
refreshToken: 'refresh-token',
|
|
104
|
-
expiresAt: Date.now() + 3600000,
|
|
105
|
-
});
|
|
131
|
+
const client = createJiraClient({ host, auth }, withResilience());
|
|
106
132
|
|
|
107
|
-
|
|
108
|
-
auth.onTokenRefresh = async (tokens) => {
|
|
109
|
-
await saveTokensToDatabase(tokens);
|
|
110
|
-
};
|
|
133
|
+
console.log(client.circuitBreaker?.getStats());
|
|
111
134
|
```
|
|
112
135
|
|
|
113
|
-
|
|
136
|
+
`withResilience()` disables the client's built-in retry, since the resilience
|
|
137
|
+
stack supplies its own — otherwise the two would compound into
|
|
138
|
+
`maxRetries * maxRetries` attempts.
|
|
114
139
|
|
|
115
|
-
|
|
140
|
+
## Building Content and Queries
|
|
141
|
+
|
|
142
|
+
### Rich text (ADF)
|
|
143
|
+
|
|
144
|
+
Jira represents descriptions and comment bodies as Atlassian Document Format,
|
|
145
|
+
not plain strings:
|
|
116
146
|
|
|
117
147
|
```typescript
|
|
118
|
-
|
|
119
|
-
const issue = await client.issues.get('PROJ-123');
|
|
120
|
-
const issue = await client.issues.get('PROJ-123', {
|
|
121
|
-
fields: ['summary', 'status', 'assignee'],
|
|
122
|
-
expand: ['changelog', 'renderedFields'],
|
|
123
|
-
});
|
|
148
|
+
import { AdfBuilder } from '@felixgeelhaar/jira-sdk';
|
|
124
149
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
description: 'Detailed description here',
|
|
131
|
-
issuetype: { name: 'Task' },
|
|
132
|
-
priority: { name: 'High' },
|
|
133
|
-
assignee: { accountId: 'user-account-id' },
|
|
134
|
-
},
|
|
135
|
-
});
|
|
150
|
+
const description = new AdfBuilder()
|
|
151
|
+
.addHeading('Steps to reproduce', 2)
|
|
152
|
+
.addOrderedList(['Open the app', 'Click Save', 'Observe the error'])
|
|
153
|
+
.addCodeBlock('TypeError: undefined is not a function', 'text')
|
|
154
|
+
.toDocument();
|
|
136
155
|
|
|
137
|
-
|
|
138
|
-
await client.issues.update('PROJ-123', {
|
|
156
|
+
await client.issues.create({
|
|
139
157
|
fields: {
|
|
140
|
-
|
|
141
|
-
|
|
158
|
+
project: { key: 'PROJ' },
|
|
159
|
+
summary: 'Crash on save',
|
|
160
|
+
issuetype: { name: 'Bug' },
|
|
161
|
+
description,
|
|
142
162
|
},
|
|
143
163
|
});
|
|
144
|
-
|
|
145
|
-
// Delete an issue
|
|
146
|
-
await client.issues.delete('PROJ-123');
|
|
147
|
-
|
|
148
|
-
// Transition an issue
|
|
149
|
-
await client.issues.transition('PROJ-123', {
|
|
150
|
-
transition: { id: '31' }, // Transition ID
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// Get available transitions
|
|
154
|
-
const transitions = await client.issues.getTransitions('PROJ-123');
|
|
155
164
|
```
|
|
156
165
|
|
|
157
|
-
###
|
|
166
|
+
### JQL
|
|
167
|
+
|
|
168
|
+
The builder quotes and escapes every value, so user input cannot break out of a
|
|
169
|
+
string literal and inject JQL:
|
|
158
170
|
|
|
159
171
|
```typescript
|
|
160
|
-
|
|
161
|
-
const comments = await client.issues.getComments('PROJ-123');
|
|
172
|
+
import { JqlQueryBuilder } from '@felixgeelhaar/jira-sdk';
|
|
162
173
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
const jql = new JqlQueryBuilder()
|
|
175
|
+
.project('PROJ')
|
|
176
|
+
.status('In Progress')
|
|
177
|
+
.assignee(untrustedUserInput) // safely escaped
|
|
178
|
+
.orderBy('created', 'DESC')
|
|
179
|
+
.build();
|
|
167
180
|
|
|
168
|
-
|
|
169
|
-
await client.issues.updateComment('PROJ-123', 'comment-id', {
|
|
170
|
-
body: 'Updated comment text',
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// Delete a comment
|
|
174
|
-
await client.issues.deleteComment('PROJ-123', 'comment-id');
|
|
181
|
+
const results = await client.search.jql(jql);
|
|
175
182
|
```
|
|
176
183
|
|
|
177
|
-
###
|
|
184
|
+
### Custom fields
|
|
178
185
|
|
|
179
186
|
```typescript
|
|
180
|
-
|
|
181
|
-
const results = await client.search.jql('project = PROJ');
|
|
182
|
-
|
|
183
|
-
// Advanced search with options
|
|
184
|
-
const results = await client.search.jql(
|
|
185
|
-
'project = PROJ AND status = "In Progress" ORDER BY created DESC',
|
|
186
|
-
{
|
|
187
|
-
startAt: 0,
|
|
188
|
-
maxResults: 50,
|
|
189
|
-
fields: ['summary', 'status', 'assignee'],
|
|
190
|
-
expand: ['changelog'],
|
|
191
|
-
}
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
// Iterate through all results
|
|
195
|
-
for (const issue of results.issues) {
|
|
196
|
-
console.log(`${issue.key}: ${issue.fields.summary}`);
|
|
197
|
-
}
|
|
187
|
+
import { CustomFields } from '@felixgeelhaar/jira-sdk';
|
|
198
188
|
|
|
199
|
-
|
|
200
|
-
|
|
189
|
+
const fields = new CustomFields()
|
|
190
|
+
.setString('customfield_10001', 'Team Alpha')
|
|
191
|
+
.setNumber('customfield_10002', 42)
|
|
192
|
+
.setLabels('customfield_10003', ['backend', 'urgent']);
|
|
201
193
|
```
|
|
202
194
|
|
|
203
|
-
|
|
195
|
+
## Authentication Methods
|
|
196
|
+
|
|
197
|
+
### API Token (Atlassian Cloud)
|
|
204
198
|
|
|
205
199
|
```typescript
|
|
206
|
-
|
|
207
|
-
const projects = await client.projects.list();
|
|
200
|
+
import { createApiTokenAuth } from '@felixgeelhaar/jira-sdk';
|
|
208
201
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
expand: ['description', 'lead', 'issueTypes'],
|
|
202
|
+
const auth = createApiTokenAuth({
|
|
203
|
+
email: 'your-email@example.com',
|
|
204
|
+
apiToken: 'your-api-token', // From https://id.atlassian.com/manage-profile/security/api-tokens
|
|
213
205
|
});
|
|
214
206
|
```
|
|
215
207
|
|
|
216
|
-
###
|
|
208
|
+
### Personal Access Token (Server/Data Center)
|
|
217
209
|
|
|
218
210
|
```typescript
|
|
219
|
-
|
|
220
|
-
const myself = await client.users.getCurrentUser();
|
|
221
|
-
|
|
222
|
-
// Get user by account ID
|
|
223
|
-
const user = await client.users.get('account-id');
|
|
224
|
-
|
|
225
|
-
// Search users
|
|
226
|
-
const users = await client.users.search({
|
|
227
|
-
query: 'john',
|
|
228
|
-
maxResults: 10,
|
|
229
|
-
});
|
|
211
|
+
import { createPatAuth } from '@felixgeelhaar/jira-sdk';
|
|
230
212
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
project: 'PROJ',
|
|
234
|
-
query: 'jane',
|
|
213
|
+
const auth = createPatAuth({
|
|
214
|
+
token: 'your-personal-access-token',
|
|
235
215
|
});
|
|
236
216
|
```
|
|
237
217
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
### Built-in Resilience Middleware
|
|
241
|
-
|
|
242
|
-
Add retry, rate limiting, and circuit breaker with one call:
|
|
218
|
+
### OAuth 2.0
|
|
243
219
|
|
|
244
220
|
```typescript
|
|
245
|
-
import {
|
|
246
|
-
|
|
247
|
-
const { middleware, circuitBreaker } = createResilienceMiddleware({
|
|
248
|
-
retry: {
|
|
249
|
-
maxRetries: 3,
|
|
250
|
-
initialDelayMs: 1000,
|
|
251
|
-
maxDelayMs: 30000,
|
|
252
|
-
multiplier: 2,
|
|
253
|
-
jitter: true,
|
|
254
|
-
},
|
|
255
|
-
rateLimit: {
|
|
256
|
-
maxRequests: 90, // Jira Cloud limit is ~100/min
|
|
257
|
-
windowMs: 60000,
|
|
258
|
-
waitForSlot: true,
|
|
259
|
-
},
|
|
260
|
-
circuitBreaker: {
|
|
261
|
-
failureThreshold: 5,
|
|
262
|
-
resetTimeoutMs: 30000,
|
|
263
|
-
failureWindowMs: 60000,
|
|
264
|
-
},
|
|
265
|
-
});
|
|
221
|
+
import { createOAuth2Auth } from '@felixgeelhaar/jira-sdk';
|
|
266
222
|
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
223
|
+
const auth = createOAuth2Auth({
|
|
224
|
+
clientId: 'your-client-id',
|
|
225
|
+
clientSecret: 'your-client-secret',
|
|
226
|
+
accessToken: 'current-access-token',
|
|
227
|
+
refreshToken: 'refresh-token',
|
|
228
|
+
expiresAt: Date.now() + 3600000, // 1 hour
|
|
271
229
|
});
|
|
272
230
|
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}, 10000);
|
|
231
|
+
// Automatic token refresh with callback
|
|
232
|
+
auth.onTokenRefresh = async (tokens) => {
|
|
233
|
+
await saveTokensToDatabase(tokens);
|
|
234
|
+
};
|
|
278
235
|
```
|
|
279
236
|
|
|
280
|
-
|
|
237
|
+
## Available Services
|
|
238
|
+
|
|
239
|
+
All 27 services are reachable from the client and constructed lazily on first
|
|
240
|
+
access, so an application that only touches issues never pays for the rest.
|
|
241
|
+
|
|
242
|
+
| Area | Accessor | Covers |
|
|
243
|
+
| ------------- | ------------------------------------------ | -------------------------------------------------------------------------- |
|
|
244
|
+
| Work items | `client.issues` | CRUD, comments, transitions, worklogs, attachments, links, votes, watchers |
|
|
245
|
+
| | `client.projects` | CRUD, components, versions, archive/restore |
|
|
246
|
+
| | `client.search` | JQL search, plus the enhanced token-paginated API |
|
|
247
|
+
| | `client.users` | Lookup, search, groups, user properties, default columns |
|
|
248
|
+
| Agile | `client.agile` | Boards, sprints, epics, backlog (`/rest/agile/1.0`) |
|
|
249
|
+
| Configuration | `client.fields` | Custom fields, contexts, options, project association |
|
|
250
|
+
| | `client.issueTypes` | Issue types and issue type schemes |
|
|
251
|
+
| | `client.issueLinkTypes` | Issue link types |
|
|
252
|
+
| | `client.priorities` / `client.resolutions` | Priorities and resolutions |
|
|
253
|
+
| | `client.screens` | Screens, tabs, screen fields |
|
|
254
|
+
| | `client.workflows` | Workflows, statuses, status categories, schemes |
|
|
255
|
+
| Views | `client.dashboards` | Dashboards and gadgets |
|
|
256
|
+
| | `client.filters` | Saved filters, favourites, share permissions |
|
|
257
|
+
| Access | `client.groups` | Groups and membership |
|
|
258
|
+
| | `client.myself` | Current user and preferences |
|
|
259
|
+
| | `client.permissions` | Permissions, schemes, project roles |
|
|
260
|
+
| | `client.securityLevels` | Issue security levels and schemes |
|
|
261
|
+
| Operations | `client.appProperties` | Application properties, advanced settings |
|
|
262
|
+
| | `client.audit` | Audit records |
|
|
263
|
+
| | `client.bulk` | Bulk create/delete with task progress polling |
|
|
264
|
+
| | `client.expressions` | Jira expression evaluation and analysis |
|
|
265
|
+
| | `client.labels` | Labels and suggestions |
|
|
266
|
+
| | `client.notifications` | Notification schemes, issue notifications |
|
|
267
|
+
| | `client.serverInfo` | Server info and instance configuration |
|
|
268
|
+
| | `client.timeTracking` | Providers, configuration, worklogs |
|
|
269
|
+
| | `client.webhooks` | Registration, refresh, delivery failures |
|
|
270
|
+
|
|
271
|
+
### Pagination
|
|
272
|
+
|
|
273
|
+
Every paginated endpoint exposes an async iterator plus an `all()` collector:
|
|
281
274
|
|
|
282
275
|
```typescript
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const client = createJiraClient({
|
|
289
|
-
host: 'https://your-domain.atlassian.net',
|
|
290
|
-
auth,
|
|
291
|
-
...options,
|
|
292
|
-
});
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
### Disable Specific Features
|
|
276
|
+
// Stream, one page fetched at a time
|
|
277
|
+
for await (const issue of client.search.iterate('project = PROJ')) {
|
|
278
|
+
console.log(issue.key);
|
|
279
|
+
}
|
|
296
280
|
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
retry: {
|
|
300
|
-
maxRetries: 5,
|
|
301
|
-
initialDelayMs: 500,
|
|
302
|
-
},
|
|
303
|
-
rateLimit: false, // Disable rate limiting
|
|
304
|
-
circuitBreaker: false, // Disable circuit breaker
|
|
305
|
-
});
|
|
281
|
+
// Or collect everything
|
|
282
|
+
const boards = await client.agile.allBoards();
|
|
306
283
|
```
|
|
307
284
|
|
|
308
285
|
## Error Handling
|
|
@@ -311,14 +288,9 @@ const { middleware } = createResilienceMiddleware({
|
|
|
311
288
|
import {
|
|
312
289
|
ApiError,
|
|
313
290
|
UnauthorizedError,
|
|
314
|
-
ForbiddenError,
|
|
315
291
|
NotFoundError,
|
|
316
292
|
RateLimitError,
|
|
317
|
-
ServerError,
|
|
318
293
|
NetworkError,
|
|
319
|
-
TimeoutError,
|
|
320
|
-
ValidationError,
|
|
321
|
-
CircuitBreakerOpenError,
|
|
322
294
|
} from '@felixgeelhaar/jira-sdk';
|
|
323
295
|
|
|
324
296
|
try {
|
|
@@ -327,23 +299,11 @@ try {
|
|
|
327
299
|
if (error instanceof NotFoundError) {
|
|
328
300
|
console.log('Issue not found');
|
|
329
301
|
} else if (error instanceof UnauthorizedError) {
|
|
330
|
-
console.log('Invalid credentials
|
|
331
|
-
} else if (error instanceof ForbiddenError) {
|
|
332
|
-
console.log('You do not have permission to access this issue');
|
|
302
|
+
console.log('Invalid credentials');
|
|
333
303
|
} else if (error instanceof RateLimitError) {
|
|
334
304
|
console.log(`Rate limited. Retry after ${error.retryAfter}ms`);
|
|
335
|
-
} else if (error instanceof ServerError) {
|
|
336
|
-
console.log('Jira server error - try again later');
|
|
337
305
|
} else if (error instanceof NetworkError) {
|
|
338
306
|
console.log('Network error:', error.message);
|
|
339
|
-
} else if (error instanceof TimeoutError) {
|
|
340
|
-
console.log('Request timed out');
|
|
341
|
-
} else if (error instanceof CircuitBreakerOpenError) {
|
|
342
|
-
console.log('Circuit breaker open - Jira may be unavailable');
|
|
343
|
-
} else if (error instanceof ValidationError) {
|
|
344
|
-
console.log('Invalid data:', error.errors);
|
|
345
|
-
} else if (error instanceof ApiError) {
|
|
346
|
-
console.log(`API error ${error.statusCode}:`, error.responseBody);
|
|
347
307
|
}
|
|
348
308
|
}
|
|
349
309
|
```
|
|
@@ -352,183 +312,63 @@ try {
|
|
|
352
312
|
|
|
353
313
|
```typescript
|
|
354
314
|
const client = createJiraClient({
|
|
355
|
-
// Required
|
|
356
315
|
host: 'https://your-domain.atlassian.net',
|
|
357
316
|
auth,
|
|
358
|
-
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
middleware: [], // Custom middleware
|
|
363
|
-
allowInsecureHttp: false, // Allow HTTP (not recommended)
|
|
317
|
+
apiVersion: '3', // Jira API version (default: '3')
|
|
318
|
+
timeout: 30000, // Request timeout in ms (default: 30000)
|
|
319
|
+
middleware: [], // Custom middleware
|
|
320
|
+
allowInsecureHttp: false, // Allow HTTP (not recommended)
|
|
364
321
|
});
|
|
365
322
|
```
|
|
366
323
|
|
|
367
|
-
##
|
|
324
|
+
## Development
|
|
368
325
|
|
|
369
|
-
|
|
370
|
-
const controller = new AbortController();
|
|
326
|
+
### Prerequisites
|
|
371
327
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
signal: controller.signal,
|
|
375
|
-
});
|
|
328
|
+
- Node.js >= 20.0.0
|
|
329
|
+
- pnpm >= 9.0.0
|
|
376
330
|
|
|
377
|
-
|
|
378
|
-
setTimeout(() => controller.abort(), 5000);
|
|
331
|
+
### Setup
|
|
379
332
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (error instanceof AbortError) {
|
|
384
|
-
console.log('Request was cancelled');
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
## Custom Middleware
|
|
390
|
-
|
|
391
|
-
```typescript
|
|
392
|
-
import type { Middleware } from '@felixgeelhaar/jira-sdk';
|
|
393
|
-
|
|
394
|
-
// Timing middleware
|
|
395
|
-
const timingMiddleware: Middleware = async (context, next) => {
|
|
396
|
-
const start = Date.now();
|
|
397
|
-
const response = await next(context);
|
|
398
|
-
console.log(`${context.request.method} ${context.request.url} - ${Date.now() - start}ms`);
|
|
399
|
-
return response;
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
// Add custom headers
|
|
403
|
-
const headerMiddleware: Middleware = async (context, next) => {
|
|
404
|
-
context.request.headers = {
|
|
405
|
-
...context.request.headers,
|
|
406
|
-
'X-Custom-Header': 'value',
|
|
407
|
-
};
|
|
408
|
-
return next(context);
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
const client = createJiraClient({
|
|
412
|
-
host: 'https://your-domain.atlassian.net',
|
|
413
|
-
auth,
|
|
414
|
-
middleware: [timingMiddleware, headerMiddleware],
|
|
415
|
-
});
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
## Logging
|
|
419
|
-
|
|
420
|
-
```typescript
|
|
421
|
-
import { createJiraClient, createLoggingMiddleware, ConsoleLogger } from '@felixgeelhaar/jira-sdk';
|
|
422
|
-
|
|
423
|
-
const logger = new ConsoleLogger('debug');
|
|
424
|
-
|
|
425
|
-
const client = createJiraClient({
|
|
426
|
-
host: 'https://your-domain.atlassian.net',
|
|
427
|
-
auth,
|
|
428
|
-
middleware: [createLoggingMiddleware(logger)],
|
|
429
|
-
});
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
## Subpath Exports
|
|
433
|
-
|
|
434
|
-
Import specific modules for better tree-shaking:
|
|
435
|
-
|
|
436
|
-
```typescript
|
|
437
|
-
// Schemas only
|
|
438
|
-
import { IssueSchema, ProjectSchema } from '@felixgeelhaar/jira-sdk/schemas';
|
|
439
|
-
|
|
440
|
-
// Services only
|
|
441
|
-
import { IssueService, SearchService } from '@felixgeelhaar/jira-sdk/services';
|
|
442
|
-
```
|
|
333
|
+
```bash
|
|
334
|
+
# Install dependencies
|
|
335
|
+
pnpm install
|
|
443
336
|
|
|
444
|
-
|
|
337
|
+
# Build
|
|
338
|
+
pnpm build
|
|
445
339
|
|
|
446
|
-
|
|
340
|
+
# Run tests
|
|
341
|
+
pnpm test
|
|
447
342
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
Issue,
|
|
451
|
-
Project,
|
|
452
|
-
User,
|
|
453
|
-
SearchResults,
|
|
454
|
-
JiraClientConfig,
|
|
455
|
-
ResilienceConfig,
|
|
456
|
-
} from '@felixgeelhaar/jira-sdk';
|
|
457
|
-
|
|
458
|
-
// Type-safe issue fields
|
|
459
|
-
const issue: Issue = await client.issues.get('PROJ-123');
|
|
460
|
-
console.log(issue.fields.summary); // Type-safe access
|
|
343
|
+
# Type check
|
|
344
|
+
pnpm typecheck
|
|
461
345
|
```
|
|
462
346
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
### Bulk Operations
|
|
347
|
+
### Project Structure
|
|
466
348
|
|
|
467
|
-
```typescript
|
|
468
|
-
// Create multiple issues
|
|
469
|
-
const issues = await Promise.all([
|
|
470
|
-
client.issues.create({
|
|
471
|
-
fields: {
|
|
472
|
-
project: { key: 'PROJ' },
|
|
473
|
-
summary: 'Task 1',
|
|
474
|
-
issuetype: { name: 'Task' },
|
|
475
|
-
},
|
|
476
|
-
}),
|
|
477
|
-
client.issues.create({
|
|
478
|
-
fields: {
|
|
479
|
-
project: { key: 'PROJ' },
|
|
480
|
-
summary: 'Task 2',
|
|
481
|
-
issuetype: { name: 'Task' },
|
|
482
|
-
},
|
|
483
|
-
}),
|
|
484
|
-
]);
|
|
485
349
|
```
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
startAt += results.issues.length;
|
|
502
|
-
if (startAt >= results.total) break;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Usage
|
|
507
|
-
for await (const issue of getAllIssues(client, 'project = PROJ')) {
|
|
508
|
-
console.log(issue.key);
|
|
509
|
-
}
|
|
350
|
+
jirasdk-ts/
|
|
351
|
+
├── src/
|
|
352
|
+
│ ├── adf/ # Atlassian Document Format builder
|
|
353
|
+
│ ├── auth/ # Authentication providers
|
|
354
|
+
│ ├── client/ # Client, options, env configuration
|
|
355
|
+
│ ├── custom-fields/ # Typed custom-field accessors
|
|
356
|
+
│ ├── errors/ # Error hierarchy
|
|
357
|
+
│ ├── jql/ # JQL query builder and escaping
|
|
358
|
+
│ ├── logging/ # Logging abstraction
|
|
359
|
+
│ ├── pagination/ # Shared pagination primitives
|
|
360
|
+
│ ├── schemas/ # Zod schemas, one directory per domain
|
|
361
|
+
│ ├── services/ # 27 domain services
|
|
362
|
+
│ ├── transport/ # HTTP client, middleware, circuit breaker
|
|
363
|
+
│ └── utils/ # Utility functions
|
|
364
|
+
└── package.json
|
|
510
365
|
```
|
|
511
366
|
|
|
512
|
-
|
|
367
|
+
## Security
|
|
513
368
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const app = express();
|
|
519
|
-
|
|
520
|
-
app.post('/webhook/jira', express.json(), (req, res) => {
|
|
521
|
-
const result = IssueSchema.safeParse(req.body.issue);
|
|
522
|
-
|
|
523
|
-
if (result.success) {
|
|
524
|
-
console.log('Issue updated:', result.data.key);
|
|
525
|
-
res.sendStatus(200);
|
|
526
|
-
} else {
|
|
527
|
-
console.error('Invalid webhook payload:', result.error);
|
|
528
|
-
res.sendStatus(400);
|
|
529
|
-
}
|
|
530
|
-
});
|
|
531
|
-
```
|
|
369
|
+
- **HTTPS Required**: HTTP URLs throw in production (configurable)
|
|
370
|
+
- **Credential Safety**: Sensitive headers automatically redacted in logs
|
|
371
|
+
- **Token Refresh**: OAuth2 uses mutex to prevent concurrent refresh races
|
|
532
372
|
|
|
533
373
|
## License
|
|
534
374
|
|