@knpkv/jira-api-client 0.2.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knpkv/jira-api-client",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Effect-based Jira Cloud REST API v3 client",
5
5
  "license": "MIT",
6
6
  "author": "knpkv",
@@ -39,19 +39,18 @@
39
39
  "access": "public"
40
40
  },
41
41
  "peerDependencies": {
42
- "effect": "^3.19.3"
42
+ "effect": "4.0.0-beta.93"
43
43
  },
44
44
  "devDependencies": {
45
- "@effect/cli": "latest",
46
- "@effect/platform-node": "latest",
47
- "@effect/vitest": "latest",
45
+ "@effect/platform-node": "4.0.0-beta.93",
46
+ "@effect/vitest": "4.0.0-beta.93",
48
47
  "@types/node": "latest",
49
- "effect": "latest",
48
+ "effect": "4.0.0-beta.93",
50
49
  "openapi-typescript": "^7.13.0",
51
50
  "openapi-typescript-helpers": "^0.1.0",
52
- "tsx": "^4.19.4",
53
- "typescript": "~5.9.0",
54
- "vitest": "^4.0.13"
51
+ "tsx": "^4.23.0",
52
+ "typescript": "~6.0.3",
53
+ "vitest": "^4.1.9"
55
54
  },
56
55
  "keywords": [
57
56
  "effect",
@@ -4,21 +4,21 @@
4
4
  *
5
5
  * Fetches OpenAPI specs from Atlassian and regenerates openapi-typescript types.
6
6
  */
7
- import { Command, Options } from "@effect/cli"
8
- import { NodeContext, NodeRuntime } from "@effect/platform-node"
7
+ import { Command, Flag as Options } from "effect/unstable/cli"
8
+ import { NodeRuntime, NodeServices } from "@effect/platform-node"
9
+ import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
9
10
  import * as Console from "effect/Console"
11
+ import * as Data from "effect/Data"
10
12
  import * as Effect from "effect/Effect"
11
- import { execSync } from "node:child_process"
12
- import * as fs from "node:fs"
13
- import * as path from "node:path"
14
- import { fileURLToPath } from "node:url"
13
+ import * as FileSystem from "effect/FileSystem"
14
+ import * as Path from "effect/Path"
15
+ import * as PlatformError from "effect/PlatformError"
16
+ import * as Schema from "effect/Schema"
17
+ import { HttpClient } from "effect/unstable/http"
18
+ import * as ChildProcess from "effect/unstable/process/ChildProcess"
19
+ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"
15
20
  import pkg from "../package.json" with { type: "json" }
16
21
 
17
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
18
- const SPECS_DIR = path.join(__dirname, "..", ".specs")
19
- const VERSION_V3_FILE = path.join(SPECS_DIR, "VERSION_V3")
20
- const GENERATED_DIR = path.join(__dirname, "..", "src", "generated", "v3")
21
-
22
22
  const SPEC_URLS = {
23
23
  v3: "https://dac-static.atlassian.com/cloud/jira/platform/swagger-v3.v3.json"
24
24
  } as const
@@ -28,61 +28,165 @@ interface SpecInfo {
28
28
  title: string
29
29
  }
30
30
 
31
- const fetchSpecInfo = (url: string): Effect.Effect<SpecInfo, Error> =>
32
- Effect.tryPromise({
33
- try: async () => {
34
- const response = await fetch(url)
35
- if (!response.ok) {
36
- throw new Error(`Failed to fetch ${url}: ${response.status}`)
37
- }
38
- const spec = await response.json() as { info: { version: string; title: string } }
39
- return { version: spec.info.version, title: spec.info.title }
40
- },
41
- catch: (e) => new Error(`Fetch failed: ${e}`)
31
+ interface ScriptPaths {
32
+ readonly packageDir: string
33
+ readonly specsDir: string
34
+ readonly versionV3File: string
35
+ readonly generatedDir: string
36
+ }
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
42
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
+ )
61
+
62
+ const fetchSpecInfo = (url: string): Effect.Effect<SpecInfo, RegenerateError, HttpClient.HttpClient> =>
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
+ )
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 }))
74
+ )
75
+
76
+ const scriptPaths: Effect.Effect<ScriptPaths, RegenerateError, Path.Path> = Effect.gen(function*() {
77
+ const path = yield* Path.Path
78
+ const scriptFile = yield* path.fromFileUrl(new URL(import.meta.url)).pipe(
79
+ Effect.mapError((cause) => new RegenerateError({ message: "Script path resolution failed", cause }))
80
+ )
81
+ const scriptDir = path.dirname(scriptFile)
82
+ const packageDir = path.join(scriptDir, "..")
83
+ const specsDir = path.join(packageDir, ".specs")
84
+
85
+ return {
86
+ packageDir,
87
+ specsDir,
88
+ versionV3File: path.join(specsDir, "VERSION_V3"),
89
+ generatedDir: path.join(packageDir, "src", "generated", "v3")
90
+ }
91
+ })
43
92
 
