@knpkv/jira-api-client 0.4.0 → 1.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/CHANGELOG.md +22 -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 -75
- package/dist/JiraApiClient.js.map +1 -1
- package/dist/generated/JiraApi.d.ts +103977 -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 -201
- package/scripts/tsconfig.json +13 -0
- package/src/JiraApiClient.ts +74 -104
- package/src/generated/JiraApi.ts +19596 -0
- package/src/index.ts +3 -14
- package/test/JiraApiClient.test.ts +154 -153
- 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 -68
- 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 -102
- 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,169 +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
|
-
|
|
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
|
+
))
|
|
32
|
+
)
|
|
13
33
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return Object.fromEntries(headers.entries())
|
|
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")
|
|
21
40
|
}
|
|
22
|
-
if (Array.isArray(headers)) return Object.fromEntries(headers)
|
|
23
|
-
return Object.fromEntries(Object.entries(headers))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const authorizationHeader = (headers: HeadersInit | undefined): string | undefined => {
|
|
27
|
-
const record = headersRecord(headers)
|
|
28
|
-
return record.Authorization ?? record.authorization
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Mock global fetch to capture requests and return canned responses.
|
|
33
|
-
*/
|
|
34
|
-
const withMockFetch = <A, E>(
|
|
35
|
-
responses: Array<{ status: number; body: unknown }>,
|
|
36
|
-
fn: (capturedRequests: Array<{ url: string; init: RequestInit }>) => Effect.Effect<A, E>
|
|
37
|
-
): Effect.Effect<A, E> => {
|
|
38
|
-
const capturedRequests: Array<{ url: string; init: RequestInit }> = []
|
|
39
|
-
let requestIndex = 0
|
|
40
|
-
const originalFetch = fetch
|
|
41
|
-
|
|
42
|
-
vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
43
|
-
const url = typeof input === "string" ? input : isRequest(input) ? input.url : input.toString()
|
|
44
|
-
const headers = isRequest(input)
|
|
45
|
-
? Object.fromEntries(input.headers.entries())
|
|
46
|
-
: headersRecord(init?.headers)
|
|
47
|
-
capturedRequests.push({ url, init: { ...init, headers } })
|
|
48
|
-
const response = responses[requestIndex] ?? { status: 200, body: {} }
|
|
49
|
-
requestIndex++
|
|
50
|
-
return new Response(JSON.stringify(response.body), {
|
|
51
|
-
status: response.status,
|
|
52
|
-
headers: { "content-type": "application/json" }
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
return Effect.ensuring(
|
|
57
|
-
fn(capturedRequests),
|
|
58
|
-
Effect.sync(() => {
|
|
59
|
-
vi.stubGlobal("fetch", originalFetch)
|
|
60
|
-
})
|
|
61
|
-
)
|
|
62
41
|
}
|
|
63
42
|
|
|
64
43
|
describe("JiraApiClient", () => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
const client = yield* JiraApiClient.pipe(
|
|
82
|
-
Effect.provide(JiraApiClient.layer),
|
|
83
|
-
Effect.provide(configLayer)
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
const issue = yield* toEffect(client.v3.client.GET("/rest/api/3/issue/{issueIdOrKey}", {
|
|
87
|
-
params: { path: { issueIdOrKey: "PROJ-1" } }
|
|
88
|
-
}))
|
|
89
|
-
|
|
90
|
-
expect(issue.id).toBe("10001")
|
|
91
|
-
expect(issue.key).toBe("PROJ-1")
|
|
92
|
-
expect(capturedRequests).toHaveLength(1)
|
|
93
|
-
|
|
94
|
-
const request = capturedRequests[0]!
|
|
95
|
-
expect(request.url).toContain("test.atlassian.net")
|
|
96
|
-
expect(request.url).toContain("/rest/api/3/issue/PROJ-1")
|
|
97
|
-
const authHeader = authorizationHeader(request.init.headers)
|
|
98
|
-
expect(authHeader).toMatch(/^Basic /)
|
|
99
|
-
})
|
|
100
|
-
))
|
|
101
|
-
|
|
102
|
-
// Verifies OAuth2 applies Bearer token and routes through Atlassian cloud proxy
|
|
103
|
-
it.effect("creates client with OAuth2 auth and applies Bearer token", () =>
|
|
104
|
-
withMockFetch(
|
|
105
|
-
[{ status: 200, body: { id: "10001", key: "PROJ-1", fields: { summary: "Test" } } }],
|
|
106
|
-
(capturedRequests) =>
|
|
107
|
-
Effect.gen(function*() {
|
|
108
|
-
const configLayer = Layer.succeed(JiraApiConfig, {
|
|
109
|
-
baseUrl: "https://test.atlassian.net",
|
|
110
|
-
auth: {
|
|
111
|
-
type: "oauth2",
|
|
112
|
-
accessToken: Redacted.make("oauth-token"),
|
|
113
|
-
cloudId: "cloud-123"
|
|
114
|
-
}
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
const client = yield* JiraApiClient.pipe(
|
|
118
|
-
Effect.provide(JiraApiClient.layer),
|
|
119
|
-
Effect.provide(configLayer)
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
const issue = yield* toEffect(client.v3.client.GET("/rest/api/3/issue/{issueIdOrKey}", {
|
|
123
|
-
params: { path: { issueIdOrKey: "PROJ-1" } }
|
|
124
|
-
}))
|
|
125
|
-
|
|
126
|
-
expect(issue.id).toBe("10001")
|
|
127
|
-
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
|
+
})
|
|
128
58
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
))
|
|
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
|
+
)))
|
|
135
74
|
})
|
|
136
75
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
})
|
|
148
141
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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)
|
|
153
147
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
148
|
+
expect(result).toBeUndefined()
|
|
149
|
+
expect(requests).toHaveLength(1)
|
|
150
|
+
}).pipe(Effect.provide(clientLayer(basicConfig, { status: 204 }, requests)))
|
|
151
|
+
})
|
|
158
152
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
)
|
|
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)))
|
|
168
169
|
})
|
|
169
170
|
})
|
package/.specs/VERSION_V3
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1001.0.0-SNAPSHOT-05f20e18d9bde3de68f911b4915970482f536d4e
|