@knpkv/clockify-api-client 0.3.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 +29 -0
- package/README.md +92 -80
- package/dist/ClockifyApiClient.d.ts +44 -45
- package/dist/ClockifyApiClient.d.ts.map +1 -1
- package/dist/ClockifyApiClient.js +52 -121
- package/dist/ClockifyApiClient.js.map +1 -1
- package/dist/generated/ClockifyApi.d.ts +14432 -0
- package/dist/generated/ClockifyApi.d.ts.map +1 -0
- package/dist/generated/ClockifyApi.js +1239 -0
- package/dist/generated/ClockifyApi.js.map +1 -0
- package/dist/index.d.ts +4 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/package.json +15 -21
- package/scripts/regenerate.ts +182 -201
- package/scripts/tsconfig.json +13 -0
- package/src/ClockifyApiClient.ts +125 -226
- package/src/generated/ClockifyApi.ts +3020 -0
- package/src/index.ts +10 -14
- package/test/ClockifyApiClient.test.ts +195 -0
- package/vitest.config.ts +7 -0
- package/.specs/VERSION +0 -1
- package/.specs/clockify-v1.json +0 -31670
- package/.specs/clockify-v1.patch.json +0 -48
- package/dist/ClockifyApiError.d.ts +0 -11
- package/dist/ClockifyApiError.d.ts.map +0 -1
- package/dist/ClockifyApiError.js +0 -14
- package/dist/ClockifyApiError.js.map +0 -1
- 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/index.d.ts +0 -2
- package/dist/generated/index.d.ts.map +0 -1
- package/dist/generated/index.js +0 -2
- package/dist/generated/index.js.map +0 -1
- package/dist/generated/schema.d.ts +0 -16204
- package/src/ClockifyApiError.ts +0 -17
- package/src/OpenApiFetchClient.ts +0 -92
- package/src/generated/index.ts +0 -1
- package/src/generated/schema.d.ts +0 -16204
- package/tsconfig.tsbuildinfo +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Root barrel export for
|
|
2
|
+
* Root barrel export for the Schema-validated Effect Clockify client.
|
|
3
3
|
*
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
|
-
export { ClockifyApiClient, type ClockifyApiClientShape, layer } from "./ClockifyApiClient.js";
|
|
7
|
-
export type { CreateTimeEntryParams, GetTimeEntriesParams, Project, StopTimeEntryParams, Tag, TimeEntry, TimeInterval, UpdateTimeEntryParams, User, Workspace } from "./ClockifyApiClient.js";
|
|
6
|
+
export { ClockifyApiClient, type ClockifyApiClientShape, type ClockifyClientError, layer, make } from "./ClockifyApiClient.js";
|
|
7
|
+
export type { AuthenticatedClockifyApi, CreateTimeEntryParams, GetTimeEntriesParams, Project, StopTimeEntryParams, Tag, TimeEntry, TimeInterval, UpdateTimeEntryParams, User, Workspace } from "./ClockifyApiClient.js";
|
|
8
8
|
export { ClockifyApiConfig, type ClockifyApiConfigShape } from "./ClockifyApiConfig.js";
|
|
9
|
-
export
|
|
10
|
-
export { FetchClientError, makeOpenApiFetchClient, type OpenApiFetchClient, type SuccessData, toEffect } from "./OpenApiFetchClient.js";
|
|
11
|
-
export type * as V1 from "./generated/index.js";
|
|
9
|
+
export * as ClockifyApi from "./generated/ClockifyApi.js";
|
|
12
10
|
//# 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;;;;GAIG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,iBAAiB,EACjB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,KAAK,EACL,IAAI,EACL,MAAM,wBAAwB,CAAA;AAE/B,YAAY,EACV,wBAAwB,EACxB,qBAAqB,EACrB,oBAAoB,EACpB,OAAO,EACP,mBAAmB,EACnB,GAAG,EACH,SAAS,EACT,YAAY,EACZ,qBAAqB,EACrB,IAAI,EACJ,SAAS,EACV,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAEvF,OAAO,KAAK,WAAW,MAAM,4BAA4B,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Root barrel export for
|
|
2
|
+
* Root barrel export for the Schema-validated Effect Clockify client.
|
|
3
3
|
*
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
|
-
export { ClockifyApiClient, layer } from "./ClockifyApiClient.js";
|
|
6
|
+
export { ClockifyApiClient, layer, make } from "./ClockifyApiClient.js";
|
|
7
7
|
export { ClockifyApiConfig } from "./ClockifyApiConfig.js";
|
|
8
|
-
export
|
|
9
|
-
export { FetchClientError, makeOpenApiFetchClient, toEffect } from "./OpenApiFetchClient.js";
|
|
8
|
+
export * as ClockifyApi from "./generated/ClockifyApi.js";
|
|
10
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;;;;GAIG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,iBAAiB,EAGjB,KAAK,EACL,IAAI,EACL,MAAM,wBAAwB,CAAA;AAgB/B,OAAO,EAAE,iBAAiB,EAA+B,MAAM,wBAAwB,CAAA;AAEvF,OAAO,KAAK,WAAW,MAAM,4BAA4B,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knpkv/clockify-api-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Effect-based Clockify REST API client",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "knpkv",
|
|
@@ -26,34 +26,27 @@
|
|
|
26
26
|
"import": "./dist/ClockifyApiConfig.js",
|
|
27
27
|
"types": "./dist/ClockifyApiConfig.d.ts"
|
|
28
28
|
},
|
|
29
|
-
"./ClockifyApiError": {
|
|
30
|
-
"import": "./dist/ClockifyApiError.js",
|
|
31
|
-
"types": "./dist/ClockifyApiError.d.ts"
|
|
32
|
-
},
|
|
33
29
|
"./generated": {
|
|
34
|
-
"import": "./dist/generated/
|
|
35
|
-
"types": "./dist/generated/
|
|
30
|
+
"import": "./dist/generated/ClockifyApi.js",
|
|
31
|
+
"types": "./dist/generated/ClockifyApi.d.ts"
|
|
36
32
|
}
|
|
37
33
|
},
|
|
38
34
|
"publishConfig": {
|
|
39
35
|
"access": "public"
|
|
40
36
|
},
|
|
41
37
|
"peerDependencies": {
|
|
42
|
-
"effect": "4.0.0-beta.
|
|
43
|
-
},
|
|
44
|
-
"dependencies": {
|
|
45
|
-
"openapi-fetch": "^0.17.0"
|
|
38
|
+
"effect": "4.0.0-beta.98"
|
|
46
39
|
},
|
|
47
40
|
"devDependencies": {
|
|
48
|
-
"@effect/
|
|
49
|
-
"@effect/
|
|
41
|
+
"@effect/openapi-generator": "4.0.0-beta.98",
|
|
42
|
+
"@effect/platform-node": "4.0.0-beta.98",
|
|
43
|
+
"@effect/vitest": "4.0.0-beta.98",
|
|
50
44
|
"@types/node": "latest",
|
|
51
|
-
"effect": "4.0.0-beta.
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"tsx": "^4.22.4",
|
|
45
|
+
"effect": "4.0.0-beta.98",
|
|
46
|
+
"prettier": "^3.9.5",
|
|
47
|
+
"tsx": "^4.23.0",
|
|
55
48
|
"typescript": "~6.0.3",
|
|
56
|
-
"vitest": "^4.1.
|
|
49
|
+
"vitest": "^4.1.10"
|
|
57
50
|
},
|
|
58
51
|
"keywords": [
|
|
59
52
|
"effect",
|
|
@@ -64,11 +57,12 @@
|
|
|
64
57
|
"openapi"
|
|
65
58
|
],
|
|
66
59
|
"scripts": {
|
|
67
|
-
"build": "tsc
|
|
68
|
-
"check": "tsc --noEmit",
|
|
60
|
+
"build": "tsc",
|
|
61
|
+
"check": "tsc --noEmit && tsc -p scripts/tsconfig.json",
|
|
69
62
|
"test": "vitest run",
|
|
70
63
|
"test:watch": "vitest",
|
|
71
64
|
"lint": "eslint src",
|
|
72
|
-
"regenerate": "tsx scripts/regenerate.ts"
|
|
65
|
+
"regenerate": "tsx scripts/regenerate.ts",
|
|
66
|
+
"regenerate:check": "tsx scripts/regenerate.ts --check"
|
|
73
67
|
}
|
|
74
68
|
}
|
package/scripts/regenerate.ts
CHANGED
|
@@ -1,249 +1,230 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Fetch, compare, patch, and regenerate the Clockify API client.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 3. Generates TypeScript types via openapi-typescript
|
|
5
|
+
* The committed OpenAPI document is the unmodified upstream document. RFC 6902
|
|
6
|
+
* patches are applied in memory before Effect's OpenAPI generator runs.
|
|
8
7
|
*/
|
|
9
|
-
import
|
|
10
|
-
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"
|
|
11
11
|
import * as Console from "effect/Console"
|
|
12
|
+
import * as Data from "effect/Data"
|
|
12
13
|
import * as Effect from "effect/Effect"
|
|
13
14
|
import * as FileSystem from "effect/FileSystem"
|
|
15
|
+
import type * as JsonSchema from "effect/JsonSchema"
|
|
16
|
+
import * as Layer from "effect/Layer"
|
|
14
17
|
import * as Path from "effect/Path"
|
|
15
|
-
import * as
|
|
16
|
-
import
|
|
17
|
-
import * as
|
|
18
|
-
import * as
|
|
18
|
+
import * as Predicate from "effect/Predicate"
|
|
19
|
+
import * as Schema from "effect/Schema"
|
|
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"
|
|
19
25
|
|
|
20
26
|
const SPEC_URL = "https://docs.clockify.me/openapi.json"
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
28
|
+
class RegenerateError extends Data.TaggedError("RegenerateError")<{
|
|
29
|
+
readonly message: string
|
|
30
|
+
readonly cause?: unknown
|
|
31
|
+
}> {}
|
|
32
|
+
|
|
33
|
+
const JsonString = Schema.fromJsonString(Schema.Json)
|
|
34
|
+
|
|
35
|
+
const OpenApiDocument = Schema.declare<OpenAPISpec>(
|
|
36
|
+
(value): value is OpenAPISpec =>
|
|
37
|
+
Predicate.hasProperty(value, "paths") &&
|
|
38
|
+
Predicate.isObject(value.paths) &&
|
|
39
|
+
(Predicate.hasProperty(value, "openapi") || Predicate.hasProperty(value, "swagger")),
|
|
40
|
+
{ identifier: "OpenApiDocument" }
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const SpecInfo = Schema.Struct({
|
|
44
|
+
info: Schema.Struct({ version: Schema.String })
|
|
45
|
+
})
|
|
26
46
|
|
|
27
47
|
interface ScriptPaths {
|
|
28
48
|
readonly packageDir: string
|
|
29
|
-
readonly specsDir: string
|
|
30
49
|
readonly specFile: string
|
|
31
50
|
readonly patchFile: string
|
|
32
51
|
readonly outputFile: string
|
|
33
52
|
readonly versionFile: string
|
|
34
53
|
}
|
|
35
54
|
|
|
36
|
-
|
|
37
|
-
properties?: Record<string, unknown>
|
|
38
|
-
required?: unknown
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface ClockifySpec {
|
|
42
|
-
components?: {
|
|
43
|
-
schemas?: Record<string, ClockifySchema>
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface ClockifySchemaPatch {
|
|
48
|
-
readonly renameProperties?: Record<string, string>
|
|
49
|
-
readonly addProperties?: Record<string, unknown>
|
|
50
|
-
readonly patchProperties?: Record<string, unknown>
|
|
51
|
-
readonly required?: unknown
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface ClockifyPatches {
|
|
55
|
-
readonly schemas?: Record<string, ClockifySchemaPatch>
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const fetchText = (url: string): Effect.Effect<string, Error, HttpClient.HttpClient> =>
|
|
59
|
-
HttpClient.get(url).pipe(
|
|
60
|
-
Effect.flatMap((response) =>
|
|
61
|
-
response.status >= 200 && response.status < 300
|
|
62
|
-
? response.text
|
|
63
|
-
: Effect.fail(new Error(`Failed to fetch ${url}: ${response.status}`))
|
|
64
|
-
),
|
|
65
|
-
Effect.mapError((e) => e instanceof Error ? e : new Error(`Fetch failed: ${e}`))
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
const fetchJson = <A>(url: string): Effect.Effect<A, Error, HttpClient.HttpClient> =>
|
|
69
|
-
fetchText(url).pipe(
|
|
70
|
-
Effect.flatMap((text) =>
|
|
71
|
-
Effect.try({
|
|
72
|
-
try: () => JSON.parse(text) as A,
|
|
73
|
-
catch: (e) => new Error(`JSON parse failed: ${e}`)
|
|
74
|
-
})
|
|
75
|
-
)
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
const scriptPaths: Effect.Effect<ScriptPaths, Error, Path.Path> = Effect.gen(function*() {
|
|
55
|
+
const paths = Effect.gen(function*() {
|
|
79
56
|
const path = yield* Path.Path
|
|
80
57
|
const scriptFile = yield* path.fromFileUrl(new URL(import.meta.url)).pipe(
|
|
81
|
-
Effect.mapError((
|
|
58
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not resolve script path", cause }))
|
|
82
59
|
)
|
|
83
|
-
const
|
|
84
|
-
const packageDir = path.join(scriptDir, "..")
|
|
60
|
+
const packageDir = path.join(path.dirname(scriptFile), "..")
|
|
85
61
|
const specsDir = path.join(packageDir, ".specs")
|
|
86
|
-
|
|
87
62
|
return {
|
|
88
63
|
packageDir,
|
|
89
|
-
specsDir,
|
|
90
64
|
specFile: path.join(specsDir, "clockify-v1.json"),
|
|
91
65
|
patchFile: path.join(specsDir, "clockify-v1.patch.json"),
|
|
92
|
-
outputFile: path.join(packageDir, "src", "generated", "
|
|
66
|
+
outputFile: path.join(packageDir, "src", "generated", "ClockifyApi.ts"),
|
|
93
67
|
versionFile: path.join(specsDir, "VERSION")
|
|
94
|
-
}
|
|
68
|
+
} satisfies ScriptPaths
|
|
95
69
|
})
|
|
96
70
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
71
|
+
const fetchUpstream = HttpClient.get(SPEC_URL).pipe(
|
|
72
|
+
Effect.flatMap((response) =>
|
|
73
|
+
response.status >= 200 && response.status < 300
|
|
74
|
+
? response.text.pipe(
|
|
75
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not read Clockify response", cause }))
|
|
76
|
+
)
|
|
77
|
+
: Effect.fail(
|
|
78
|
+
new RegenerateError({
|
|
79
|
+
message: `Clockify spec request failed with status ${response.status}`
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
),
|
|
83
|
+
Effect.flatMap(Schema.decodeUnknownEffect(JsonString)),
|
|
84
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not fetch or decode Clockify spec", cause }))
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const canonicalJson = (value: Schema.Json) =>
|
|
88
|
+
Schema.encodeEffect(JsonString)(value).pipe(
|
|
89
|
+
Effect.map((json) => `${json}\n`),
|
|
90
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not encode Clockify spec", cause }))
|
|
100
91
|
)
|
|
101
92
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
93
|
+
const formattedJson = (value: Schema.Json) =>
|
|
94
|
+
Schema.encodeEffect(JsonString)(value).pipe(
|
|
95
|
+
Effect.flatMap((source) =>
|
|
96
|
+
Effect.tryPromise({
|
|
97
|
+
try: () => formatSource(source, { parser: "json", printWidth: 120, trailingComma: "none", semi: false }),
|
|
98
|
+
catch: (cause) => new RegenerateError({ message: "Could not format Clockify spec", cause })
|
|
99
|
+
})
|
|
100
|
+
),
|
|
101
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not format Clockify spec", cause }))
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const stripUnreliableMetadata = (schema: JsonSchema.JsonSchema): JsonSchema.JsonSchema => {
|
|
105
|
+
const { default: _default, examples: _examples, ...normalized } = schema
|
|
106
|
+
return normalized
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const stripTrailingWhitespace = (source: string): string => source.replace(/[ \t]+$/gm, "")
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
112
|
+
* The pinned Effect generator emits an unused decoder when a spec declares no typed error bodies.
|
|
113
|
+
* Remove only that exact unused block; fail if the generator shape changes.
|
|
114
114
|
*/
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
fs.readFileString(specFile),
|
|
125
|
-
fs.readFileString(patchFile)
|
|
126
|
-
]).pipe(
|
|
127
|
-
Effect.mapError((e) => new Error(`Patch read failed: ${e.message}`))
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
const { patches, spec } = yield* Effect.try({
|
|
131
|
-
try: () => ({
|
|
132
|
-
spec: JSON.parse(specText) as ClockifySpec,
|
|
133
|
-
patches: JSON.parse(patchText) as ClockifyPatches
|
|
134
|
-
}),
|
|
135
|
-
catch: (e) => new Error(`Patch parse failed: ${e}`)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
yield* Effect.try({
|
|
139
|
-
try: () => {
|
|
140
|
-
const schemas = spec.components?.schemas ?? {}
|
|
141
|
-
|
|
142
|
-
for (const [schemaName, patch] of Object.entries(patches.schemas ?? {})) {
|
|
143
|
-
const schema = schemas[schemaName]
|
|
144
|
-
if (!schema) continue
|
|
145
|
-
|
|
146
|
-
// Rename properties (e.g. get_id -> id)
|
|
147
|
-
if (patch.renameProperties) {
|
|
148
|
-
for (const [from, to] of Object.entries(patch.renameProperties)) {
|
|
149
|
-
if (schema.properties?.[from]) {
|
|
150
|
-
schema.properties[to] = schema.properties[from]
|
|
151
|
-
delete schema.properties[from]
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Add new properties
|
|
157
|
-
if (patch.addProperties) {
|
|
158
|
-
schema.properties = { ...schema.properties, ...patch.addProperties }
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Patch existing properties (overwrite)
|
|
162
|
-
if (patch.patchProperties) {
|
|
163
|
-
schema.properties = { ...schema.properties }
|
|
164
|
-
for (const [name, value] of Object.entries(patch.patchProperties)) {
|
|
165
|
-
schema.properties[name] = value
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Set required fields
|
|
170
|
-
if (patch.required) {
|
|
171
|
-
schema.required = patch.required
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
catch: (e) => new Error(`Patch failed: ${e}`)
|
|
176
|
-
})
|
|
115
|
+
const removeUnusedErrorDecoder = (source: string): string => {
|
|
116
|
+
const marker = " const decodeError ="
|
|
117
|
+
if (!source.includes(marker)) return source
|
|
118
|
+
const cleaned = source.replace(/\n {2}const decodeError =[\s\S]*?\n {2}return \{/m, "\n return {")
|
|
119
|
+
if (cleaned === source || cleaned.includes(marker)) {
|
|
120
|
+
throw new RegenerateError({ message: "Effect generator output changed around decodeError" })
|
|
121
|
+
}
|
|
122
|
+
return cleaned
|
|
123
|
+
}
|
|
177
124
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
125
|
+
/**
|
|
126
|
+
* The generator currently passes record-shaped multipart payloads to
|
|
127
|
+
* `bodyFormData`, which expects an already-built Web FormData value. Use the
|
|
128
|
+
* record-aware Effect helper so strings and Blob/File values are serialized
|
|
129
|
+
* with a real multipart boundary by the runtime.
|
|
130
|
+
*/
|
|
131
|
+
const normalizeMultipartBodies = (source: string): string =>
|
|
132
|
+
source.replaceAll("HttpClientRequest.bodyFormData(", "HttpClientRequest.bodyFormDataRecord(")
|
|
182
133
|
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
134
|
+
const generate = Effect.fn("Clockify.regenerate.generate")(function*(
|
|
135
|
+
scriptPaths: ScriptPaths,
|
|
136
|
+
upstream: Schema.Json
|
|
137
|
+
) {
|
|
138
|
+
const fs = yield* FileSystem.FileSystem
|
|
139
|
+
const patch = yield* OpenApiPatch.parsePatchInput(scriptPaths.patchFile).pipe(
|
|
140
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not parse Clockify JSON patch", cause }))
|
|
188
141
|
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
142
|
+
const patched = yield* OpenApiPatch.applyPatches(
|
|
143
|
+
[{ source: scriptPaths.patchFile, patch }],
|
|
144
|
+
upstream
|
|
145
|
+
).pipe(
|
|
146
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not apply Clockify JSON patch", cause }))
|
|
147
|
+
)
|
|
148
|
+
const document = yield* Schema.decodeUnknownEffect(OpenApiDocument)(patched).pipe(
|
|
149
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Patched Clockify document is not OpenAPI", cause }))
|
|
150
|
+
)
|
|
151
|
+
const generator = yield* OpenApiGenerator.OpenApiGenerator
|
|
152
|
+
const warnings: Array<OpenApiGenerator.OpenApiGeneratorWarning> = []
|
|
153
|
+
const generated = yield* generator.generate(document, {
|
|
154
|
+
name: "ClockifyApi",
|
|
155
|
+
format: "httpclient",
|
|
156
|
+
onEnter: stripUnreliableMetadata,
|
|
157
|
+
onWarning: (warning) => warnings.push(warning)
|
|
205
158
|
})
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
yield* Console.log("Fetching Clockify API spec version...")
|
|
212
|
-
|
|
213
|
-
const info = yield* fetchSpecInfo
|
|
214
|
-
const current = yield* fs.exists(paths.versionFile).pipe(
|
|
215
|
-
Effect.flatMap((exists) => exists ? fs.readFileString(paths.versionFile) : Effect.succeed(null)),
|
|
216
|
-
Effect.map((content) => content?.trim() ?? null),
|
|
217
|
-
Effect.mapError((e) => new Error(`Version read failed: ${e.message}`))
|
|
159
|
+
yield* Effect.forEach(
|
|
160
|
+
warnings,
|
|
161
|
+
(warning) => Console.warn(`[${warning.code}] ${warning.method ?? ""} ${warning.path ?? ""}: ${warning.message}`),
|
|
162
|
+
{ discard: true }
|
|
218
163
|
)
|
|
164
|
+
yield* fs.writeFileString(
|
|
165
|
+
scriptPaths.outputFile,
|
|
166
|
+
stripTrailingWhitespace(normalizeMultipartBodies(removeUnusedErrorDecoder(generated)))
|
|
167
|
+
).pipe(
|
|
168
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not write generated Clockify client", cause }))
|
|
169
|
+
)
|
|
170
|
+
})
|
|
219
171
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
yield* Console.log("Spec up to date, regenerating types...")
|
|
224
|
-
} else {
|
|
225
|
-
yield* fs.makeDirectory(paths.specsDir, { recursive: true }).pipe(
|
|
226
|
-
Effect.mapError((e) => new Error(`Spec directory creation failed: ${e.message}`))
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
yield* Console.log(`Fetching spec (${info.version})...`)
|
|
230
|
-
yield* fetchAndSaveSpec(paths.specFile)
|
|
231
|
-
yield* fs.writeFileString(paths.versionFile, info.version).pipe(
|
|
232
|
-
Effect.mapError((e) => new Error(`Version write failed: ${e.message}`))
|
|
233
|
-
)
|
|
234
|
-
}
|
|
172
|
+
const check = Flag.boolean("check").pipe(
|
|
173
|
+
Flag.withDescription("Exit non-zero when the committed upstream spec differs from Clockify")
|
|
174
|
+
)
|
|
235
175
|
|
|
236
|
-
|
|
237
|
-
|
|
176
|
+
const local = Flag.boolean("local").pipe(
|
|
177
|
+
Flag.withDescription("Regenerate from the committed spec without contacting Clockify")
|
|
178
|
+
)
|
|
238
179
|
|
|
239
|
-
|
|
240
|
-
|
|
180
|
+
const root = Command.make("clockify-api-regenerate", { check, local }).pipe(
|
|
181
|
+
Command.withHandler(
|
|
182
|
+
Effect.fn("Clockify.regenerate")(function*({ check, local }) {
|
|
183
|
+
const fs = yield* FileSystem.FileSystem
|
|
184
|
+
const scriptPaths = yield* paths
|
|
185
|
+
const upstream = local
|
|
186
|
+
? yield* fs.readFileString(scriptPaths.specFile).pipe(
|
|
187
|
+
Effect.flatMap(Schema.decodeUnknownEffect(JsonString)),
|
|
188
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not read committed Clockify spec", cause }))
|
|
189
|
+
)
|
|
190
|
+
: yield* fetchUpstream
|
|
191
|
+
const encoded = yield* canonicalJson(upstream)
|
|
192
|
+
const current = yield* fs.readFileString(scriptPaths.specFile).pipe(
|
|
193
|
+
Effect.flatMap(Schema.decodeUnknownEffect(JsonString)),
|
|
194
|
+
Effect.flatMap(canonicalJson),
|
|
195
|
+
Effect.orElseSucceed(() => "")
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if (check) {
|
|
199
|
+
if (local) {
|
|
200
|
+
return yield* new RegenerateError({ message: "--check and --local cannot be combined" })
|
|
201
|
+
}
|
|
202
|
+
if (current === encoded) {
|
|
203
|
+
return yield* Console.log("Clockify OpenAPI spec is current.")
|
|
204
|
+
}
|
|
205
|
+
return yield* new RegenerateError({
|
|
206
|
+
message: "Clockify OpenAPI spec changed; run `pnpm --filter @knpkv/clockify-api-client regenerate`"
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const { info } = yield* Schema.decodeUnknownEffect(SpecInfo)(upstream).pipe(
|
|
211
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Clockify spec has no version", cause }))
|
|
212
|
+
)
|
|
213
|
+
yield* fs.writeFileString(scriptPaths.specFile, yield* formattedJson(upstream))
|
|
214
|
+
yield* fs.writeFileString(scriptPaths.versionFile, `${info.version}\n`)
|
|
215
|
+
yield* generate(scriptPaths, upstream)
|
|
216
|
+
yield* Console.log(`Generated Clockify ${info.version} client from ${SPEC_URL}`)
|
|
217
|
+
})
|
|
218
|
+
)
|
|
219
|
+
)
|
|
241
220
|
|
|
242
|
-
|
|
243
|
-
|
|
221
|
+
const MainLayer = Layer.mergeAll(
|
|
222
|
+
OpenApiGenerator.layerTransformerSchema,
|
|
223
|
+
NodeHttpClient.layerFetch,
|
|
224
|
+
NodeServices.layer
|
|
225
|
+
)
|
|
244
226
|
|
|
245
|
-
|
|
246
|
-
Effect.provide(
|
|
247
|
-
Effect.provide(NodeServices.layer),
|
|
227
|
+
Command.run(root, { version: "1.0.0" }).pipe(
|
|
228
|
+
Effect.provide(MainLayer),
|
|
248
229
|
NodeRuntime.runMain
|
|
249
230
|
)
|
|
@@ -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
|
+
}
|