44
- const fetchAndSaveSpec = (url: string, outputPath: string): Effect.Effect<void, Error> =>
45
- Effect.tryPromise({
46
- try: async () => {
47
- const response = await fetch(url)
48
- if (!response.ok) {
49
- throw new Error(`Failed to fetch ${url}: ${response.status}`)
50
- }
51
- const spec = await response.text()
52
- fs.writeFileSync(outputPath, spec, "utf-8")
53
- },
54
- catch: (e) => new Error(`Fetch/save failed: ${e}`)
93
+ const fetchAndSaveSpec = (
94
+ packageDir: string,
95
+ url: string,
96
+ outputPath: string
97
+ ): Effect.Effect<
98
+ void,
99
+ RegenerateError,
100
+ ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | HttpClient.HttpClient
101
+ > =>
102
+ Effect.gen(function*() {
103
+ const fs = yield* FileSystem.FileSystem
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)
55
110
  })
56
111
 
57
- const readVersion = (file: string): Effect.Effect<string | null> =>
58
- Effect.sync(() => {
59
- try {
60
- return fs.readFileSync(file, "utf-8").trim()
61
- } catch {
62
- return null
63
- }
112
+ const readVersion = (file: string): Effect.Effect<string | null, RegenerateError, FileSystem.FileSystem> =>
113
+ Effect.gen(function*() {
114
+ const fs = yield* FileSystem.FileSystem
115
+ const exists = yield* fs.exists(file).pipe(
116
+ Effect.mapError((cause) => new RegenerateError({ message: "Version lookup failed", cause }))
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
+ )
64
124
  })
65
125
 
66
- const writeVersion = (file: string, version: string): Effect.Effect<void> =>
67
- Effect.sync(() => fs.writeFileSync(file, version, "utf-8"))
126
+ const writeVersion = (file: string, version: string): Effect.Effect<void, RegenerateError, FileSystem.FileSystem> =>
127
+ Effect.gen(function*() {
128
+ const fs = yield* FileSystem.FileSystem
129
+ yield* fs.writeFileString(file, version).pipe(
130
+ Effect.mapError((cause) => new RegenerateError({ message: "Version write failed", cause }))
131
+ )
132
+ })
133
+
134
+ const ensureDir = (dir: string): Effect.Effect<void, RegenerateError, FileSystem.FileSystem> =>
135
+ Effect.gen(function*() {
136
+ const fs = yield* FileSystem.FileSystem
137
+ yield* fs.makeDirectory(dir, { recursive: true }).pipe(
138
+ Effect.mapError((cause) => new RegenerateError({ message: "Directory creation failed", cause }))
139
+ )
140
+ })
68
141
 
69
- const ensureDir = (dir: string): Effect.Effect<void> =>
70
- Effect.sync(() => {
71
- if (!fs.existsSync(dir)) {
72
- fs.mkdirSync(dir, { recursive: true })
142
+ const generateTypes = (
143
+ packageDir: string,
144
+ generatedDir: string,
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
+ )
159
+
160
+ if (exitCode !== 0) {
161
+ return yield* Effect.fail(new RegenerateError({ message: "Type generation failed", cause: { exitCode } }))
73
162
  }
74
163
  })
75
164
 
76
- const generateTypes = (specPath: string): Effect.Effect<void, Error> =>
77
- Effect.tryPromise({
78
- try: async () => {
79
- const outputFile = path.join(GENERATED_DIR, "schema.d.ts")
80
- execSync(
81
- `pnpm exec openapi-typescript "${specPath}" -o "${outputFile}"`,
82
- { cwd: path.join(__dirname, "..") }
83
- )
84
- },
85
- catch: (e) => new Error(`Type generation failed: ${e}`)
165
+ const commandExitCode = (
166
+ command: ChildProcess.Command
167
+ ): Effect.Effect<ChildProcessSpawner.ExitCode, PlatformError.PlatformError, ChildProcessSpawner.ChildProcessSpawner> =>
168
+ ChildProcessSpawner.ChildProcessSpawner.pipe(
169
+ Effect.flatMap((spawner) => spawner.exitCode(command))
170
+ )
171
+
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
+ }
86
190
  })
87
191
 
88
192
  const checkOnly = Options.boolean("check").pipe(
@@ -92,10 +196,13 @@ const checkOnly = Options.boolean("check").pipe(
92
196
 
93
197
  const regenerate = Command.make("regenerate", { checkOnly }, ({ checkOnly }) =>
94
198
  Effect.gen(function*() {
199
+ const path = yield* Path.Path
200
+ const paths = yield* scriptPaths
201
+
95
202
  yield* Console.log("Fetching Jira API spec version...")
96
203
 
97
204
  const v3Info = yield* fetchSpecInfo(SPEC_URLS.v3)
98
- const currentV3 = yield* readVersion(VERSION_V3_FILE)
205
+ const currentV3 = yield* readVersion(paths.versionV3File)
99
206
 
100
207
  yield* Console.log(`V3: current=${currentV3 ?? "none"}, remote=${v3Info.version}`)
101
208
 
@@ -109,23 +216,23 @@ const regenerate = Command.make("regenerate", { checkOnly }, ({ checkOnly }) =>
109
216
  if (checkOnly) {
110
217
  yield* Console.log("Spec is outdated!")
111
218
  yield* Console.log(` V3: ${currentV3} -> ${v3Info.version}`)
112
- return yield* Effect.fail(new Error("Spec outdated"))
219
+ return yield* Effect.fail(new RegenerateError({ message: "Spec outdated" }))
113
220
  }
114
221
 
115
- yield* ensureDir(SPECS_DIR)
116
- yield* ensureDir(GENERATED_DIR)
222
+ yield* ensureDir(paths.specsDir)
223
+ yield* ensureDir(paths.generatedDir)
117
224
 
118
- const specPath = path.join(SPECS_DIR, `jira-v3-${v3Info.version}.json`)
225
+ const specPath = path.join(paths.specsDir, `jira-v3-${v3Info.version}.json`)
119
226
 
120
227
  yield* Console.log(`Fetching V3 spec (${v3Info.version})...`)
121
- yield* fetchAndSaveSpec(SPEC_URLS.v3, specPath)
228
+ yield* fetchAndSaveSpec(paths.packageDir, SPEC_URLS.v3, specPath)
122
229
  yield* Console.log(`Saved: ${specPath}`)
123
230
 
124
231
  yield* Console.log("Generating types...")
125
- yield* generateTypes(specPath)
232
+ yield* generateTypes(paths.packageDir, paths.generatedDir, specPath)
126
233
  yield* Console.log(`Generated: src/generated/v3/schema.d.ts`)
127
234
 
128
- yield* writeVersion(VERSION_V3_FILE, v3Info.version)
235
+ yield* writeVersion(paths.versionV3File, v3Info.version)
129
236
  yield* Console.log("Done!")
130
237
  })
131
238
  ).pipe(Command.withDescription("Regenerate Jira API types from OpenAPI specs"))
@@ -135,7 +242,8 @@ const cli = Command.run(regenerate, {
135
242
  version: pkg.version
136
243
  })
137
244
 
138
- Effect.suspend(() => cli(process.argv)).pipe(
139
- Effect.provide(NodeContext.layer),
245
+ cli.pipe(
246
+ Effect.provide(NodeHttpClient.layerFetch),
247
+ Effect.provide(NodeServices.layer),
140
248
  NodeRuntime.runMain
141
249
  )
@@ -85,9 +85,9 @@ export interface JiraApiClientShape {
85
85
  *
86
86
  * @category Client
87
87
  */
88
- export class JiraApiClient extends Context.Tag(
88
+ export class JiraApiClient extends Context.Service<JiraApiClient, JiraApiClientShape>()(
89
89
  "@knpkv/jira-api-client/JiraApiClient"
90
- )<JiraApiClient, JiraApiClientShape>() {
90
+ ) {
91
91
  /**
92
92
  * Layer that provides JiraApiClient.
93
93
  *
@@ -46,6 +46,6 @@ export interface JiraApiConfigShape {
46
46
  *
47
47
  * @category Config
48
48
  */
49
- export class JiraApiConfig extends Context.Tag(
49
+ export class JiraApiConfig extends Context.Service<JiraApiConfig, JiraApiConfigShape>()(
50
50
  "@knpkv/jira-api-client/JiraApiConfig"
51
- )<JiraApiConfig, JiraApiConfigShape>() {}
51
+ ) {}