@knpkv/jira-api-client 0.3.1 → 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/CHANGELOG.md +28 -0
- package/README.md +88 -65
- package/dist/JiraApiClient.d.ts +20 -86
- package/dist/JiraApiClient.d.ts.map +1 -1
- package/dist/JiraApiClient.js +26 -76
- package/dist/JiraApiClient.js.map +1 -1
- package/dist/generated/JiraApi.d.ts +103971 -0
- package/dist/generated/JiraApi.d.ts.map +1 -0
- package/dist/generated/JiraApi.js +7608 -0
- package/dist/generated/JiraApi.js.map +1 -0
- package/dist/index.d.ts +3 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -11
- package/dist/index.js.map +1 -1
- package/package.json +13 -16
- package/scripts/regenerate.ts +209 -202
- package/scripts/tsconfig.json +13 -0
- package/src/JiraApiClient.ts +74 -105
- package/src/generated/JiraApi.ts +19596 -0
- package/src/index.ts +3 -14
- package/test/JiraApiClient.test.ts +155 -137
- package/.specs/VERSION_V3 +0 -1
- package/.specs/jira-v3-1001.0.0-SNAPSHOT-05f20e18d9bde3de68f911b4915970482f536d4e.json +0 -56275
- package/.specs/jira-v3-1001.0.0-SNAPSHOT-d7cd03e966bb408ad32ffe870114289c528f56fd.json +0 -56129
- package/.specs/jira-v3-1001.0.0-SNAPSHOT-ea9913a8c4412d2bfb1911480b023a04544e19d8.json +0 -55618
- package/dist/OpenApiFetchClient.d.ts +0 -55
- package/dist/OpenApiFetchClient.d.ts.map +0 -1
- package/dist/OpenApiFetchClient.js +0 -63
- package/dist/OpenApiFetchClient.js.map +0 -1
- package/dist/generated/v3/index.d.ts +0 -7
- package/dist/generated/v3/index.d.ts.map +0 -1
- package/dist/generated/v3/index.js +0 -2
- package/dist/generated/v3/index.js.map +0 -1
- package/dist/generated/v3/schema.d.ts +0 -64454
- package/src/OpenApiFetchClient.ts +0 -92
- package/src/generated/v3/index.ts +0 -6
- package/src/generated/v3/schema.d.ts +0 -64454
- package/tsconfig.tsbuildinfo +0 -1
package/src/index.ts
CHANGED
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* **Mental model**
|
|
5
|
-
*
|
|
6
|
-
* - **Namespaced version**: `V3` re-exports the generated OpenAPI types (paths, components, operations).
|
|
7
|
-
* - **Config + client split**: {@link JiraApiConfig} is provided separately from the
|
|
8
|
-
* generated client to support auth-polymorphic usage.
|
|
9
|
-
* - **openapi-fetch wrapper**: {@link OpenApiFetchClient} provides Effect-based `execute` method
|
|
10
|
-
* around raw openapi-fetch clients.
|
|
2
|
+
* Schema-validated Effect client for Jira Cloud REST API v3.
|
|
11
3
|
*
|
|
12
4
|
* @packageDocumentation
|
|
13
5
|
*/
|
|
14
6
|
|
|
15
|
-
export { JiraApiClient, type JiraApiClientShape, layer } from "./JiraApiClient.js"
|
|
7
|
+
export { JiraApiClient, type JiraApiClientShape, layer, make, type UploadAttachmentInput } from "./JiraApiClient.js"
|
|
16
8
|
|
|
17
9
|
export { JiraApiConfig, type JiraApiConfigShape } from "./JiraApiConfig.js"
|
|
18
10
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
// Re-export generated V3 types
|
|
22
|
-
export type * as V3 from "./generated/v3/index.js"
|
|
11
|
+
export * as JiraApi from "./generated/JiraApi.js"
|
|
@@ -1,152 +1,170 @@
|
|
|
1
|
-
import { describe, expect, it
|
|
1
|
+
import { describe, expect, it } from "@effect/vitest"
|
|
2
2
|
import * as Effect from "effect/Effect"
|
|
3
3
|
import * as Layer from "effect/Layer"
|
|
4
|
-
import * as Predicate from "effect/Predicate"
|
|
5
4
|
import * as Redacted from "effect/Redacted"
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return new Response(JSON.stringify(response.body), {
|
|
34
|
-
status: response.status,
|
|
35
|
-
headers: { "content-type": "application/json" }
|
|
36
|
-
})
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
return Effect.ensuring(
|
|
40
|
-
fn(capturedRequests),
|
|
41
|
-
Effect.sync(() => {
|
|
42
|
-
vi.stubGlobal("fetch", originalFetch)
|
|
43
|
-
})
|
|
5
|
+
import * as HttpClient from "effect/unstable/http/HttpClient"
|
|
6
|
+
import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
|
|
7
|
+
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
|
|
8
|
+
import { JiraApiClient, JiraApiConfig, type JiraApiConfigShape } from "../src/index.js"
|
|
9
|
+
|
|
10
|
+
const clientLayer = (
|
|
11
|
+
config: JiraApiConfigShape,
|
|
12
|
+
response: { readonly status: number; readonly body?: unknown },
|
|
13
|
+
requests: Array<HttpClientRequest.HttpClientRequest>
|
|
14
|
+
) =>
|
|
15
|
+
JiraApiClient.layer.pipe(
|
|
16
|
+
Layer.provide(Layer.succeed(JiraApiConfig, config)),
|
|
17
|
+
Layer.provide(Layer.succeed(
|
|
18
|
+
HttpClient.HttpClient,
|
|
19
|
+
HttpClient.make((request) =>
|
|
20
|
+
Effect.sync(() => {
|
|
21
|
+
requests.push(request)
|
|
22
|
+
return HttpClientResponse.fromWeb(
|
|
23
|
+
request,
|
|
24
|
+
new Response(response.body === undefined ? null : JSON.stringify(response.body), {
|
|
25
|
+
status: response.status,
|
|
26
|
+
headers: { "content-type": "application/json" }
|
|
27
|
+
})
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
))
|
|
44
32
|
)
|
|
33
|
+
|
|
34
|
+
const basicConfig: JiraApiConfigShape = {
|
|
35
|
+
baseUrl: "https://jira.test",
|
|
36
|
+
auth: {
|
|
37
|
+
type: "basic",
|
|
38
|
+
email: "user@example.com",
|
|
39
|
+
apiToken: Redacted.make("test-token")
|
|
40
|
+
}
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
describe("JiraApiClient", () => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
const client = yield* JiraApiClient.pipe(
|
|
65
|
-
Effect.provide(JiraApiClient.layer),
|
|
66
|
-
Effect.provide(configLayer)
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
const issue = yield* toEffect(client.v3.client.GET("/rest/api/3/issue/{issueIdOrKey}", {
|
|
70
|
-
params: { path: { issueIdOrKey: "PROJ-1" } }
|
|
71
|
-
}))
|
|
72
|
-
|
|
73
|
-
expect(issue.id).toBe("10001")
|
|
74
|
-
expect(issue.key).toBe("PROJ-1")
|
|
75
|
-
expect(capturedRequests).toHaveLength(1)
|
|
76
|
-
|
|
77
|
-
const request = capturedRequests[0]!
|
|
78
|
-
expect(request.url).toContain("test.atlassian.net")
|
|
79
|
-
expect(request.url).toContain("/rest/api/3/issue/PROJ-1")
|
|
80
|
-
const authHeader = (request.init.headers as Record<string, string>)["Authorization"] ??
|
|
81
|
-
(request.init.headers as Record<string, string>)["authorization"]
|
|
82
|
-
expect(authHeader).toMatch(/^Basic /)
|
|
83
|
-
})
|
|
84
|
-
))
|
|
85
|
-
|
|
86
|
-
// Verifies OAuth2 applies Bearer token and routes through Atlassian cloud proxy
|
|
87
|
-
it.effect("creates client with OAuth2 auth and applies Bearer token", () =>
|
|
88
|
-
withMockFetch(
|
|
89
|
-
[{ status: 200, body: { id: "10001", key: "PROJ-1", fields: { summary: "Test" } } }],
|
|
90
|
-
(capturedRequests) =>
|
|
91
|
-
Effect.gen(function*() {
|
|
92
|
-
const configLayer = Layer.succeed(JiraApiConfig, {
|
|
93
|
-
baseUrl: "https://test.atlassian.net",
|
|
94
|
-
auth: {
|
|
95
|
-
type: "oauth2",
|
|
96
|
-
accessToken: Redacted.make("oauth-token"),
|
|
97
|
-
cloudId: "cloud-123"
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
const client = yield* JiraApiClient.pipe(
|
|
102
|
-
Effect.provide(JiraApiClient.layer),
|
|
103
|
-
Effect.provide(configLayer)
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
const issue = yield* toEffect(client.v3.client.GET("/rest/api/3/issue/{issueIdOrKey}", {
|
|
107
|
-
params: { path: { issueIdOrKey: "PROJ-1" } }
|
|
108
|
-
}))
|
|
109
|
-
|
|
110
|
-
expect(issue.id).toBe("10001")
|
|
111
|
-
expect(capturedRequests).toHaveLength(1)
|
|
44
|
+
it.effect("applies basic auth and decodes dynamic issue fields", () => {
|
|
45
|
+
const requests: Array<HttpClientRequest.HttpClientRequest> = []
|
|
46
|
+
return Effect.gen(function*() {
|
|
47
|
+
const client = yield* JiraApiClient
|
|
48
|
+
const issue = yield* client.getIssue("PROJ-1", undefined)
|
|
49
|
+
expect(issue.key).toBe("PROJ-1")
|
|
50
|
+
expect(issue.fields?.customfield_10001).toBe("dynamic")
|
|
51
|
+
expect(requests[0]?.url).toBe("https://jira.test/rest/api/3/issue/PROJ-1")
|
|
52
|
+
expect(requests[0]?.headers.authorization).toMatch(/^Basic /)
|
|
53
|
+
}).pipe(Effect.provide(clientLayer(basicConfig, {
|
|
54
|
+
status: 200,
|
|
55
|
+
body: { id: "10001", key: "PROJ-1", fields: { customfield_10001: "dynamic" } }
|
|
56
|
+
}, requests)))
|
|
57
|
+
})
|
|
112
58
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
))
|
|
59
|
+
it.effect("routes OAuth2 through the Atlassian cloud gateway", () => {
|
|
60
|
+
const requests: Array<HttpClientRequest.HttpClientRequest> = []
|
|
61
|
+
return Effect.gen(function*() {
|
|
62
|
+
const client = yield* JiraApiClient
|
|
63
|
+
yield* client.getIssue("PROJ-1", undefined)
|
|
64
|
+
expect(requests[0]?.url).toBe("https://api.atlassian.com/ex/jira/cloud-123/rest/api/3/issue/PROJ-1")
|
|
65
|
+
expect(requests[0]?.headers.authorization).toBe("Bearer oauth-token")
|
|
66
|
+
}).pipe(Effect.provide(clientLayer(
|
|
67
|
+
{
|
|
68
|
+
baseUrl: "",
|
|
69
|
+
auth: { type: "oauth2", accessToken: Redacted.make("oauth-token"), cloudId: "cloud-123" }
|
|
70
|
+
},
|
|
71
|
+
{ status: 200, body: { id: "10001", key: "PROJ-1", fields: {} } },
|
|
72
|
+
requests
|
|
73
|
+
)))
|
|
120
74
|
})
|
|
121
75
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
76
|
+
it.effect("keeps bodyless-spec 404 responses in the typed error channel", () =>
|
|
77
|
+
Effect.gen(function*() {
|
|
78
|
+
const client = yield* JiraApiClient
|
|
79
|
+
const result = yield* Effect.result(client.getIssue("NOPE-999", undefined))
|
|
80
|
+
expect(result._tag).toBe("Failure")
|
|
81
|
+
if (result._tag === "Failure") expect(result.failure._tag).toBe("GetIssue404")
|
|
82
|
+
}).pipe(Effect.provide(clientLayer(basicConfig, {
|
|
83
|
+
status: 404,
|
|
84
|
+
body: { errorMessages: ["Issue does not exist"] }
|
|
85
|
+
}, []))))
|
|
86
|
+
|
|
87
|
+
it.effect("never treats a genuinely empty 404 as success", () =>
|
|
88
|
+
Effect.gen(function*() {
|
|
89
|
+
const client = yield* JiraApiClient
|
|
90
|
+
const result = yield* Effect.result(client.getIssue("NOPE-EMPTY", undefined))
|
|
91
|
+
expect(result._tag).toBe("Failure")
|
|
92
|
+
}).pipe(Effect.provide(clientLayer(basicConfig, { status: 404 }, []))))
|
|
93
|
+
|
|
94
|
+
it.effect("decodes a terminal JQL page whose nextPageToken is null", () =>
|
|
95
|
+
Effect.gen(function*() {
|
|
96
|
+
const client = yield* JiraApiClient
|
|
97
|
+
const result = yield* client.searchIssuesUsingJql({ params: { jql: "project = PROJ" } })
|
|
98
|
+
expect(result.nextPageToken).toBeNull()
|
|
99
|
+
expect(result.isLast).toBe(true)
|
|
100
|
+
}).pipe(Effect.provide(clientLayer(basicConfig, {
|
|
101
|
+
status: 200,
|
|
102
|
+
body: { issues: [], isLast: true, nextPageToken: null }
|
|
103
|
+
}, []))))
|
|
104
|
+
|
|
105
|
+
it.effect("preserves Jira Premium contributors on decoded versions", () =>
|
|
106
|
+
Effect.gen(function*() {
|
|
107
|
+
const client = yield* JiraApiClient
|
|
108
|
+
const version = yield* client.getVersion("10000", undefined)
|
|
109
|
+
expect(version.contributors).toEqual([
|
|
110
|
+
"account-1",
|
|
111
|
+
{ accountId: "account-2", displayName: "Ada" }
|
|
112
|
+
])
|
|
113
|
+
}).pipe(Effect.provide(clientLayer(basicConfig, {
|
|
114
|
+
status: 200,
|
|
115
|
+
body: {
|
|
116
|
+
id: "10000",
|
|
117
|
+
name: "1.0.0",
|
|
118
|
+
contributors: ["account-1", { accountId: "account-2", displayName: "Ada" }]
|
|
119
|
+
}
|
|
120
|
+
}, []))))
|
|
121
|
+
|
|
122
|
+
it.effect("rejects malformed successful responses", () =>
|
|
123
|
+
Effect.gen(function*() {
|
|
124
|
+
const client = yield* JiraApiClient
|
|
125
|
+
const result = yield* Effect.result(client.getFields(undefined))
|
|
126
|
+
expect(result._tag).toBe("Failure")
|
|
127
|
+
}).pipe(Effect.provide(clientLayer(basicConfig, { status: 200, body: {} }, []))))
|
|
128
|
+
|
|
129
|
+
it.effect("returns void for Jira 204 responses that advertise an empty JSON schema", () => {
|
|
130
|
+
const requests: Array<HttpClientRequest.HttpClientRequest> = []
|
|
131
|
+
return Effect.gen(function*() {
|
|
132
|
+
const client = yield* JiraApiClient
|
|
133
|
+
const result = yield* client.setBanner({
|
|
134
|
+
payload: { isEnabled: true, message: "Maintenance" }
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
expect(result).toBeUndefined()
|
|
138
|
+
expect(requests).toHaveLength(1)
|
|
139
|
+
}).pipe(Effect.provide(clientLayer(basicConfig, { status: 204 }, requests)))
|
|
140
|
+
})
|
|
133
141
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
it.effect("keeps naturally bodyless delete operations void", () => {
|
|
143
|
+
const requests: Array<HttpClientRequest.HttpClientRequest> = []
|
|
144
|
+
return Effect.gen(function*() {
|
|
145
|
+
const client = yield* JiraApiClient
|
|
146
|
+
const result = yield* client.deleteIssue("PROJ-1", undefined)
|
|
138
147
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
expect(result).toBeUndefined()
|
|
149
|
+
expect(requests).toHaveLength(1)
|
|
150
|
+
}).pipe(Effect.provide(clientLayer(basicConfig, { status: 204 }, requests)))
|
|
151
|
+
})
|
|
143
152
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
it.effect("uploads multipart data with the Atlassian CSRF bypass header", () => {
|
|
154
|
+
const requests: Array<HttpClientRequest.HttpClientRequest> = []
|
|
155
|
+
return Effect.gen(function*() {
|
|
156
|
+
const client = yield* JiraApiClient
|
|
157
|
+
const attachments = yield* client.uploadAttachment("PROJ-1", {
|
|
158
|
+
bytes: new Uint8Array([1, 2, 3]),
|
|
159
|
+
filename: "example.bin",
|
|
160
|
+
mediaType: "application/octet-stream"
|
|
161
|
+
})
|
|
162
|
+
expect(attachments[0]?.filename).toBe("example.bin")
|
|
163
|
+
expect(requests[0]?.headers["x-atlassian-token"]).toBe("no-check")
|
|
164
|
+
expect(requests[0]?.body._tag).toBe("FormData")
|
|
165
|
+
}).pipe(Effect.provide(clientLayer(basicConfig, {
|
|
166
|
+
status: 200,
|
|
167
|
+
body: [{ id: "1", filename: "example.bin", size: 3 }]
|
|
168
|
+
}, requests)))
|
|
151
169
|
})
|
|
152
170
|
})
|
package/.specs/VERSION_V3
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1001.0.0-SNAPSHOT-05f20e18d9bde3de68f911b4915970482f536d4e
|