@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +92 -80
  3. package/dist/ClockifyApiClient.d.ts +45 -46
  4. package/dist/ClockifyApiClient.d.ts.map +1 -1
  5. package/dist/ClockifyApiClient.js +53 -122
  6. package/dist/ClockifyApiClient.js.map +1 -1
  7. package/dist/ClockifyApiConfig.d.ts +1 -1
  8. package/dist/ClockifyApiConfig.d.ts.map +1 -1
  9. package/dist/ClockifyApiConfig.js +1 -1
  10. package/dist/ClockifyApiConfig.js.map +1 -1
  11. package/dist/generated/ClockifyApi.d.ts +14432 -0
  12. package/dist/generated/ClockifyApi.d.ts.map +1 -0
  13. package/dist/generated/ClockifyApi.js +1239 -0
  14. package/dist/generated/ClockifyApi.js.map +1 -0
  15. package/dist/index.d.ts +4 -6
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +3 -4
  18. package/dist/index.js.map +1 -1
  19. package/package.json +16 -21
  20. package/scripts/regenerate.ts +208 -116
  21. package/scripts/tsconfig.json +13 -0
  22. package/src/ClockifyApiClient.ts +127 -228
  23. package/src/ClockifyApiConfig.ts +2 -2
  24. package/src/generated/ClockifyApi.ts +3020 -0
  25. package/src/index.ts +10 -14
  26. package/test/ClockifyApiClient.test.ts +129 -0
  27. package/vitest.config.ts +7 -0
  28. package/.specs/VERSION +0 -1
  29. package/.specs/clockify-v1.json +0 -31670
  30. package/.specs/clockify-v1.patch.json +0 -48
  31. package/dist/ClockifyApiError.d.ts +0 -11
  32. package/dist/ClockifyApiError.d.ts.map +0 -1
  33. package/dist/ClockifyApiError.js +0 -14
  34. package/dist/ClockifyApiError.js.map +0 -1
  35. package/dist/OpenApiFetchClient.d.ts +0 -55
  36. package/dist/OpenApiFetchClient.d.ts.map +0 -1
  37. package/dist/OpenApiFetchClient.js +0 -63
  38. package/dist/OpenApiFetchClient.js.map +0 -1
  39. package/dist/generated/index.d.ts +0 -2
  40. package/dist/generated/index.d.ts.map +0 -1
  41. package/dist/generated/index.js +0 -2
  42. package/dist/generated/index.js.map +0 -1
  43. package/dist/generated/schema.d.ts +0 -16204
  44. package/src/ClockifyApiError.ts +0 -17
  45. package/src/OpenApiFetchClient.ts +0 -92
  46. package/src/generated/index.ts +0 -1
  47. package/src/generated/schema.d.ts +0 -16204
  48. package/tsconfig.tsbuildinfo +0 -1
package/src/index.ts CHANGED
@@ -1,12 +1,19 @@
1
1
  /**
2
- * Root barrel export for `@knpkv/clockify-api-client` -- openapi-fetch + Effect Clockify REST client.
2
+ * Root barrel export for the Schema-validated Effect Clockify client.
3
3
  *
4
4
  * @packageDocumentation
5
5
  */
6
6
 
7
- export { ClockifyApiClient, type ClockifyApiClientShape, layer } from "./ClockifyApiClient.js"
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 { ClockifyApiError } from "./ClockifyApiError.js"
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
+ })
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vitest/config"
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["test/**/*.test.ts"]
6
+ }
7
+ })
package/.specs/VERSION DELETED
@@ -1 +0,0 @@
1
- v1