@knpkv/clockify-api-client 0.2.0 → 1.0.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/CHANGELOG.md +29 -0
- package/README.md +92 -80
- package/dist/ClockifyApiClient.d.ts +45 -46
- package/dist/ClockifyApiClient.d.ts.map +1 -1
- package/dist/ClockifyApiClient.js +53 -122
- package/dist/ClockifyApiClient.js.map +1 -1
- package/dist/ClockifyApiConfig.d.ts +1 -1
- package/dist/ClockifyApiConfig.d.ts.map +1 -1
- package/dist/ClockifyApiConfig.js +1 -1
- package/dist/ClockifyApiConfig.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 +16 -21
- package/scripts/regenerate.ts +208 -116
- package/scripts/tsconfig.json +13 -0
- package/src/ClockifyApiClient.ts +127 -228
- package/src/ClockifyApiConfig.ts +2 -2
- package/src/generated/ClockifyApi.ts +3020 -0
- package/src/index.ts +10 -14
- package/test/ClockifyApiClient.test.ts +129 -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.0",
|
|
4
4
|
"description": "Effect-based Clockify REST API client",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "knpkv",
|
|
@@ -26,33 +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": "
|
|
43
|
-
},
|
|
44
|
-
"dependencies": {
|
|
45
|
-
"openapi-fetch": "^0.17.0"
|
|
38
|
+
"effect": "4.0.0-beta.98"
|
|
46
39
|
},
|
|
47
40
|
"devDependencies": {
|
|
48
|
-
"@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",
|
|
49
44
|
"@types/node": "latest",
|
|
50
|
-
"effect": "
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"vitest": "^4.0.13"
|
|
45
|
+
"effect": "4.0.0-beta.98",
|
|
46
|
+
"prettier": "^3.9.5",
|
|
47
|
+
"tsx": "^4.23.0",
|
|
48
|
+
"typescript": "~6.0.3",
|
|
49
|
+
"vitest": "^4.1.10"
|
|
56
50
|
},
|
|
57
51
|
"keywords": [
|
|
58
52
|
"effect",
|
|
@@ -63,11 +57,12 @@
|
|
|
63
57
|
"openapi"
|
|
64
58
|
],
|
|
65
59
|
"scripts": {
|
|
66
|
-
"build": "tsc
|
|
67
|
-
"check": "tsc --noEmit",
|
|
60
|
+
"build": "tsc",
|
|
61
|
+
"check": "tsc --noEmit && tsc -p scripts/tsconfig.json",
|
|
68
62
|
"test": "vitest run",
|
|
69
63
|
"test:watch": "vitest",
|
|
70
64
|
"lint": "eslint src",
|
|
71
|
-
"regenerate": "tsx scripts/regenerate.ts"
|
|
65
|
+
"regenerate": "tsx scripts/regenerate.ts",
|
|
66
|
+
"regenerate:check": "tsx scripts/regenerate.ts --check"
|
|
72
67
|
}
|
|
73
68
|
}
|
package/scripts/regenerate.ts
CHANGED
|
@@ -1,138 +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
|
*/
|
|
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"
|
|
12
|
+
import * as Data from "effect/Data"
|
|
10
13
|
import * as Effect from "effect/Effect"
|
|
11
|
-
import
|
|
12
|
-
import * as
|
|
13
|
-
import * as
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
import * as FileSystem from "effect/FileSystem"
|
|
15
|
+
import type * as JsonSchema from "effect/JsonSchema"
|
|
16
|
+
import * as Layer from "effect/Layer"
|
|
17
|
+
import * as Path from "effect/Path"
|
|
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"
|
|
25
|
+
|
|
22
26
|
const SPEC_URL = "https://docs.clockify.me/openapi.json"
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
+
class RegenerateError extends Data.TaggedError("RegenerateError")<{
|
|
29
|
+
readonly message: string
|
|
30
|
+
readonly cause?: unknown
|
|
31
|
+
}> {}
|
|
28
32
|
|
|
29
|
-
const
|
|
30
|
-
Effect.tryPromise({
|
|
31
|
-
try: async () => {
|
|
32
|
-
const response = await fetch(SPEC_URL)
|
|
33
|
-
if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`)
|
|
34
|
-
const spec = (await response.json()) as { info: { version: string; title: string } }
|
|
35
|
-
return { version: spec.info.version, title: spec.info.title }
|
|
36
|
-
},
|
|
37
|
-
catch: (e) => new Error(`Fetch failed: ${e}`)
|
|
38
|
-
})
|
|
33
|
+
const JsonString = Schema.fromJsonString(Schema.Json)
|
|
39
34
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
catch: (e) => new Error(`Fetch/save failed: ${e}`)
|
|
48
|
-
})
|
|
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
|
+
)
|
|
49
42
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
*/
|
|
54
|
-
const applyPatches = (): Effect.Effect<void, Error> =>
|
|
55
|
-
Effect.try({
|
|
56
|
-
try: () => {
|
|
57
|
-
if (!fs.existsSync(PATCH_FILE)) return
|
|
58
|
-
|
|
59
|
-
const spec = JSON.parse(fs.readFileSync(SPEC_FILE, "utf-8"))
|
|
60
|
-
const patches = JSON.parse(fs.readFileSync(PATCH_FILE, "utf-8"))
|
|
61
|
-
const schemas = spec.components?.schemas ?? {}
|
|
62
|
-
|
|
63
|
-
for (const [schemaName, patch] of Object.entries(patches.schemas ?? {}) as Array<[string, Record<string, unknown>]>) {
|
|
64
|
-
const schema = schemas[schemaName]
|
|
65
|
-
if (!schema) continue
|
|
66
|
-
|
|
67
|
-
// Rename properties (e.g. get_id → id)
|
|
68
|
-
if (patch.renameProperties) {
|
|
69
|
-
for (const [from, to] of Object.entries(patch.renameProperties as Record<string, string>)) {
|
|
70
|
-
if (schema.properties?.[from]) {
|
|
71
|
-
schema.properties[to] = schema.properties[from]
|
|
72
|
-
delete schema.properties[from]
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Add new properties
|
|
78
|
-
if (patch.addProperties) {
|
|
79
|
-
schema.properties = { ...schema.properties, ...patch.addProperties }
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Patch existing properties (overwrite)
|
|
83
|
-
if (patch.patchProperties) {
|
|
84
|
-
for (const [name, value] of Object.entries(patch.patchProperties as Record<string, unknown>)) {
|
|
85
|
-
schema.properties[name] = value
|
|
86
|
-
}
|
|
87
|
-
}
|
|
43
|
+
const SpecInfo = Schema.Struct({
|
|
44
|
+
info: Schema.Struct({ version: Schema.String })
|
|
45
|
+
})
|
|
88
46
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
47
|
+
interface ScriptPaths {
|
|
48
|
+
readonly packageDir: string
|
|
49
|
+
readonly specFile: string
|
|
50
|
+
readonly patchFile: string
|
|
51
|
+
readonly outputFile: string
|
|
52
|
+
readonly versionFile: string
|
|
53
|
+
}
|
|
94
54
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
55
|
+
const paths = Effect.gen(function*() {
|
|
56
|
+
const path = yield* Path.Path
|
|
57
|
+
const scriptFile = yield* path.fromFileUrl(new URL(import.meta.url)).pipe(
|
|
58
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not resolve script path", cause }))
|
|
59
|
+
)
|
|
60
|
+
const packageDir = path.join(path.dirname(scriptFile), "..")
|
|
61
|
+
const specsDir = path.join(packageDir, ".specs")
|
|
62
|
+
return {
|
|
63
|
+
packageDir,
|
|
64
|
+
specFile: path.join(specsDir, "clockify-v1.json"),
|
|
65
|
+
patchFile: path.join(specsDir, "clockify-v1.patch.json"),
|
|
66
|
+
outputFile: path.join(packageDir, "src", "generated", "ClockifyApi.ts"),
|
|
67
|
+
versionFile: path.join(specsDir, "VERSION")
|
|
68
|
+
} satisfies ScriptPaths
|
|
69
|
+
})
|
|
99
70
|
|
|
100
|
-
const
|
|
101
|
-
Effect.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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 }))
|
|
91
|
+
)
|
|
92
|
+
|
|
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 })
|
|
106
99
|
})
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const program = Effect.gen(function*() {
|
|
112
|
-
yield* Console.log("Fetching Clockify API spec version...")
|
|
100
|
+
),
|
|
101
|
+
Effect.mapError((cause) => new RegenerateError({ message: "Could not format Clockify spec", cause }))
|
|
102
|
+
)
|
|
113
103
|
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
104
|
+
const stripUnreliableMetadata = (schema: JsonSchema.JsonSchema): JsonSchema.JsonSchema => {
|
|
105
|
+
const { default: _default, examples: _examples, ...normalized } = schema
|
|
106
|
+
return normalized
|
|
107
|
+
}
|
|
118
108
|
|
|
119
|
-
|
|
120
|
-
yield* Console.log("Spec up to date, regenerating types...")
|
|
121
|
-
} else {
|
|
122
|
-
if (!fs.existsSync(SPECS_DIR)) fs.mkdirSync(SPECS_DIR, { recursive: true })
|
|
109
|
+
const stripTrailingWhitespace = (source: string): string => source.replace(/[ \t]+$/gm, "")
|
|
123
110
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
111
|
+
/**
|
|
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
|
+
*/
|
|
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" })
|
|
127
121
|
}
|
|
122
|
+
return cleaned
|
|
123
|
+
}
|
|
128
124
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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(")
|
|
133
|
+
|
|
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 }))
|
|
141
|
+
)
|
|
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)
|
|
158
|
+
})
|
|
159
|
+
yield* Effect.forEach(
|
|
160
|
+
warnings,
|
|
161
|
+
(warning) => Console.warn(`[${warning.code}] ${warning.method ?? ""} ${warning.path ?? ""}: ${warning.message}`),
|
|
162
|
+
{ discard: true }
|
|
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
|
+
)
|
|
136
170
|
})
|
|
137
171
|
|
|
138
|
-
|
|
172
|
+
const check = Flag.boolean("check").pipe(
|
|
173
|
+
Flag.withDescription("Exit non-zero when the committed upstream spec differs from Clockify")
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
const local = Flag.boolean("local").pipe(
|
|
177
|
+
Flag.withDescription("Regenerate from the committed spec without contacting Clockify")
|
|
178
|
+
)
|
|
179
|
+
|
|
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
|
+
)
|
|
220
|
+
|
|
221
|
+
const MainLayer = Layer.mergeAll(
|
|
222
|
+
OpenApiGenerator.layerTransformerSchema,
|
|
223
|
+
NodeHttpClient.layerFetch,
|
|
224
|
+
NodeServices.layer
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
Command.run(root, { version: "1.0.0" }).pipe(
|
|
228
|
+
Effect.provide(MainLayer),
|
|
229
|
+
NodeRuntime.runMain
|
|
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
|
+
}
|