@knpkv/clockify-api-client 0.3.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 +10 -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 +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/src/index.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
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
6
|
|
|
7
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
ClockifyApiClient,
|
|
9
|
+
type ClockifyApiClientShape,
|
|
10
|
+
type ClockifyClientError,
|
|
11
|
+
layer,
|
|
12
|
+
make
|
|
13
|
+
} from "./ClockifyApiClient.js"
|
|
8
14
|
|
|
9
15
|
export type {
|
|
16
|
+
AuthenticatedClockifyApi,
|
|
10
17
|
CreateTimeEntryParams,
|
|
11
18
|
GetTimeEntriesParams,
|
|
12
19
|
Project,
|
|
@@ -21,15 +28,4 @@ export type {
|
|
|
21
28
|
|
|
22
29
|
export { ClockifyApiConfig, type ClockifyApiConfigShape } from "./ClockifyApiConfig.js"
|
|
23
30
|
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
export {
|
|
27
|
-
FetchClientError,
|
|
28
|
-
makeOpenApiFetchClient,
|
|
29
|
-
type OpenApiFetchClient,
|
|
30
|
-
type SuccessData,
|
|
31
|
-
toEffect
|
|
32
|
-
} from "./OpenApiFetchClient.js"
|
|
33
|
-
|
|
34
|
-
// Re-export generated types
|
|
35
|
-
export type * as V1 from "./generated/index.js"
|
|
31
|
+
export * as ClockifyApi from "./generated/ClockifyApi.js"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, expect, it } from "@effect/vitest"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Layer from "effect/Layer"
|
|
4
|
+
import * as Redacted from "effect/Redacted"
|
|
5
|
+
import * as HttpClient from "effect/unstable/http/HttpClient"
|
|
6
|
+
import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
|
|
7
|
+
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
|
|
8
|
+
import { ClockifyApiClient, ClockifyApiConfig, make } from "../src/index.js"
|
|
9
|
+
|
|
10
|
+
const clientLayer = (
|
|
11
|
+
response: { readonly status: number; readonly body: unknown },
|
|
12
|
+
requests: Array<HttpClientRequest.HttpClientRequest>
|
|
13
|
+
) =>
|
|
14
|
+
ClockifyApiClient.layer.pipe(
|
|
15
|
+
Layer.provide(Layer.succeed(ClockifyApiConfig, {
|
|
16
|
+
apiKey: Redacted.make("secret"),
|
|
17
|
+
workspaceId: "workspace-1",
|
|
18
|
+
userId: "user-1",
|
|
19
|
+
baseUrl: "https://clockify.test/api"
|
|
20
|
+
})),
|
|
21
|
+
Layer.provide(Layer.succeed(
|
|
22
|
+
HttpClient.HttpClient,
|
|
23
|
+
HttpClient.make((request) =>
|
|
24
|
+
Effect.sync(() => {
|
|
25
|
+
requests.push(request)
|
|
26
|
+
return HttpClientResponse.fromWeb(
|
|
27
|
+
request,
|
|
28
|
+
new Response(JSON.stringify(response.body), {
|
|
29
|
+
status: response.status,
|
|
30
|
+
headers: { "content-type": "application/json" }
|
|
31
|
+
})
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
)
|
|
35
|
+
))
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
describe("ClockifyApiClient", () => {
|
|
39
|
+
it.effect("authenticates requests and decodes responses with Schema", () => {
|
|
40
|
+
const requests: Array<HttpClientRequest.HttpClientRequest> = []
|
|
41
|
+
return Effect.gen(function*() {
|
|
42
|
+
const client = yield* ClockifyApiClient
|
|
43
|
+
const user = yield* client.getUser()
|
|
44
|
+
expect(user.id).toBe("user-1")
|
|
45
|
+
expect(user.status).toBe("ACTIVE")
|
|
46
|
+
expect(requests).toHaveLength(1)
|
|
47
|
+
expect(requests[0]?.url).toBe("https://clockify.test/api/v1/user")
|
|
48
|
+
expect(requests[0]?.headers["x-api-key"]).toBe("secret")
|
|
49
|
+
}).pipe(
|
|
50
|
+
Effect.provide(clientLayer({
|
|
51
|
+
status: 200,
|
|
52
|
+
body: { id: "user-1", name: "Ada", email: "ada@example.com", status: "ACTIVE" }
|
|
53
|
+
}, requests))
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it.effect("decodes nullable interval fields for a running time entry", () => {
|
|
58
|
+
const requests: Array<HttpClientRequest.HttpClientRequest> = []
|
|
59
|
+
return Effect.gen(function*() {
|
|
60
|
+
const client = yield* ClockifyApiClient
|
|
61
|
+
const entry = yield* client.getRunningTimer("workspace-1", "user-1")
|
|
62
|
+
expect(entry?.timeInterval?.end).toBeNull()
|
|
63
|
+
expect(entry?.timeInterval?.duration).toBeNull()
|
|
64
|
+
}).pipe(
|
|
65
|
+
Effect.provide(clientLayer({
|
|
66
|
+
status: 200,
|
|
67
|
+
body: [{
|
|
68
|
+
id: "entry-1",
|
|
69
|
+
description: "In progress",
|
|
70
|
+
billable: false,
|
|
71
|
+
userId: "user-1",
|
|
72
|
+
workspaceId: "workspace-1",
|
|
73
|
+
timeInterval: {
|
|
74
|
+
start: "2026-07-11T08:00:00Z",
|
|
75
|
+
end: null,
|
|
76
|
+
duration: null
|
|
77
|
+
}
|
|
78
|
+
}]
|
|
79
|
+
}, requests))
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it.effect("leaves multipart content type unset so the transport can add its boundary", () => {
|
|
84
|
+
const requests: Array<HttpClientRequest.HttpClientRequest> = []
|
|
85
|
+
const httpClient = HttpClient.make((request) =>
|
|
86
|
+
Effect.sync(() => {
|
|
87
|
+
requests.push(request)
|
|
88
|
+
return HttpClientResponse.fromWeb(
|
|
89
|
+
request,
|
|
90
|
+
new Response(JSON.stringify({ name: "avatar.png", url: "https://clockify.test/avatar.png" }), {
|
|
91
|
+
status: 200,
|
|
92
|
+
headers: { "content-type": "application/json" }
|
|
93
|
+
})
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
)
|
|
97
|
+
const client = make(httpClient, {
|
|
98
|
+
apiKey: Redacted.make("secret"),
|
|
99
|
+
baseUrl: "https://clockify.test/api"
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return Effect.gen(function*() {
|
|
103
|
+
yield* client.uploadImage({ file: new Blob(["avatar bytes"], { type: "image/png" }) })
|
|
104
|
+
expect(requests[0]?.headers["content-type"]).toBeUndefined()
|
|
105
|
+
expect(requests[0]?.body._tag).toBe("FormData")
|
|
106
|
+
if (requests[0]?.body._tag !== "FormData") throw new Error("Expected a FormData request body")
|
|
107
|
+
const file = requests[0].body.formData.get("file")
|
|
108
|
+
expect(requests[0].body.formData).toBeInstanceOf(FormData)
|
|
109
|
+
expect(typeof file).not.toBe("string")
|
|
110
|
+
if (file === null || typeof file === "string") throw new Error("Expected a file field")
|
|
111
|
+
expect(yield* Effect.promise(() => file.text())).toBe("avatar bytes")
|
|
112
|
+
expect(requests[0]?.headers["x-api-key"]).toBe("secret")
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it.effect("fails when a successful response violates the generated schema", () =>
|
|
117
|
+
Effect.gen(function*() {
|
|
118
|
+
const client = yield* ClockifyApiClient
|
|
119
|
+
const result = yield* Effect.result(client.getUser())
|
|
120
|
+
expect(result._tag).toBe("Failure")
|
|
121
|
+
}).pipe(Effect.provide(clientLayer({ status: 200, body: { id: "user-1" } }, []))))
|
|
122
|
+
|
|
123
|
+
it.effect("fails on non-success status codes", () =>
|
|
124
|
+
Effect.gen(function*() {
|
|
125
|
+
const client = yield* ClockifyApiClient
|
|
126
|
+
const result = yield* Effect.result(client.getUser())
|
|
127
|
+
expect(result._tag).toBe("Failure")
|
|
128
|
+
}).pipe(Effect.provide(clientLayer({ status: 401, body: { message: "Unauthorized" } }, []))))
|
|
129
|
+
})
|
package/vitest.config.ts
ADDED
package/.specs/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
v1
|