@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/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
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
|
-
export { JiraApiClient, type JiraApiClientShape, layer } from "./JiraApiClient.js";
|
|
6
|
+
export { JiraApiClient, type JiraApiClientShape, layer, make, type UploadAttachmentInput } from "./JiraApiClient.js";
|
|
15
7
|
export { JiraApiConfig, type JiraApiConfigShape } from "./JiraApiConfig.js";
|
|
16
|
-
export
|
|
17
|
-
export type * as V3 from "./generated/v3/index.js";
|
|
8
|
+
export * as JiraApi from "./generated/JiraApi.js";
|
|
18
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAEpH,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAE3E,OAAO,KAAK,OAAO,MAAM,wBAAwB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
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
|
-
export { JiraApiClient, layer } from "./JiraApiClient.js";
|
|
6
|
+
export { JiraApiClient, layer, make } from "./JiraApiClient.js";
|
|
15
7
|
export { JiraApiConfig } from "./JiraApiConfig.js";
|
|
16
|
-
export
|
|
8
|
+
export * as JiraApi from "./generated/JiraApi.js";
|
|
17
9
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,aAAa,EAA2B,KAAK,EAAE,IAAI,EAA8B,MAAM,oBAAoB,CAAA;AAEpH,OAAO,EAAE,aAAa,EAA2B,MAAM,oBAAoB,CAAA;AAE3E,OAAO,KAAK,OAAO,MAAM,wBAAwB,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knpkv/jira-api-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Effect-based Jira Cloud REST API v3 client",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "knpkv",
|
|
@@ -30,27 +30,27 @@
|
|
|
30
30
|
"import": "./dist/JiraApiConfig.js",
|
|
31
31
|
"types": "./dist/JiraApiConfig.d.ts"
|
|
32
32
|
},
|
|
33
|
-
"./
|
|
34
|
-
"import": "./dist/generated/
|
|
35
|
-
"types": "./dist/generated/
|
|
33
|
+
"./generated": {
|
|
34
|
+
"import": "./dist/generated/JiraApi.js",
|
|
35
|
+
"types": "./dist/generated/JiraApi.d.ts"
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"effect": "4.0.0-beta.
|
|
42
|
+
"effect": "4.0.0-beta.98"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@effect/
|
|
46
|
-
"@effect/
|
|
45
|
+
"@effect/openapi-generator": "4.0.0-beta.98",
|
|
46
|
+
"@effect/platform-node": "4.0.0-beta.98",
|
|
47
|
+
"@effect/vitest": "4.0.0-beta.98",
|
|
47
48
|
"@types/node": "latest",
|
|
48
|
-
"effect": "4.0.0-beta.
|
|
49
|
-
"
|
|
50
|
-
"openapi-typescript-helpers": "^0.1.0",
|
|
49
|
+
"effect": "4.0.0-beta.98",
|
|
50
|
+
"prettier": "^3.9.5",
|
|
51
51
|
"tsx": "^4.23.0",
|
|
52
52
|
"typescript": "~6.0.3",
|
|
53
|
-
"vitest": "^4.1.
|
|
53
|
+
"vitest": "^4.1.10"
|
|
54
54
|
},
|
|
55
55
|
"keywords": [
|
|
56
56
|
"effect",
|
|
@@ -60,17 +60,14 @@
|
|
|
60
60
|
"api-client",
|
|
61
61
|
"openapi"
|
|
62
62
|
],
|
|
63
|
-
"dependencies": {
|
|
64
|
-
"openapi-fetch": "^0.17.0"
|
|
65
|
-
},
|
|
66
63
|
"scripts": {
|
|
67
|
-
"build": "tsc
|
|
64
|
+
"build": "tsc",
|
|
68
65
|
"clean": "rimraf dist dist-test .tsbuildinfo",
|
|
69
66
|
"test": "vitest run",
|
|
70
67
|
"test:watch": "vitest",
|
|
71
68
|
"lint": "eslint src",
|
|
72
69
|
"lint:fix": "eslint src --fix",
|
|
73
|
-
"check": "tsc --noEmit",
|
|
70
|
+
"check": "tsc --noEmit && tsc -p scripts/tsconfig.json",
|
|
74
71
|
"regenerate": "tsx scripts/regenerate.ts",
|
|
75
72
|
"regenerate:check": "tsx scripts/regenerate.ts --check"
|
|
76
73
|
}
|
package/scripts/regenerate.ts
CHANGED
|
@@ -1,39 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Fetch, compare, patch, normalize, and regenerate the Jira Cloud API client.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* The committed OpenAPI document is the unmodified upstream document. RFC 6902
|
|
6
|
+
* patches and deterministic compatibility normalizations are applied in memory.
|
|
6
7
|
*/
|
|
7
|
-
import
|
|
8
|
-
import * as
|
|
8
|
+
import * as OpenApiGenerator from "@effect/openapi-generator/OpenApiGenerator"
|
|
9
|
+
import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
|
|
10
|
+
import { NodeHttpClient, NodeRuntime, NodeServices } from "@effect/platform-node"
|
|
9
11
|
import * as Console from "effect/Console"
|
|
10
12
|
import * as Data from "effect/Data"
|
|
11
13
|
import * as Effect from "effect/Effect"
|
|
12
14
|
import * as FileSystem from "effect/FileSystem"
|
|
15
|
+
import type * as JsonSchema from "effect/JsonSchema"
|
|
16
|
+
import * as Layer from "effect/Layer"
|
|
13
17
|
import * as Path from "effect/Path"
|
|
14
|
-
import
|
|
18
|
+
import * as Predicate from "effect/Predicate"
|
|
15
19
|
import * as Schema from "effect/Schema"
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import * as
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
|
|
22
|
-
const SPEC_URLS: Readonly<Record<"v3", string>> = {
|
|
23
|
-
v3: "https://dac-static.atlassian.com/cloud/jira/platform/swagger-v3.v3.json"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface SpecInfo {
|
|
27
|
-
version: string
|
|
28
|
-
title: string
|
|
29
|
-
}
|
|
20
|
+
import * as Command from "effect/unstable/cli/Command"
|
|
21
|
+
import * as Flag from "effect/unstable/cli/Flag"
|
|
22
|
+
import * as HttpClient from "effect/unstable/http/HttpClient"
|
|
23
|
+
import type { OpenAPISpec } from "effect/unstable/httpapi/OpenApi"
|
|
24
|
+
import { format as formatSource } from "prettier"
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
readonly specsDir: string
|
|
34
|
-
readonly versionV3File: string
|
|
35
|
-
readonly generatedDir: string
|
|
36
|
-
}
|
|
26
|
+
const SPEC_URL = "https://dac-static.atlassian.com/cloud/jira/platform/swagger-v3.v3.json"
|
|
27
|
+
const METHODS = new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"])
|
|
37
28
|
|
|
38
29
|
class RegenerateError extends Data.TaggedError("RegenerateError")<{
|
|
39
30
|
readonly message: string
|
|
@@ -42,207 +33,224 @@ class RegenerateError extends Data.TaggedError("RegenerateError")<{
|
|
|
42
33
|
|
|
43
34
|
const JsonString = Schema.fromJsonString(Schema.Json)
|
|
44
35
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const fetchText = (url: string): Effect.Effect<string, RegenerateError, HttpClient.HttpClient> =>
|
|
53
|
-
HttpClient.get(url).pipe(
|
|
54
|
-
Effect.flatMap((response) =>
|
|
55
|
-
response.status >= 200 && response.status < 300
|
|
56
|
-
? response.text
|
|
57
|
-
: Effect.fail(new RegenerateError({ message: `Failed to fetch ${url}`, cause: { status: response.status } }))
|
|
58
|
-
),
|
|
59
|
-
Effect.mapError((cause) => new RegenerateError({ message: `Fetch failed: ${url}`, cause }))
|
|
60
|
-
)
|
|
36
|
+
const OpenApiDocument = Schema.declare<OpenAPISpec>(
|
|
37
|
+
(value): value is OpenAPISpec =>
|
|
38
|
+
Predicate.hasProperty(value, "paths") &&
|
|
39
|
+
Predicate.isObject(value.paths) &&
|
|
40
|
+
Predicate.hasProperty(value, "openapi"),
|
|
41
|
+
{ identifier: "OpenApiDocument" }
|
|
42
|
+
)
|
|
61
43
|
|
|
62
|
-
const
|
|
63
|
-
fetchText(url).pipe(
|
|
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
|
-
)
|
|
44
|
+
const SpecInfo = Schema.Struct({ info: Schema.Struct({ version: Schema.String }) })
|
|
68
45
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
46
|
+
interface ScriptPaths {
|
|
47
|
+
readonly specFile: string
|
|
48
|
+
readonly patchFile: string
|
|
49
|
+
readonly outputFile: string
|
|
50
|
+
}
|
|
75
51
|
|
|
76
|
-
const
|
|
52
|
+
const paths = Effect.gen(function*() {
|
|
77
53
|
const path = yield* Path.Path
|
|
78
54
|
const scriptFile = yield* path.fromFileUrl(new URL(import.meta.url)).pipe(
|
|
79
|
-
Effect.mapError((cause) => new RegenerateError({ message: "
|
|
55
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not resolve script path", cause }))
|
|
80
56
|
)
|
|
81
|
-
const
|
|
82
|
-
const packageDir = path.join(scriptDir, "..")
|
|
57
|
+
const packageDir = path.join(path.dirname(scriptFile), "..")
|
|
83
58
|
const specsDir = path.join(packageDir, ".specs")
|
|
84
|
-
|
|
85
59
|
return {
|
|
86
|
-
|
|
87
|
-
specsDir,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
60
|
+
specFile: path.join(specsDir, "jira-v3.json"),
|
|
61
|
+
patchFile: path.join(specsDir, "jira-v3.patch.json"),
|
|
62
|
+
outputFile: path.join(packageDir, "src", "generated", "JiraApi.ts")
|
|
63
|
+
} satisfies ScriptPaths
|
|
91
64
|
})
|
|
92
65
|
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
):
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
Effect.
|
|
103
|
-
|
|
104
|
-
const spec = yield* fetchText(url)
|
|
105
|
-
const formattedSpec = yield* formatJson(spec)
|
|
106
|
-
yield* fs.writeFileString(outputPath, formattedSpec).pipe(
|
|
107
|
-
Effect.mapError((cause) => new RegenerateError({ message: "Save failed", cause }))
|
|
108
|
-
)
|
|
109
|
-
yield* formatFile(packageDir, outputPath)
|
|
110
|
-
})
|
|
66
|
+
const fetchUpstream = HttpClient.get(SPEC_URL).pipe(
|
|
67
|
+
Effect.flatMap((response) =>
|
|
68
|
+
response.status >= 200 && response.status < 300
|
|
69
|
+
? response.text.pipe(
|
|
70
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not read Jira response", cause }))
|
|
71
|
+
)
|
|
72
|
+
: Effect.fail(new RegenerateError({ message: `Jira spec request failed with status ${response.status}` }))
|
|
73
|
+
),
|
|
74
|
+
Effect.flatMap(Schema.decodeUnknownEffect(JsonString)),
|
|
75
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not fetch or decode Jira spec", cause }))
|
|
76
|
+
)
|
|
111
77
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
118
|
-
if (!exists) return null
|
|
119
|
-
|
|
120
|
-
return yield* fs.readFileString(file).pipe(
|
|
121
|
-
Effect.map((version) => version.trim()),
|
|
122
|
-
Effect.mapError((cause) => new RegenerateError({ message: "Version read failed", cause }))
|
|
123
|
-
)
|
|
124
|
-
})
|
|
78
|
+
const canonicalJson = (value: Schema.Json) =>
|
|
79
|
+
Schema.encodeEffect(JsonString)(value).pipe(
|
|
80
|
+
Effect.map((json) => `${json}\n`),
|
|
81
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not encode Jira spec", cause }))
|
|
82
|
+
)
|
|
125
83
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
84
|
+
const formattedJson = (value: Schema.Json) =>
|
|
85
|
+
Schema.encodeEffect(JsonString)(value).pipe(
|
|
86
|
+
Effect.flatMap((source) =>
|
|
87
|
+
Effect.tryPromise({
|
|
88
|
+
try: () => formatSource(source, { parser: "json", printWidth: 120, trailingComma: "none", semi: false }),
|
|
89
|
+
catch: (cause) => new RegenerateError({ message: "Could not format Jira spec", cause })
|
|
90
|
+
})
|
|
91
|
+
),
|
|
92
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not format Jira spec", cause }))
|
|
93
|
+
)
|
|
133
94
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Jira marks mixed objects as open. The generator's intersection type cannot
|
|
97
|
+
* combine an unrestricted JSON index signature with optional known fields, so
|
|
98
|
+
* close only those mixed objects. Pure map schemas such as IssueBean.fields
|
|
99
|
+
* retain their schema-valued additionalProperties and remain dynamic.
|
|
100
|
+
*
|
|
101
|
+
* Jira also publishes defaults that contradict enum/array schemas. Defaults
|
|
102
|
+
* and examples are documentation metadata, not response-validation rules.
|
|
103
|
+
*/
|
|
104
|
+
const normalizeSchema = (schema: JsonSchema.JsonSchema): JsonSchema.JsonSchema => {
|
|
105
|
+
const { default: _default, examples: _examples, ...normalized } = schema
|
|
106
|
+
return normalized.additionalProperties === true && Predicate.isObject(normalized.properties)
|
|
107
|
+
? { ...normalized, additionalProperties: false }
|
|
108
|
+
: normalized
|
|
109
|
+
}
|
|
141
110
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
specPath: string
|
|
146
|
-
): Effect.Effect<void, RegenerateError, ChildProcessSpawner.ChildProcessSpawner | Path.Path> =>
|
|
147
|
-
Effect.gen(function*() {
|
|
148
|
-
const path = yield* Path.Path
|
|
149
|
-
const outputFile = path.join(generatedDir, "schema.d.ts")
|
|
150
|
-
const command = ChildProcess.make("pnpm", ["exec", "openapi-typescript", specPath, "-o", outputFile], {
|
|
151
|
-
cwd: packageDir,
|
|
152
|
-
stdin: "inherit",
|
|
153
|
-
stdout: "inherit",
|
|
154
|
-
stderr: "inherit"
|
|
155
|
-
})
|
|
156
|
-
const exitCode = yield* commandExitCode(command).pipe(
|
|
157
|
-
Effect.mapError((cause) => new RegenerateError({ message: "Type generation failed", cause }))
|
|
158
|
-
)
|
|
111
|
+
const bodylessErrorContent: Schema.Json = {
|
|
112
|
+
"application/json": { schema: {} }
|
|
113
|
+
}
|
|
159
114
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
115
|
+
/**
|
|
116
|
+
* A 204 response can never contain a message body, but Jira attaches empty JSON
|
|
117
|
+
* schemas to many of them. Remove that misleading content so the generator
|
|
118
|
+
* emits `void` handlers instead of JSON decoders.
|
|
119
|
+
*
|
|
120
|
+
* Atlassian also omits bodies from many documented 4xx/5xx responses. Without
|
|
121
|
+
* a content schema the generator treats those statuses as successful voids.
|
|
122
|
+
* Give them a permissive JSON body so they remain failures with status-bearing
|
|
123
|
+
* generated JiraApiError values.
|
|
124
|
+
*/
|
|
125
|
+
const normalizeResponses = (document: Schema.Json): Schema.Json => {
|
|
126
|
+
if (!Predicate.isObject(document)) return document
|
|
127
|
+
const root = document
|
|
128
|
+
const rootPaths = root.paths
|
|
129
|
+
if (!Predicate.isObject(rootPaths)) return document
|
|
130
|
+
|
|
131
|
+
const normalizedPaths = Object.fromEntries(
|
|
132
|
+
Object.entries(rootPaths).map(([path, pathItem]) => {
|
|
133
|
+
if (!Predicate.isObject(pathItem)) return [path, pathItem]
|
|
134
|
+
const normalizedPathItem = Object.fromEntries(
|
|
135
|
+
Object.entries(pathItem).map(([key, operation]) => {
|
|
136
|
+
if (!METHODS.has(key) || !Predicate.isObject(operation) || !Predicate.isObject(operation.responses)) {
|
|
137
|
+
return [key, operation]
|
|
138
|
+
}
|
|
139
|
+
const responses = Object.fromEntries(
|
|
140
|
+
Object.entries(operation.responses).map(([status, response]) => {
|
|
141
|
+
const code = Number(status)
|
|
142
|
+
if (code === 204 && Predicate.isObject(response)) {
|
|
143
|
+
return [
|
|
144
|
+
status,
|
|
145
|
+
Object.fromEntries(
|
|
146
|
+
Object.entries(response).filter(([key]) => key !== "content")
|
|
147
|
+
)
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
if (!Number.isInteger(code) || code < 400 || !Predicate.isObject(response) || "content" in response) {
|
|
151
|
+
return [status, response]
|
|
152
|
+
}
|
|
153
|
+
return [status, { ...response, content: bodylessErrorContent }]
|
|
154
|
+
})
|
|
155
|
+
)
|
|
156
|
+
return [key, { ...operation, responses }]
|
|
157
|
+
})
|
|
158
|
+
)
|
|
159
|
+
return [path, normalizedPathItem]
|
|
160
|
+
})
|
|
161
|
+
)
|
|
164
162
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
): Effect.Effect<ChildProcessSpawner.ExitCode, PlatformError.PlatformError, ChildProcessSpawner.ChildProcessSpawner> =>
|
|
168
|
-
ChildProcessSpawner.ChildProcessSpawner.pipe(
|
|
169
|
-
Effect.flatMap((spawner) => spawner.exitCode(command))
|
|
163
|
+
return Schema.decodeUnknownSync(Schema.Json)(
|
|
164
|
+
Object.fromEntries([...Object.entries(root), ["paths", normalizedPaths]])
|
|
170
165
|
)
|
|
166
|
+
}
|
|
171
167
|
|
|
172
|
-
const
|
|
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
|
-
)
|
|
168
|
+
const stripTrailingWhitespace = (source: string): string => source.replace(/[ \t]+$/gm, "")
|
|
186
169
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
170
|
+
const generate = Effect.fn("Jira.regenerate.generate")(function*(scriptPaths: ScriptPaths, upstream: Schema.Json) {
|
|
171
|
+
const fs = yield* FileSystem.FileSystem
|
|
172
|
+
const patch = yield* OpenApiPatch.parsePatchInput(scriptPaths.patchFile).pipe(
|
|
173
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not parse Jira JSON patch", cause }))
|
|
174
|
+
)
|
|
175
|
+
const patched = yield* OpenApiPatch.applyPatches(
|
|
176
|
+
[{ source: scriptPaths.patchFile, patch }],
|
|
177
|
+
upstream
|
|
178
|
+
).pipe(
|
|
179
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not apply Jira JSON patch", cause }))
|
|
180
|
+
)
|
|
181
|
+
const normalized = normalizeResponses(patched)
|
|
182
|
+
const document = yield* Schema.decodeUnknownEffect(OpenApiDocument)(normalized).pipe(
|
|
183
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Normalized Jira document is not OpenAPI", cause }))
|
|
184
|
+
)
|
|
185
|
+
const generator = yield* OpenApiGenerator.OpenApiGenerator
|
|
186
|
+
const warnings: Array<OpenApiGenerator.OpenApiGeneratorWarning> = []
|
|
187
|
+
const generated = yield* generator.generate(document, {
|
|
188
|
+
name: "JiraApi",
|
|
189
|
+
format: "httpclient",
|
|
190
|
+
onEnter: normalizeSchema,
|
|
191
|
+
onWarning: (warning) => warnings.push(warning)
|
|
190
192
|
})
|
|
193
|
+
yield* Effect.forEach(
|
|
194
|
+
warnings,
|
|
195
|
+
(warning) => Console.warn(`[${warning.code}] ${warning.method ?? ""} ${warning.path ?? ""}: ${warning.message}`),
|
|
196
|
+
{ discard: true }
|
|
197
|
+
)
|
|
198
|
+
yield* fs.writeFileString(scriptPaths.outputFile, stripTrailingWhitespace(generated)).pipe(
|
|
199
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not write generated Jira client", cause }))
|
|
200
|
+
)
|
|
201
|
+
})
|
|
191
202
|
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
Options.withDefault(false)
|
|
203
|
+
const check = Flag.boolean("check").pipe(
|
|
204
|
+
Flag.withDescription("Exit non-zero when the committed upstream spec differs from Jira")
|
|
195
205
|
)
|
|
196
206
|
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const paths = yield* scriptPaths
|
|
201
|
-
|
|
202
|
-
yield* Console.log("Fetching Jira API spec version...")
|
|
203
|
-
|
|
204
|
-
const v3Info = yield* fetchSpecInfo(SPEC_URLS.v3)
|
|
205
|
-
const currentV3 = yield* readVersion(paths.versionV3File)
|
|
206
|
-
|
|
207
|
-
yield* Console.log(`V3: current=${currentV3 ?? "none"}, remote=${v3Info.version}`)
|
|
208
|
-
|
|
209
|
-
const v3Changed = currentV3 !== v3Info.version
|
|
210
|
-
|
|
211
|
-
if (!v3Changed) {
|
|
212
|
-
yield* Console.log("Spec is up to date.")
|
|
213
|
-
return
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (checkOnly) {
|
|
217
|
-
yield* Console.log("Spec is outdated!")
|
|
218
|
-
yield* Console.log(` V3: ${currentV3} -> ${v3Info.version}`)
|
|
219
|
-
return yield* Effect.fail(new RegenerateError({ message: "Spec outdated" }))
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
yield* ensureDir(paths.specsDir)
|
|
223
|
-
yield* ensureDir(paths.generatedDir)
|
|
224
|
-
|
|
225
|
-
const specPath = path.join(paths.specsDir, `jira-v3-${v3Info.version}.json`)
|
|
226
|
-
|
|
227
|
-
yield* Console.log(`Fetching V3 spec (${v3Info.version})...`)
|
|
228
|
-
yield* fetchAndSaveSpec(paths.packageDir, SPEC_URLS.v3, specPath)
|
|
229
|
-
yield* Console.log(`Saved: ${specPath}`)
|
|
230
|
-
|
|
231
|
-
yield* Console.log("Generating types...")
|
|
232
|
-
yield* generateTypes(paths.packageDir, paths.generatedDir, specPath)
|
|
233
|
-
yield* Console.log(`Generated: src/generated/v3/schema.d.ts`)
|
|
207
|
+
const local = Flag.boolean("local").pipe(
|
|
208
|
+
Flag.withDescription("Regenerate from the committed spec without contacting Jira")
|
|
209
|
+
)
|
|
234
210
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
211
|
+
const root = Command.make("jira-api-regenerate", { check, local }).pipe(
|
|
212
|
+
Command.withHandler(
|
|
213
|
+
Effect.fn("Jira.regenerate")(function*({ check, local }) {
|
|
214
|
+
const fs = yield* FileSystem.FileSystem
|
|
215
|
+
const scriptPaths = yield* paths
|
|
216
|
+
const upstream = local
|
|
217
|
+
? yield* fs.readFileString(scriptPaths.specFile).pipe(
|
|
218
|
+
Effect.flatMap(Schema.decodeUnknownEffect(JsonString)),
|
|
219
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not read committed Jira spec", cause }))
|
|
220
|
+
)
|
|
221
|
+
: yield* fetchUpstream
|
|
222
|
+
const encoded = yield* canonicalJson(upstream)
|
|
223
|
+
const current = yield* fs.readFileString(scriptPaths.specFile).pipe(
|
|
224
|
+
Effect.flatMap(Schema.decodeUnknownEffect(JsonString)),
|
|
225
|
+
Effect.flatMap(canonicalJson),
|
|
226
|
+
Effect.orElseSucceed(() => "")
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if (check) {
|
|
230
|
+
if (local) return yield* new RegenerateError({ message: "--check and --local cannot be combined" })
|
|
231
|
+
if (current === encoded) return yield* Console.log("Jira OpenAPI spec is current.")
|
|
232
|
+
return yield* new RegenerateError({
|
|
233
|
+
message: "Jira OpenAPI spec changed; run `pnpm --filter @knpkv/jira-api-client regenerate`"
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const { info } = yield* Schema.decodeUnknownEffect(SpecInfo)(upstream).pipe(
|
|
238
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Jira spec has no version", cause }))
|
|
239
|
+
)
|
|
240
|
+
yield* fs.writeFileString(scriptPaths.specFile, yield* formattedJson(upstream))
|
|
241
|
+
yield* generate(scriptPaths, upstream)
|
|
242
|
+
yield* Console.log(`Generated Jira ${info.version} client from ${local ? "the committed spec" : SPEC_URL}`)
|
|
243
|
+
})
|
|
244
|
+
)
|
|
245
|
+
)
|
|
238
246
|
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
247
|
+
const MainLayer = Layer.mergeAll(
|
|
248
|
+
OpenApiGenerator.layerTransformerSchema,
|
|
249
|
+
NodeHttpClient.layerFetch,
|
|
250
|
+
NodeServices.layer
|
|
251
|
+
)
|
|
243
252
|
|
|
244
|
-
|
|
245
|
-
Effect.provide(
|
|
246
|
-
Effect.provide(NodeServices.layer),
|
|
253
|
+
Command.run(root, { version: "1.0.0" }).pipe(
|
|
254
|
+
Effect.provide(MainLayer),
|
|
247
255
|
NodeRuntime.runMain
|
|
248
256
|
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.jsonc",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"composite": false,
|
|
5
|
+
"declaration": false,
|
|
6
|
+
"declarationMap": false,
|
|
7
|
+
"incremental": false,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"rootDir": "..",
|
|
10
|
+
"types": ["node"]
|
|
11
|
+
},
|
|
12
|
+
"include": ["./**/*.ts"]
|
|
13
|
+
}
|