@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/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @knpkv/clockify-api-client
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#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.
8
+
9
+ ### Patch Changes
10
+
11
+ - [#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.
12
+
13
+ ## 0.3.0
14
+
15
+ ### Minor Changes
16
+
17
+ - [#71](https://github.com/knpkv/npm/pull/71) [`e3c3805`](https://github.com/knpkv/npm/commit/e3c3805ee527a6edb69ed91977c95c586b563ff9) Thanks [@konopkov](https://github.com/konopkov)! - Migrate the package workspace to Effect v4 beta.
18
+
19
+ This updates runtime and peer dependencies to the Effect v4 beta module layout,
20
+ adopts Effect platform/runtime services for Node process, HTTP, filesystem, and
21
+ clock access, and refreshes package export metadata to point published type
22
+ entries at emitted `dist/*.d.ts` declarations.
23
+
24
+ CodeCommit packages now use Effect v4-compatible AWS and cache layers, including
25
+ typed `distilled-aws` context services, shared cached-comment decoding, and
26
+ schema-derived config defaults. Jira and Confluence OAuth callback servers bind
27
+ the expected local callback port range again under the Effect v4 Node HTTP
28
+ server layer.
29
+
30
+ The retired Claude AI packages have been removed from the workspace.
31
+
3
32
  ## 0.2.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
  }
61
- declare const ClockifyApiClient_base: Context.TagClass<ClockifyApiClient, "@knpkv/clockify-api-client/ClockifyApiClient", ClockifyApiClientShape>;
56
+ export declare const make: (httpClient: HttpClient.HttpClient, options: {
57
+ readonly apiKey: Redacted.Redacted<string>;
58
+ readonly baseUrl: string;
59
+ }) => AuthenticatedClockifyApi;
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
- // ---------------------------------------------------------------------------
31
- export class ClockifyApiClient extends Context.Tag("@knpkv/clockify-api-client/ClockifyApiClient")() {
32
- static layer = Layer.effect(ClockifyApiClient, Effect.gen(function* () {
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
+ export class ClockifyApiClient extends Context.Service()("@knpkv/clockify-api-client/ClockifyApiClient") {
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;