@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +92 -80
  3. package/dist/ClockifyApiClient.d.ts +44 -45
  4. package/dist/ClockifyApiClient.d.ts.map +1 -1
  5. package/dist/ClockifyApiClient.js +52 -121
  6. package/dist/ClockifyApiClient.js.map +1 -1
  7. package/dist/generated/ClockifyApi.d.ts +14432 -0
  8. package/dist/generated/ClockifyApi.d.ts.map +1 -0
  9. package/dist/generated/ClockifyApi.js +1239 -0
  10. package/dist/generated/ClockifyApi.js.map +1 -0
  11. package/dist/index.d.ts +4 -6
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +3 -4
  14. package/dist/index.js.map +1 -1
  15. package/package.json +15 -21
  16. package/scripts/regenerate.ts +182 -201
  17. package/scripts/tsconfig.json +13 -0
  18. package/src/ClockifyApiClient.ts +125 -226
  19. package/src/generated/ClockifyApi.ts +3020 -0
  20. package/src/index.ts +10 -14
  21. package/test/ClockifyApiClient.test.ts +195 -0
  22. package/vitest.config.ts +7 -0
  23. package/.specs/VERSION +0 -1
  24. package/.specs/clockify-v1.json +0 -31670
  25. package/.specs/clockify-v1.patch.json +0 -48
  26. package/dist/ClockifyApiError.d.ts +0 -11
  27. package/dist/ClockifyApiError.d.ts.map +0 -1
  28. package/dist/ClockifyApiError.js +0 -14
  29. package/dist/ClockifyApiError.js.map +0 -1
  30. package/dist/OpenApiFetchClient.d.ts +0 -55
  31. package/dist/OpenApiFetchClient.d.ts.map +0 -1
  32. package/dist/OpenApiFetchClient.js +0 -63
  33. package/dist/OpenApiFetchClient.js.map +0 -1
  34. package/dist/generated/index.d.ts +0 -2
  35. package/dist/generated/index.d.ts.map +0 -1
  36. package/dist/generated/index.js +0 -2
  37. package/dist/generated/index.js.map +0 -1
  38. package/dist/generated/schema.d.ts +0 -16204
  39. package/src/ClockifyApiError.ts +0 -17
  40. package/src/OpenApiFetchClient.ts +0 -92
  41. package/src/generated/index.ts +0 -1
  42. package/src/generated/schema.d.ts +0 -16204
  43. package/tsconfig.tsbuildinfo +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @knpkv/clockify-api-client
