@knpkv/jira-api-client 0.3.0 → 0.4.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/.specs/VERSION_V3 +1 -1
- package/.specs/jira-v3-1001.0.0-SNAPSHOT-05f20e18d9bde3de68f911b4915970482f536d4e.json +56275 -0
- package/.specs/jira-v3-1001.0.0-SNAPSHOT-d7cd03e966bb408ad32ffe870114289c528f56fd.json +56129 -0
- package/CHANGELOG.md +18 -0
- package/dist/JiraApiClient.d.ts.map +1 -1
- package/dist/JiraApiClient.js +1 -2
- package/dist/JiraApiClient.js.map +1 -1
- package/dist/OpenApiFetchClient.d.ts +2 -2
- package/dist/OpenApiFetchClient.d.ts.map +1 -1
- package/dist/OpenApiFetchClient.js +10 -5
- package/dist/OpenApiFetchClient.js.map +1 -1
- package/dist/generated/v3/schema.d.ts +1537 -1436
- package/package.json +6 -6
- package/scripts/regenerate.ts +79 -46
- package/src/JiraApiClient.ts +1 -2
- package/src/OpenApiFetchClient.ts +20 -10
- package/src/generated/v3/schema.d.ts +1537 -1436
- package/test/JiraApiClient.test.ts +37 -13
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knpkv/jira-api-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Effect-based Jira Cloud REST API v3 client",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "knpkv",
|
|
@@ -39,16 +39,16 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"effect": "4.0.0-beta.
|
|
42
|
+
"effect": "4.0.0-beta.93"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@effect/platform-node": "4.0.0-beta.
|
|
46
|
-
"@effect/vitest": "4.0.0-beta.
|
|
45
|
+
"@effect/platform-node": "4.0.0-beta.93",
|
|
46
|
+
"@effect/vitest": "4.0.0-beta.93",
|
|
47
47
|
"@types/node": "latest",
|
|
48
|
-
"effect": "4.0.0-beta.
|
|
48
|
+
"effect": "4.0.0-beta.93",
|
|
49
49
|
"openapi-typescript": "^7.13.0",
|
|
50
50
|
"openapi-typescript-helpers": "^0.1.0",
|
|
51
|
-
"tsx": "^4.
|
|
51
|
+
"tsx": "^4.23.0",
|
|
52
52
|
"typescript": "~6.0.3",
|
|
53
53
|
"vitest": "^4.1.9"
|
|
54
54
|
},
|
package/scripts/regenerate.ts
CHANGED
|
@@ -4,23 +4,24 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Fetches OpenAPI specs from Atlassian and regenerates openapi-typescript types.
|
|
6
6
|
*/
|
|
7
|
-
import { Command, Flag as Options } from "effect/unstable/cli"
|
|
8
7
|
import { NodeRuntime, NodeServices } from "@effect/platform-node"
|
|
9
8
|
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
|
|
10
9
|
import * as Console from "effect/Console"
|
|
10
|
+
import * as Data from "effect/Data"
|
|
11
11
|
import * as Effect from "effect/Effect"
|
|
12
12
|
import * as FileSystem from "effect/FileSystem"
|
|
13
13
|
import * as Path from "effect/Path"
|
|
14
|
-
import * as PlatformError from "effect/PlatformError"
|
|
15
|
-
import * as
|
|
14
|
+
import type * as PlatformError from "effect/PlatformError"
|
|
15
|
+
import * as Schema from "effect/Schema"
|
|
16
|
+
import { Command, Flag as Options } from "effect/unstable/cli"
|
|
16
17
|
import { HttpClient } from "effect/unstable/http"
|
|
17
18
|
import * as ChildProcess from "effect/unstable/process/ChildProcess"
|
|
18
19
|
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"
|
|
19
20
|
import pkg from "../package.json" with { type: "json" }
|
|
20
21
|
|
|
21
|
-
const SPEC_URLS = {
|
|
22
|
+
const SPEC_URLS: Readonly<Record<"v3", string>> = {
|
|
22
23
|
v3: "https://dac-static.atlassian.com/cloud/jira/platform/swagger-v3.v3.json"
|
|
23
|
-
}
|
|
24
|
+
}
|
|
24
25
|
|
|
25
26
|
interface SpecInfo {
|
|
26
27
|
version: string
|
|
@@ -34,30 +35,48 @@ interface ScriptPaths {
|
|
|
34
35
|
readonly generatedDir: string
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
class RegenerateError extends Data.TaggedError("RegenerateError")<{
|
|
39
|
+
readonly message: string
|
|
40
|
+
readonly cause?: unknown
|
|
41
|
+
}> {}
|
|
42
|
+
|
|
43
|
+
const JsonString = Schema.fromJsonString(Schema.Json)
|
|
44
|
+
|
|
45
|
+
const SpecInfoResponse = Schema.Struct({
|
|
46
|
+
info: Schema.Struct({
|
|
47
|
+
version: Schema.String,
|
|
48
|
+
title: Schema.String
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const fetchText = (url: string): Effect.Effect<string, RegenerateError, HttpClient.HttpClient> =>
|
|
38
53
|
HttpClient.get(url).pipe(
|
|
39
54
|
Effect.flatMap((response) =>
|
|
40
55
|
response.status >= 200 && response.status < 300
|
|
41
56
|
? response.text
|
|
42
|
-
: Effect.fail(new
|
|
57
|
+
: Effect.fail(new RegenerateError({ message: `Failed to fetch ${url}`, cause: { status: response.status } }))
|
|
43
58
|
),
|
|
44
|
-
Effect.mapError((
|
|
59
|
+
Effect.mapError((cause) => new RegenerateError({ message: `Fetch failed: ${url}`, cause }))
|
|
45
60
|
)
|
|
46
61
|
|
|
47
|
-
const
|
|
62
|
+
const fetchSpecInfo = (url: string): Effect.Effect<SpecInfo, RegenerateError, HttpClient.HttpClient> =>
|
|
48
63
|
fetchText(url).pipe(
|
|
49
|
-
Effect.flatMap((
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(SpecInfoResponse))),
|
|
65
|
+
Effect.map(({ info }) => ({ version: info.version, title: info.title })),
|
|
66
|
+
Effect.mapError((cause) => new RegenerateError({ message: "JSON decode failed", cause }))
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const formatJson = (text: string): Effect.Effect<string, RegenerateError> =>
|
|
70
|
+
Schema.decodeUnknownEffect(JsonString)(text).pipe(
|
|
71
|
+
Effect.flatMap(Schema.encodeEffect(JsonString)),
|
|
72
|
+
Effect.map((encoded) => `${encoded}\n`),
|
|
73
|
+
Effect.mapError((cause) => new RegenerateError({ message: "JSON format failed", cause }))
|
|
55
74
|
)
|
|
56
75
|
|
|
57
|
-
const scriptPaths: Effect.Effect<ScriptPaths,
|
|
76
|
+
const scriptPaths: Effect.Effect<ScriptPaths, RegenerateError, Path.Path> = Effect.gen(function*() {
|
|
58
77
|
const path = yield* Path.Path
|
|
59
78
|
const scriptFile = yield* path.fromFileUrl(new URL(import.meta.url)).pipe(
|
|
60
|
-
Effect.mapError((
|
|
79
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Script path resolution failed", cause }))
|
|
61
80
|
)
|
|
62
81
|
const scriptDir = path.dirname(scriptFile)
|
|
63
82
|
const packageDir = path.join(scriptDir, "..")
|
|
@@ -71,50 +90,52 @@ const scriptPaths: Effect.Effect<ScriptPaths, Error, Path.Path> = Effect.gen(fun
|
|
|
71
90
|
}
|
|
72
91
|
})
|
|
73
92
|
|
|
74
|
-
const fetchSpecInfo = (url: string): Effect.Effect<SpecInfo, Error, HttpClient.HttpClient> =>
|
|
75
|
-
fetchJson<{ info: { version: string; title: string } }>(url).pipe(
|
|
76
|
-
Effect.map((spec) => ({ version: spec.info.version, title: spec.info.title }))
|
|
77
|
-
)
|
|
78
|
-
|
|
79
93
|
const fetchAndSaveSpec = (
|
|
94
|
+
packageDir: string,
|
|
80
95
|
url: string,
|
|
81
96
|
outputPath: string
|
|
82
|
-
): Effect.Effect<
|
|
97
|
+
): Effect.Effect<
|
|
98
|
+
void,
|
|
99
|
+
RegenerateError,
|
|
100
|
+
ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | HttpClient.HttpClient
|
|
101
|
+
> =>
|
|
83
102
|
Effect.gen(function*() {
|
|
84
103
|
const fs = yield* FileSystem.FileSystem
|
|
85
104
|
const spec = yield* fetchText(url)
|
|
86
|
-
yield*
|
|
87
|
-
|
|
105
|
+
const formattedSpec = yield* formatJson(spec)
|
|
106
|
+
yield* fs.writeFileString(outputPath, formattedSpec).pipe(
|
|
107
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Save failed", cause }))
|
|
88
108
|
)
|
|
109
|
+
yield* formatFile(packageDir, outputPath)
|
|
89
110
|
})
|
|
90
111
|
|
|
91
|
-
const readVersion = (file: string): Effect.Effect<string | null,
|
|
112
|
+
const readVersion = (file: string): Effect.Effect<string | null, RegenerateError, FileSystem.FileSystem> =>
|
|
92
113
|
Effect.gen(function*() {
|
|
93
114
|
const fs = yield* FileSystem.FileSystem
|
|
94
115
|
const exists = yield* fs.exists(file).pipe(
|
|
95
|
-
Effect.mapError((
|
|
116
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Version lookup failed", cause }))
|
|
96
117
|
)
|
|
97
118
|
if (!exists) return null
|
|
98
119
|
|
|
99
120
|
return yield* fs.readFileString(file).pipe(
|
|
100
121
|
Effect.map((version) => version.trim()),
|
|
101
|
-
Effect.mapError((
|
|
122
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Version read failed", cause }))
|
|
102
123
|
)
|
|
103
124
|
})
|
|
104
125
|
|
|
105
|
-
const writeVersion = (file: string, version: string): Effect.Effect<void,
|
|
126
|
+
const writeVersion = (file: string, version: string): Effect.Effect<void, RegenerateError, FileSystem.FileSystem> =>
|
|
106
127
|
Effect.gen(function*() {
|
|
107
128
|
const fs = yield* FileSystem.FileSystem
|
|
108
129
|
yield* fs.writeFileString(file, version).pipe(
|
|
109
|
-
Effect.mapError((
|
|
130
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Version write failed", cause }))
|
|
110
131
|
)
|
|
111
132
|
})
|
|
112
133
|
|
|
113
|
-
const ensureDir = (dir: string): Effect.Effect<void,
|
|
134
|
+
const ensureDir = (dir: string): Effect.Effect<void, RegenerateError, FileSystem.FileSystem> =>
|
|
114
135
|
Effect.gen(function*() {
|
|
115
136
|
const fs = yield* FileSystem.FileSystem
|
|
116
137
|
yield* fs.makeDirectory(dir, { recursive: true }).pipe(
|
|
117
|
-
Effect.mapError((
|
|
138
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Directory creation failed", cause }))
|
|
118
139
|
)
|
|
119
140
|
})
|
|
120
141
|
|
|
@@ -122,7 +143,7 @@ const generateTypes = (
|
|
|
122
143
|
packageDir: string,
|
|
123
144
|
generatedDir: string,
|
|
124
145
|
specPath: string
|
|
125
|
-
): Effect.Effect<void,
|
|
146
|
+
): Effect.Effect<void, RegenerateError, ChildProcessSpawner.ChildProcessSpawner | Path.Path> =>
|
|
126
147
|
Effect.gen(function*() {
|
|
127
148
|
const path = yield* Path.Path
|
|
128
149
|
const outputFile = path.join(generatedDir, "schema.d.ts")
|
|
@@ -133,11 +154,11 @@ const generateTypes = (
|
|
|
133
154
|
stderr: "inherit"
|
|
134
155
|
})
|
|
135
156
|
const exitCode = yield* commandExitCode(command).pipe(
|
|
136
|
-
Effect.mapError((
|
|
157
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Type generation failed", cause }))
|
|
137
158
|
)
|
|
138
159
|
|
|
139
160
|
if (exitCode !== 0) {
|
|
140
|
-
return yield* Effect.fail(new
|
|
161
|
+
return yield* Effect.fail(new RegenerateError({ message: "Type generation failed", cause: { exitCode } }))
|
|
141
162
|
}
|
|
142
163
|
})
|
|
143
164
|
|
|
@@ -148,9 +169,25 @@ const commandExitCode = (
|
|
|
148
169
|
Effect.flatMap((spawner) => spawner.exitCode(command))
|
|
149
170
|
)
|
|
150
171
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
172
|
+
const formatFile = (
|
|
173
|
+
packageDir: string,
|
|
174
|
+
file: string
|
|
175
|
+
): Effect.Effect<void, RegenerateError, ChildProcessSpawner.ChildProcessSpawner> =>
|
|
176
|
+
Effect.gen(function*() {
|
|
177
|
+
const command = ChildProcess.make("pnpm", ["exec", "prettier", "--write", file], {
|
|
178
|
+
cwd: packageDir,
|
|
179
|
+
stdin: "inherit",
|
|
180
|
+
stdout: "inherit",
|
|
181
|
+
stderr: "inherit"
|
|
182
|
+
})
|
|
183
|
+
const exitCode = yield* commandExitCode(command).pipe(
|
|
184
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Formatting failed", cause }))
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if (exitCode !== 0) {
|
|
188
|
+
return yield* Effect.fail(new RegenerateError({ message: "Formatting failed", cause: { exitCode } }))
|
|
189
|
+
}
|
|
190
|
+
})
|
|
154
191
|
|
|
155
192
|
const checkOnly = Options.boolean("check").pipe(
|
|
156
193
|
Options.withDescription("Check only, exit 1 if outdated"),
|
|
@@ -179,7 +216,7 @@ const regenerate = Command.make("regenerate", { checkOnly }, ({ checkOnly }) =>
|
|
|
179
216
|
if (checkOnly) {
|
|
180
217
|
yield* Console.log("Spec is outdated!")
|
|
181
218
|
yield* Console.log(` V3: ${currentV3} -> ${v3Info.version}`)
|
|
182
|
-
return yield* Effect.fail(new
|
|
219
|
+
return yield* Effect.fail(new RegenerateError({ message: "Spec outdated" }))
|
|
183
220
|
}
|
|
184
221
|
|
|
185
222
|
yield* ensureDir(paths.specsDir)
|
|
@@ -188,7 +225,7 @@ const regenerate = Command.make("regenerate", { checkOnly }, ({ checkOnly }) =>
|
|
|
188
225
|
const specPath = path.join(paths.specsDir, `jira-v3-${v3Info.version}.json`)
|
|
189
226
|
|
|
190
227
|
yield* Console.log(`Fetching V3 spec (${v3Info.version})...`)
|
|
191
|
-
yield* fetchAndSaveSpec(SPEC_URLS.v3, specPath)
|
|
228
|
+
yield* fetchAndSaveSpec(paths.packageDir, SPEC_URLS.v3, specPath)
|
|
192
229
|
yield* Console.log(`Saved: ${specPath}`)
|
|
193
230
|
|
|
194
231
|
yield* Console.log("Generating types...")
|
|
@@ -197,18 +234,14 @@ const regenerate = Command.make("regenerate", { checkOnly }, ({ checkOnly }) =>
|
|
|
197
234
|
|
|
198
235
|
yield* writeVersion(paths.versionV3File, v3Info.version)
|
|
199
236
|
yield* Console.log("Done!")
|
|
200
|
-
})
|
|
201
|
-
).pipe(Command.withDescription("Regenerate Jira API types from OpenAPI specs"))
|
|
237
|
+
})).pipe(Command.withDescription("Regenerate Jira API types from OpenAPI specs"))
|
|
202
238
|
|
|
203
239
|
const cli = Command.run(regenerate, {
|
|
204
240
|
name: pkg.name,
|
|
205
241
|
version: pkg.version
|
|
206
242
|
})
|
|
207
243
|
|
|
208
|
-
|
|
209
|
-
const argv = yield* readArgv
|
|
210
|
-
yield* cli(argv)
|
|
211
|
-
}).pipe(
|
|
244
|
+
cli.pipe(
|
|
212
245
|
Effect.provide(NodeHttpClient.layerFetch),
|
|
213
246
|
Effect.provide(NodeServices.layer),
|
|
214
247
|
NodeRuntime.runMain
|
package/src/JiraApiClient.ts
CHANGED
|
@@ -110,8 +110,7 @@ export class JiraApiClient extends Context.Service<JiraApiClient, JiraApiClientS
|
|
|
110
110
|
|
|
111
111
|
const headers = {
|
|
112
112
|
Authorization: authHeader,
|
|
113
|
-
Accept: "application/json"
|
|
114
|
-
"Content-Type": "application/json"
|
|
113
|
+
Accept: "application/json"
|
|
115
114
|
}
|
|
116
115
|
|
|
117
116
|
return {
|
|
@@ -31,8 +31,20 @@ export class FetchClientError extends Data.TaggedError("FetchClientError")<{
|
|
|
31
31
|
|
|
32
32
|
/** Extract success `data` from a FetchResponse discriminated union. */
|
|
33
33
|
export type SuccessData<T> = T extends { data: infer D; error?: undefined } ? D
|
|
34
|
-
: T extends { data?: infer D } ?
|
|
35
|
-
:
|
|
34
|
+
: T extends { data?: infer D } ? D | undefined
|
|
35
|
+
: undefined
|
|
36
|
+
|
|
37
|
+
const isRecord = (value: unknown): value is Readonly<Record<PropertyKey, unknown>> =>
|
|
38
|
+
typeof value === "object" && value !== null
|
|
39
|
+
|
|
40
|
+
const errorPayload = (value: unknown): unknown => isRecord(value) && "error" in value ? value.error : value
|
|
41
|
+
|
|
42
|
+
const errorStatus = (value: unknown): number => isRecord(value) && typeof value.status === "number" ? value.status : 0
|
|
43
|
+
|
|
44
|
+
const errorMessage = (value: unknown): string => {
|
|
45
|
+
const payload = errorPayload(value)
|
|
46
|
+
return typeof payload === "string" ? payload : JSON.stringify(payload)
|
|
47
|
+
}
|
|
36
48
|
|
|
37
49
|
/**
|
|
38
50
|
* Wrap an openapi-fetch `Promise<FetchResponse>` in Effect.
|
|
@@ -57,15 +69,13 @@ export const toEffect = <T extends Record<string | number, any>, O, M extends Me
|
|
|
57
69
|
if (error !== undefined || !response.ok) {
|
|
58
70
|
throw { error, status: response.status }
|
|
59
71
|
}
|
|
60
|
-
return data
|
|
72
|
+
return data
|
|
61
73
|
}),
|
|
62
74
|
catch: (e) =>
|
|
63
75
|
new FetchClientError({
|
|
64
|
-
error: (e
|
|
65
|
-
status: (
|
|
66
|
-
message:
|
|
67
|
-
? (e as Record<string, unknown>).error as string
|
|
68
|
-
: JSON.stringify((e as Record<string, unknown>).error ?? e)
|
|
76
|
+
error: errorPayload(e),
|
|
77
|
+
status: errorStatus(e),
|
|
78
|
+
message: errorMessage(e)
|
|
69
79
|
})
|
|
70
80
|
})
|
|
71
81
|
|
|
@@ -76,7 +86,7 @@ export const toEffect = <T extends Record<string | number, any>, O, M extends Me
|
|
|
76
86
|
*/
|
|
77
87
|
export interface OpenApiFetchClient<Paths extends {}> {
|
|
78
88
|
/** Type-safe openapi-fetch client. Use with `toEffect()` to get Effect. */
|
|
79
|
-
readonly client: Client<Paths
|
|
89
|
+
readonly client: Client<Paths>
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
/**
|
|
@@ -88,5 +98,5 @@ export const makeOpenApiFetchClient = <Paths extends {}>(
|
|
|
88
98
|
baseUrl: string,
|
|
89
99
|
headers: Record<string, string>
|
|
90
100
|
): OpenApiFetchClient<Paths> => ({
|
|
91
|
-
client: createClient<Paths
|
|
101
|
+
client: createClient<Paths>({ baseUrl, headers })
|
|
92
102
|
})
|