2
2
 
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#249](https://github.com/knpkv/npm/pull/249) [`5a61061`](https://github.com/knpkv/npm/commit/5a610619cef7609148b396d9248924422138221b) Thanks [@konopkov](https://github.com/konopkov)! - Fix `jcf` commands failing to decode Clockify time-entry responses when optional
8
+ fields come back as explicit `null`:
9
+
10
+ - `jcf timer start` failed with `Expected string, got null at ["kioskId"]` —
11
+ Clockify returns `kioskId`, `projectId`, and `taskId` as `null` (not absent).
12
+ - `jcf sync reconcile` failed with `Expected array, got null at [0]["tagIds"]` —
13
+ Clockify returns `tagIds` as `null` for entries with no tags.
14
+
15
+ Patch the OpenAPI spec so those fields decode as nullable across the time-entry
16
+ response schemas (`TimeEntryDtoImplV1`, `TimeEntryDtoV1`,
17
+ `TimeEntryWithRatesDtoV1`) and regenerate the client.
18
+
19
+ Also stop `jcf timer start` from printing a misleading `Timer started` line
20
+ after the start actually failed.
21
+
22
+ ## 1.0.0
23
+
24
+ ### Major Changes
25
+
26
+ - [#125](https://github.com/knpkv/npm/pull/125) [`f820c19`](https://github.com/knpkv/npm/commit/f820c1906e00f2f2d17c2e7cc3921ba26522db43) Thanks [@konopkov](https://github.com/konopkov)! - Replace the openapi-fetch Clockify surface with a Schema-validated client generated by Effect's official OpenAPI generator. The old raw client, `ClockifyApiError`, `toEffect`, and `FetchClientError` exports are removed; consumers now use the generated `ClockifyApi` operations or the authenticated service conveniences.
27
+
28
+ ### Patch Changes
29
+
30
+ - [#125](https://github.com/knpkv/npm/pull/125) [`f820c19`](https://github.com/knpkv/npm/commit/f820c1906e00f2f2d17c2e7cc3921ba26522db43) Thanks [@konopkov](https://github.com/konopkov)! - Upgrade the workspace to Effect 4.0.0-beta.98 and current compatible dependencies. Replace ad hoc object guards with Effect Predicate helpers and migrate retry schedules to the current Schedule API.
31
+
3
32
  ## 0.3.0
4
33
 
5
34
  ### Minor Changes
package/README.md CHANGED
@@ -1,120 +1,132 @@
1
1
  # @knpkv/clockify-api-client
2
2
 
3
- Effect-based Clockify REST API client. Type-safe paths and responses generated from a patched OpenAPI spec via `openapi-typescript` + `openapi-fetch`.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- pnpm add @knpkv/clockify-api-client
9
- ```
10
-
11
- Peer dependencies: `effect`
3
+ Schema-validated Effect client for Clockify's REST API. Request builders,
4
+ response codecs, operation types, and the raw client are generated from
5
+ Clockify's OpenAPI document by `@effect/openapi-generator`.
12
6
 
13
7
  ## Usage
14
8
 
15
- ### Method-based API
16
-
17
- Convenience methods for common operations:
18
-
19
9
  ```typescript
20
- import { Effect, Layer } from "effect"
21
- import * as Redacted from "effect/Redacted"
10
+ import { NodeHttpClient } from "@effect/platform-node"
22
11
  import { ClockifyApiClient, ClockifyApiConfig } from "@knpkv/clockify-api-client"
12
+ import { Effect, Layer, Redacted } from "effect"
23
13
 
24
14
  const program = Effect.gen(function* () {
25
15
  const clockify = yield* ClockifyApiClient
26
-
27
16
  const user = yield* clockify.getUser()
28
17
  const projects = yield* clockify.getProjects("workspace-id")
29
18
 
30
- // Start a timer
31
- const entry = yield* clockify.createTimeEntry("workspace-id", {
32
- start: new Date().toISOString(),
33
- description: "Working on PROJ-123"
34
- })
35
-
36
- // Stop timer
37
- yield* clockify.stopTimer("workspace-id", user.id, {
38
- end: new Date().toISOString()
39
- })
40
-
41
- // Get running timer
42
- const running = yield* clockify.getRunningTimer("workspace-id", user.id)
19
+ return { projects, user }
43
20
  })
44
21
 
45
- const configLayer = Layer.succeed(ClockifyApiConfig, {
22
+ const ConfigLive = Layer.succeed(ClockifyApiConfig, {
46
23
  apiKey: Redacted.make("your-clockify-api-key"),
47
24
  workspaceId: "workspace-id",
48
25
  userId: "user-id",
49
26
  baseUrl: "https://api.clockify.me/api"
50
27
  })
51
28
 
52
- Effect.runPromise(program.pipe(Effect.provide(ClockifyApiClient.layer), Effect.provide(configLayer)))
53
- ```
29
+ const ClockifyLive = ClockifyApiClient.layer.pipe(Layer.provide(ConfigLive), Layer.provide(NodeHttpClient.layerFetch))
54
30
 
55
- ### Raw API
31
+ Effect.runPromise(program.pipe(Effect.provide(ClockifyLive)))
32
+ ```
56
33
 
57
- Direct openapi-fetch access for any endpoint:
34
+ `ClockifyApiClient` exposes the timer application's domain conveniences. The
35
+ complete generated client is exported as `ClockifyApi`, and `make` constructs
36
+ an authenticated `AuthenticatedClockifyApi` wrapper from any Effect
37
+ `HttpClient`. Its `uploadImage` method accepts a `Blob` and builds valid native
38
+ `FormData`:
58
39
 
59
40
  ```typescript
60
- import { toEffect } from "@knpkv/clockify-api-client"
41
+ import { type AuthenticatedClockifyApi, make } from "@knpkv/clockify-api-client"
61
42
 
62
- const program = Effect.gen(function* () {
63
- const clockify = yield* ClockifyApiClient
43
+ const raw: AuthenticatedClockifyApi = make(httpClient, {
44
+ apiKey: Redacted.make("key"),
45
+ baseUrl: "https://api.clockify.me/api"
46
+ })
64
47
 
65
- const user = yield* toEffect(clockify.api.client.GET("/v1/user"))
48
+ const entries =
49
+ yield *
50
+ raw.getTimeEntries("workspace-id", "user-id", {
51
+ params: { "in-progress": "true" }
52
+ })
53
+ ```
66
54
 
67
- const entries = yield* toEffect(
68
- clockify.api.client.GET("/v1/workspaces/{workspaceId}/user/{userId}/time-entries", {
69
- params: { path: { workspaceId: "ws-id", userId: "u-id" } }
70
- })
71
- )
72
- })
55
+ All successful response bodies are decoded with generated Effect Schemas.
56
+ Malformed success bodies fail with `SchemaError`; transport and non-success
57
+ HTTP responses fail with `HttpClientError`.
58
+
59
+ ## Regenerating the client
60
+
61
+ Run from the repository root:
62
+
63
+ ```bash
64
+ pnpm --filter @knpkv/clockify-api-client regenerate
65
+ pnpm --filter @knpkv/clockify-api-client build
66
+ pnpm --filter @knpkv/clockify-api-client test
73
67
  ```
74
68
 
75
- ## Available Methods
76
-
77
- | Method | Description |
78
- | ---------------------------------------- | ----------------------------- |
79
- | `getUser()` | Current authenticated user |
80
- | `getWorkspaces()` | List workspaces |
81
- | `getProjects(wsId)` | List projects in workspace |
82
- | `getProjectByName(wsId, name)` | Find project by name |
83
- | `createTimeEntry(wsId, params)` | Start or create entry |
84
- | `stopTimer(wsId, userId, params)` | Stop running timer |
85
- | `getTimeEntries(wsId, userId, params?)` | List time entries |
86
- | `getRunningTimer(wsId, userId)` | Get in-progress entry or null |
87
- | `getTimeEntry(wsId, entryId)` | Get single entry |
88
- | `updateTimeEntry(wsId, entryId, params)` | Update entry |
89
- | `deleteTimeEntry(wsId, entryId)` | Delete entry |
90
- | `getTags(wsId)` | List tags |
91
- | `createTag(wsId, name)` | Create tag |
92
- | `findOrCreateTag(wsId, name)` | Find or create tag |
93
-
94
- ## Errors
95
-
96
- `FetchClientError` — tagged error with `status` and `message`:
69
+ `regenerate` performs the complete deterministic pipeline:
70
+
71
+ 1. Fetch `https://docs.clockify.me/openapi.json`.
72
+ 2. Parse and canonicalize it as JSON.
73
+ 3. Save the unmodified upstream document to `.specs/clockify-v1.json`.
74
+ 4. Apply `.specs/clockify-v1.patch.json` in memory as RFC 6902 JSON Patch.
75
+ The patch corrects known upstream schema mismatches, including Clockify's
76
+ string-valued account status and nullable `end`/`duration` fields on running
77
+ time entries. The V1 `end` patch replaces the property and omits its
78
+ `date-time` format because the current generator drops `null` from a
79
+ formatted nullable string; the runtime value remains an ISO date-time string
80
+ when present.
81
+ 5. Remove unreliable `examples` and `default` schema annotations. Clockify's
82
+ document currently contains metadata whose values contradict its schemas.
83
+ 6. Generate `src/generated/ClockifyApi.ts` with
84
+ `@effect/openapi-generator`'s `httpclient` format.
85
+ 7. Normalize generated multipart request construction to Effect's record-aware
86
+ `bodyFormDataRecord` helper. The exported `uploadImage` method accepts a
87
+ `Blob`, so the runtime builds a real `FormData` body and boundary.
88
+ 8. Record the upstream version in `.specs/VERSION`.
89
+
90
+ Never edit `src/generated/ClockifyApi.ts` or the committed upstream spec by
91
+ hand. API corrections belong in `clockify-v1.patch.json`; generator behavior
92
+ belongs in `scripts/regenerate.ts`. After changing only the patch or generator,
93
+ regenerate without network access:
97
94
 
98
- ```typescript
99
- import { FetchClientError } from "@knpkv/clockify-api-client"
95
+ ```bash
96
+ pnpm --filter @knpkv/clockify-api-client regenerate --local
97
+ ```
98
+
99
+ Review every regeneration with:
100
+
101
+ ```bash
102
+ git diff -- packages/clockify-api-client/.specs
103
+ git diff -- packages/clockify-api-client/src/generated/ClockifyApi.ts
104
+ pnpm --filter @knpkv/clockify-api-client build
105
+ pnpm --filter @knpkv/clockify-api-client test
106
+ pnpm --filter @knpkv/jira-clockify test
107
+ ```
100
108
 
101
- program.pipe(Effect.catchTag("FetchClientError", (e) => Console.log(`HTTP ${e.status}: ${e.message}`)))
109
+ ## Checking specification freshness
110
+
111
+ ```bash
112
+ pnpm --filter @knpkv/clockify-api-client regenerate:check
102
113
  ```
103
114
 
104
- ## Notes
115
+ The command fetches and canonicalizes the current upstream document, then
116
+ compares it byte-for-byte with the committed unmodified document. It exits
117
+ non-zero with the regeneration command when they differ.
105
118
 
106
- - The OpenAPI spec is patched for accurate types (the official spec has gaps).
107
- - API key is stored as `Redacted<string>` to prevent accidental logging.
119
+ The scheduled `Clockify API Spec Check` GitHub Actions workflow runs this check
120
+ daily. When the spec changes it regenerates the client, builds it, runs the
121
+ Clockify and jira-clockify tests, adds a changeset, and opens or updates the
122
+ `chore/clockify-api-spec-update` pull request.
108
123
 
109
- ## Subpath Exports
124
+ ## Generated-code policy
110
125
 
111
- | Export | Contents |
112
- | ---------------------------------------------- | ------------------------------- |
113
- | `@knpkv/clockify-api-client` | Client, config, toEffect, types |
114
- | `@knpkv/clockify-api-client/generated` | Generated OpenAPI types |
115
- | `@knpkv/clockify-api-client/ClockifyApiClient` | Client service only |
116
- | `@knpkv/clockify-api-client/ClockifyApiConfig` | Config service only |
117
- | `@knpkv/clockify-api-client/ClockifyApiError` | Error types |
126
+ `src/generated/**` is excluded from handwritten-source lint rules. The official
127
+ Effect beta generator currently emits a small number of internal casts needed
128
+ by its generic transport implementation. Generated code is still compiled and
129
+ tested; only style/AST rules intended for maintained source are skipped.
118
130
 
119
131
  ## License
120
132
 
@@ -1,39 +1,36 @@
1
1
  /**
2
- * Effect Layer wrapping openapi-fetch Clockify v1 client with auth and base URL.
2
+ * Schema-validated Clockify API client.
3
3
  *
4
- * **Mental model**
5
- *
6
- * - **Auth via X-Api-Key**: The layer reads {@link ClockifyApiConfig} to build the
7
- * `X-Api-Key` header and derive the base URL.
8
- * - **openapi-fetch wrapper**: Uses {@link OpenApiFetchClient} for type-safe HTTP calls.
9
- * - **Method-based interface**: Exposes convenience methods (getUser, createTimeEntry, etc.)
10
- * for backwards compatibility with jira-clockify consumers.
11
- * - **Raw API access**: `.api` exposes the raw `OpenApiFetchClient<paths>` for direct access.
12
- *
13
- * **Common tasks**
14
- *
15
- * - Use the client: `const clockify = yield* ClockifyApiClient`
16
- * - Call a method: `clockify.getProjects(workspaceId)`
17
- * - Raw access: `toEffect(clockify.api.client.GET("/v1/user"))`
18
- * - Provide the layer: `Effect.provide(ClockifyApiClient.layer)`
4
+ * The OpenAPI generator owns request construction and response decoding. This
5
+ * module only applies Clockify authentication and provides domain conveniences
6
+ * used by the timer application.
19
7
  *
20
8
  * @module
21
9
  */
22
10
  import * as Context from "effect/Context";
23
11
  import * as Effect from "effect/Effect";
24
12
  import * as Layer from "effect/Layer";
13
+ import * as Redacted from "effect/Redacted";
14
+ import type { SchemaError } from "effect/Schema";
15
+ import * as HttpClient from "effect/unstable/http/HttpClient";
16
+ import type * as HttpClientError from "effect/unstable/http/HttpClientError";
25
17
  import { ClockifyApiConfig } from "./ClockifyApiConfig.js";
26
- import type { components, paths } from "./generated/schema.js";
27
- import { type FetchClientError, type OpenApiFetchClient } from "./OpenApiFetchClient.js";
28
- export type User = components["schemas"]["UserDto"];
29
- export type Workspace = components["schemas"]["WorkspaceDtoV1"];
30
- export type Project = components["schemas"]["ProjectDtoV1"];
31
- export type Tag = components["schemas"]["TagDto"];
32
- export type TimeInterval = components["schemas"]["TimeIntervalDto"];
33
- export type TimeEntry = components["schemas"]["TimeEntryDto"];
34
- export type CreateTimeEntryParams = components["schemas"]["CreateTimeEntryRequest"];
35
- export type StopTimeEntryParams = components["schemas"]["StopTimeEntryRequest"];
36
- export type UpdateTimeEntryParams = components["schemas"]["UpdateTimeEntryRequest"];
18
+ import * as Generated from "./generated/ClockifyApi.js";
19
+ export type ClockifyClientError = HttpClientError.HttpClientError | SchemaError;
20
+ export type User = Generated.GetLoggedUser200;
21
+ export type Workspace = Generated.GetWorkspacesOfUser200[number];
22
+ export type Project = Generated.GetProjects200[number];
23
+ export type Tag = Generated.GetTags200[number];
24
+ export type TimeEntry = Generated.GetTimeEntries200[number] | Generated.CreateTimeEntry201;
25
+ export type TimeInterval = NonNullable<TimeEntry["timeInterval"]>;
26
+ export type CreateTimeEntryParams = Generated.CreateTimeEntryRequestJson;
27
+ export type StopTimeEntryParams = Generated.StopRunningTimeEntryRequestJson;
28
+ export type UpdateTimeEntryParams = Generated.UpdateTimeEntryRequestJson;
29
+ export type AuthenticatedClockifyApi = Omit<Generated.ClockifyApi, "uploadImage"> & {
30
+ readonly uploadImage: (input: {
31
+ readonly file: Blob;
32
+ }) => Effect.Effect<Generated.UploadImage200, ClockifyClientError>;
33
+ };
37
34
  export interface GetTimeEntriesParams {
38
35
  readonly start?: string | undefined;
39
36
  readonly end?: string | undefined;
@@ -41,27 +38,29 @@ export interface GetTimeEntriesParams {
41
38
  readonly pageSize?: number | undefined;
42
39
  }
43
40
  export interface ClockifyApiClientShape {
44
- /** Raw openapi-fetch client for direct type-safe API access. */
45
- readonly api: OpenApiFetchClient<paths>;
46
- readonly getUser: () => Effect.Effect<User, FetchClientError>;
47
- readonly getWorkspaces: () => Effect.Effect<ReadonlyArray<Workspace>, FetchClientError>;
48
- readonly getProjects: (workspaceId: string) => Effect.Effect<ReadonlyArray<Project>, FetchClientError>;
49
- readonly getProjectByName: (workspaceId: string, name: string) => Effect.Effect<Project | null, FetchClientError>;
50
- readonly createTimeEntry: (workspaceId: string, params: CreateTimeEntryParams) => Effect.Effect<TimeEntry, FetchClientError>;
51
- readonly stopTimer: (workspaceId: string, userId: string, params: StopTimeEntryParams) => Effect.Effect<TimeEntry, FetchClientError>;
52
- readonly getTimeEntries: (workspaceId: string, userId: string, params?: GetTimeEntriesParams) => Effect.Effect<ReadonlyArray<TimeEntry>, FetchClientError>;
53
- readonly getRunningTimer: (workspaceId: string, userId: string) => Effect.Effect<TimeEntry | null, FetchClientError>;
54
- readonly getTags: (workspaceId: string) => Effect.Effect<ReadonlyArray<Tag>, FetchClientError>;
55
- readonly createTag: (workspaceId: string, name: string) => Effect.Effect<Tag, FetchClientError>;
56
- readonly findOrCreateTag: (workspaceId: string, name: string) => Effect.Effect<Tag, FetchClientError>;
57
- readonly getTimeEntry: (workspaceId: string, timeEntryId: string) => Effect.Effect<TimeEntry, FetchClientError>;
58
- readonly deleteTimeEntry: (workspaceId: string, timeEntryId: string) => Effect.Effect<void, FetchClientError>;
59
- readonly updateTimeEntry: (workspaceId: string, timeEntryId: string, params: UpdateTimeEntryParams) => Effect.Effect<TimeEntry, FetchClientError>;
41
+ readonly getUser: () => Effect.Effect<User, ClockifyClientError>;
42
+ readonly getWorkspaces: () => Effect.Effect<ReadonlyArray<Workspace>, ClockifyClientError>;
43
+ readonly getProjects: (workspaceId: string) => Effect.Effect<ReadonlyArray<Project>, ClockifyClientError>;
44
+ readonly getProjectByName: (workspaceId: string, name: string) => Effect.Effect<Project | null, ClockifyClientError>;
45
+ readonly createTimeEntry: (workspaceId: string, params: CreateTimeEntryParams) => Effect.Effect<Generated.CreateTimeEntry201, ClockifyClientError>;
46
+ readonly stopTimer: (workspaceId: string, userId: string, params: StopTimeEntryParams) => Effect.Effect<Generated.StopRunningTimeEntry200, ClockifyClientError>;
47
+ readonly getTimeEntries: (workspaceId: string, userId: string, params?: GetTimeEntriesParams) => Effect.Effect<Generated.GetTimeEntries200, ClockifyClientError>;
48
+ readonly getRunningTimer: (workspaceId: string, userId: string) => Effect.Effect<Generated.GetTimeEntries200[number] | null, ClockifyClientError>;
49
+ readonly getTags: (workspaceId: string) => Effect.Effect<Generated.GetTags200, ClockifyClientError>;
50
+ readonly createTag: (workspaceId: string, name: string) => Effect.Effect<Generated.CreateNewTag201, ClockifyClientError>;
51
+ readonly findOrCreateTag: (workspaceId: string, name: string) => Effect.Effect<Generated.GetTags200[number] | Generated.CreateNewTag201, ClockifyClientError>;
52
+ readonly getTimeEntry: (workspaceId: string, timeEntryId: string) => Effect.Effect<Generated.GetTimeEntry200, ClockifyClientError>;
53
+ readonly deleteTimeEntry: (workspaceId: string, timeEntryId: string) => Effect.Effect<void, ClockifyClientError>;
54
+ readonly updateTimeEntry: (workspaceId: string, timeEntryId: string, params: UpdateTimeEntryParams) => Effect.Effect<Generated.UpdateTimeEntry200, ClockifyClientError>;
60
55
  }
56
+ export declare const make: (httpClient: HttpClient.HttpClient, options: {
57
+ readonly apiKey: Redacted.Redacted<string>;
58
+ readonly baseUrl: string;
59
+ }) => AuthenticatedClockifyApi;
61
60
  declare const ClockifyApiClient_base: Context.ServiceClass<ClockifyApiClient, "@knpkv/clockify-api-client/ClockifyApiClient", ClockifyApiClientShape>;
62
61
  export declare class ClockifyApiClient extends ClockifyApiClient_base {
63
- static readonly layer: Layer.Layer<ClockifyApiClient, never, ClockifyApiConfig>;
62
+ static readonly layer: Layer.Layer<ClockifyApiClient, never, ClockifyApiConfig | HttpClient.HttpClient>;
64
63
  }
65
- export declare const layer: Layer.Layer<ClockifyApiClient, never, ClockifyApiConfig>;
64
+ export declare const layer: Layer.Layer<ClockifyApiClient, never, ClockifyApiConfig | HttpClient.HttpClient>;
66
65
  export {};
67
66
  //# sourceMappingURL=ClockifyApiClient.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ClockifyApiClient.d.ts","sourceRoot":"","sources":["../src/ClockifyApiClient.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ClockifyApiClient.d.ts","sourceRoot":"","sources":["../src/ClockifyApiClient.ts"],"names":[],"mappings":"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"}
@@ -1,140 +1,71 @@
1
1
  /**
2
- * Effect Layer wrapping openapi-fetch Clockify v1 client with auth and base URL.
2
+ * Schema-validated Clockify API client.
3
3
  *
4
- * **Mental model**
5
- *
6
- * - **Auth via X-Api-Key**: The layer reads {@link ClockifyApiConfig} to build the
7
- * `X-Api-Key` header and derive the base URL.
8
- * - **openapi-fetch wrapper**: Uses {@link OpenApiFetchClient} for type-safe HTTP calls.
9
- * - **Method-based interface**: Exposes convenience methods (getUser, createTimeEntry, etc.)
10
- * for backwards compatibility with jira-clockify consumers.
11
- * - **Raw API access**: `.api` exposes the raw `OpenApiFetchClient<paths>` for direct access.
12
- *
13
- * **Common tasks**
14
- *
15
- * - Use the client: `const clockify = yield* ClockifyApiClient`
16
- * - Call a method: `clockify.getProjects(workspaceId)`
17
- * - Raw access: `toEffect(clockify.api.client.GET("/v1/user"))`
18
- * - Provide the layer: `Effect.provide(ClockifyApiClient.layer)`
4
+ * The OpenAPI generator owns request construction and response decoding. This
5
+ * module only applies Clockify authentication and provides domain conveniences
6
+ * used by the timer application.
19
7
  *
20
8
  * @module
21
9
  */
22
10
  import * as Context from "effect/Context";
23
11
  import * as Effect from "effect/Effect";
12
+ import { flow } from "effect/Function";
24
13
  import * as Layer from "effect/Layer";
25
14
  import * as Redacted from "effect/Redacted";
15
+ import * as HttpClient from "effect/unstable/http/HttpClient";
16
+ import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
17
+ import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
26
18
  import { ClockifyApiConfig } from "./ClockifyApiConfig.js";
27
- import { makeOpenApiFetchClient, toEffect } from "./OpenApiFetchClient.js";
28
- // ---------------------------------------------------------------------------
29
- // Service tag + layer
30
- // ---------------------------------------------------------------------------
19
+ import * as Generated from "./generated/ClockifyApi.js";
20
+ export const make = (httpClient, options) => {
21
+ const api = Generated.make(httpClient.pipe(HttpClient.mapRequest(flow(HttpClientRequest.prependUrl(options.baseUrl), HttpClientRequest.setHeader("X-Api-Key", Redacted.value(options.apiKey))))));
22
+ return {
23
+ ...api,
24
+ uploadImage: (input) => {
25
+ const form = new FormData();
26
+ form.append("file", input.file);
27
+ return api.httpClient.execute(HttpClientRequest.post("/v1/file/image").pipe(HttpClientRequest.bodyFormData(form))).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.flatMap(HttpClientResponse.schemaBodyJson(Generated.UploadImage200)));
28
+ }
29
+ };
30
+ };
31
31
  export class ClockifyApiClient extends Context.Service()("@knpkv/clockify-api-client/ClockifyApiClient") {
32
- static layer = Layer.effect(ClockifyApiClient, Effect.gen(function* () {
32
+ static layer = Layer
33
+ .effect(ClockifyApiClient, Effect.gen(function* () {
33
34
  const config = yield* ClockifyApiConfig;
34
- const headers = {
35
- "X-Api-Key": Redacted.value(config.apiKey),
36
- "Content-Type": "application/json"
37
- };
38
- const api = makeOpenApiFetchClient(config.baseUrl, headers);
39
- const { client } = api;
40
- return {
41
- api,
42
- getUser: () => toEffect(client.GET("/v1/user")),
43
- getWorkspaces: () => toEffect(client.GET("/v1/workspaces")),
44
- getProjects: (workspaceId) => toEffect(client.GET("/v1/workspaces/{workspaceId}/projects", {
45
- params: {
46
- path: { workspaceId },
47
- query: { archived: false, "page-size": 500 }
48
- }
49
- })),
50
- getProjectByName: (workspaceId, name) => Effect.gen(function* () {
51
- const projects = yield* toEffect(client.GET("/v1/workspaces/{workspaceId}/projects", {
52
- params: {
53
- path: { workspaceId },
54
- query: { name, archived: false }
55
- }
56
- }));
57
- return projects.find((p) => p.name.toLowerCase() === name.toLowerCase()) ?? null;
58
- }),
59
- createTimeEntry: (workspaceId, params) => toEffect(client.POST("/v1/workspaces/{workspaceId}/time-entries", {
60
- params: { path: { workspaceId } },
61
- body: {
62
- description: params.description,
63
- start: params.start,
64
- ...(params.end ? { end: params.end } : {}),
65
- ...(params.projectId ? { projectId: params.projectId } : {}),
66
- ...(params.taskId ? { taskId: params.taskId } : {}),
67
- ...(params.billable !== undefined ? { billable: params.billable } : {}),
68
- ...(params.tagIds !== undefined ? { tagIds: [...params.tagIds] } : {})
69
- }
70
- })),
71
- stopTimer: (workspaceId, userId, params) => toEffect(client.PATCH("/v1/workspaces/{workspaceId}/user/{userId}/time-entries", {
72
- params: { path: { workspaceId, userId } },
73
- body: { end: params.end }
74
- })),
75
- getTimeEntries: (workspaceId, userId, params) => toEffect(client.GET("/v1/workspaces/{workspaceId}/user/{userId}/time-entries", {
35
+ const httpClient = yield* HttpClient.HttpClient;
36
+ const api = make(httpClient, config);
37
+ const getProjects = (workspaceId) => api.getProjects(workspaceId, { params: { archived: false, "page-size": 500 } });
38
+ const getTags = (workspaceId) => api.getTags(workspaceId, { params: { archived: false, "page-size": 200 } });
39
+ return ClockifyApiClient.of({
40
+ getUser: () => api.getLoggedUser(undefined),
41
+ getWorkspaces: () => api.getWorkspacesOfUser(undefined),
42
+ getProjects,
43
+ getProjectByName: (workspaceId, name) => api.getProjects(workspaceId, { params: { name, archived: false } }).pipe(Effect.map((projects) => projects.find((project) => project.name.toLowerCase() === name.toLowerCase()) ?? null)),
44
+ createTimeEntry: (workspaceId, payload) => api.createTimeEntry(workspaceId, { payload }),
45
+ stopTimer: (workspaceId, userId, payload) => api.stopRunningTimeEntry(workspaceId, userId, { payload }),
46
+ getTimeEntries: (workspaceId, userId, params) => api.getTimeEntries(workspaceId, userId, {
76
47
  params: {
77
- path: { workspaceId, userId },
78
- query: {
79
- ...(params?.start !== undefined ? { start: params.start } : {}),
80
- ...(params?.end !== undefined ? { end: params.end } : {}),
81
- ...(params?.page !== undefined ? { page: params.page } : {}),
82
- ...(params?.pageSize !== undefined ? { "page-size": params.pageSize } : {})
83
- }
48
+ ...(params?.start !== undefined ? { start: params.start } : {}),
49
+ ...(params?.end !== undefined ? { end: params.end } : {}),
50
+ ...(params?.page !== undefined ? { page: params.page } : {}),
51
+ ...(params?.pageSize !== undefined ? { "page-size": params.pageSize } : {})
84
52
  }
85
- })),
86
- getRunningTimer: (workspaceId, userId) => Effect.gen(function* () {
87
- const entries = yield* toEffect(client.GET("/v1/workspaces/{workspaceId}/user/{userId}/time-entries", {
88
- params: {
89
- path: { workspaceId, userId },
90
- query: { "in-progress": true, "page-size": 1 }
91
- }
92
- }));
93
- return entries.length > 0 ? entries[0] : null;
94
53
  }),
95
- getTimeEntry: (workspaceId, timeEntryId) => toEffect(client.GET("/v1/workspaces/{workspaceId}/time-entries/{id}", {
96
- params: { path: { workspaceId, id: timeEntryId } }
54
+ getRunningTimer: (workspaceId, userId) => api.getTimeEntries(workspaceId, userId, {
55
+ params: { "in-progress": "true", "page-size": 1 }
56
+ }).pipe(Effect.map((entries) => entries[0] ?? null)),
57
+ getTags,
58
+ createTag: (workspaceId, name) => api.createNewTag(workspaceId, { payload: { name } }),
59
+ findOrCreateTag: (workspaceId, name) => api.getTags(workspaceId, { params: { name, archived: false } }).pipe(Effect.flatMap((tags) => {
60
+ const existing = tags.find((tag) => tag.name?.toLowerCase() === name.toLowerCase());
61
+ return existing === undefined
62
+ ? api.createNewTag(workspaceId, { payload: { name } })
63
+ : Effect.succeed(existing);
97
64
  })),
98
- updateTimeEntry: (workspaceId, timeEntryId, params) => toEffect(client.PUT("/v1/workspaces/{workspaceId}/time-entries/{id}", {
99
- params: { path: { workspaceId, id: timeEntryId } },
100
- body: {
101
- start: params.start,
102
- ...(params.end !== undefined ? { end: params.end } : {}),
103
- ...(params.description !== undefined ? { description: params.description } : {}),
104
- ...(params.projectId !== undefined ? { projectId: params.projectId } : {}),
105
- ...(params.billable !== undefined ? { billable: params.billable } : {}),
106
- ...(params.tagIds !== undefined ? { tagIds: [...params.tagIds] } : {})
107
- }
108
- })),
109
- getTags: (workspaceId) => toEffect(client.GET("/v1/workspaces/{workspaceId}/tags", {
110
- params: {
111
- path: { workspaceId },
112
- query: { archived: false, "page-size": 200 }
113
- }
114
- })),
115
- createTag: (workspaceId, name) => toEffect(client.POST("/v1/workspaces/{workspaceId}/tags", {
116
- params: { path: { workspaceId } },
117
- body: { name }
118
- })),
119
- findOrCreateTag: (workspaceId, name) => Effect.gen(function* () {
120
- const tags = yield* toEffect(client.GET("/v1/workspaces/{workspaceId}/tags", {
121
- params: {
122
- path: { workspaceId },
123
- query: { name, archived: false }
124
- }
125
- }));
126
- const existing = tags.find((t) => t.name.toLowerCase() === name.toLowerCase());
127
- if (existing)
128
- return existing;
129
- return yield* toEffect(client.POST("/v1/workspaces/{workspaceId}/tags", {
130
- params: { path: { workspaceId } },
131
- body: { name }
132
- }));
133
- }),
134
- deleteTimeEntry: (workspaceId, timeEntryId) => toEffect(client.DELETE("/v1/workspaces/{workspaceId}/time-entries/{id}", {
135
- params: { path: { workspaceId, id: timeEntryId } }
136
- }))
137
- };
65
+ getTimeEntry: (workspaceId, timeEntryId) => api.getTimeEntry(workspaceId, timeEntryId, undefined),
66
+ deleteTimeEntry: (workspaceId, timeEntryId) => api.deleteTimeEntry(workspaceId, timeEntryId, undefined),
67
+ updateTimeEntry: (workspaceId, timeEntryId, payload) => api.updateTimeEntry(workspaceId, timeEntryId, { payload })
68
+ });
138
69
  }));
139
70
  }
140
71
  export const layer = ClockifyApiClient.layer;