@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.
- package/CHANGELOG.md +29 -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 +195 -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/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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
53
|
-
```
|
|
29
|
+
const ClockifyLive = ClockifyApiClient.layer.pipe(Layer.provide(ConfigLive), Layer.provide(NodeHttpClient.layerFetch))
|
|
54
30
|
|
|
55
|
-
|
|
31
|
+
Effect.runPromise(program.pipe(Effect.provide(ClockifyLive)))
|
|
32
|
+
```
|
|
56
33
|
|
|
57
|
-
|
|
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 {
|
|
41
|
+
import { type AuthenticatedClockifyApi, make } from "@knpkv/clockify-api-client"
|
|
61
42
|
|
|
62
|
-
const
|
|
63
|
-
|
|
43
|
+
const raw: AuthenticatedClockifyApi = make(httpClient, {
|
|
44
|
+
apiKey: Redacted.make("key"),
|
|
45
|
+
baseUrl: "https://api.clockify.me/api"
|
|
46
|
+
})
|
|
64
47
|
|
|
65
|
-
|
|
48
|
+
const entries =
|
|
49
|
+
yield *
|
|
50
|
+
raw.getTimeEntries("workspace-id", "user-id", {
|
|
51
|
+
params: { "in-progress": "true" }
|
|
52
|
+
})
|
|
53
|
+
```
|
|
66
54
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`
|
|
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
|
-
```
|
|
99
|
-
|
|
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
|
-
|
|
109
|
+
## Checking specification freshness
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pnpm --filter @knpkv/clockify-api-client regenerate:check
|
|
102
113
|
```
|
|
103
114
|
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
##
|
|
124
|
+
## Generated-code policy
|
|
110
125
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
*
|
|
2
|
+
* Schema-validated Clockify API client.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
27
|
-
|
|
28
|
-
export type User =
|
|
29
|
-
export type Workspace =
|
|
30
|
-
export type Project =
|
|
31
|
-
export type Tag =
|
|
32
|
-
export type
|
|
33
|
-
export type
|
|
34
|
-
export type CreateTimeEntryParams =
|
|
35
|
-
export type StopTimeEntryParams =
|
|
36
|
-
export type UpdateTimeEntryParams =
|
|
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
|
-
|
|
45
|
-
readonly
|
|
46
|
-
readonly
|
|
47
|
-
readonly
|
|
48
|
-
readonly
|
|
49
|
-
readonly
|
|
50
|
-
readonly
|
|
51
|
-
readonly
|
|
52
|
-
readonly
|
|
53
|
-
readonly
|
|
54
|
-
readonly
|
|
55
|
-
readonly
|
|
56
|
-
readonly
|
|
57
|
-
readonly
|
|
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":"AAAA
|
|
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
|
-
*
|
|
2
|
+
* Schema-validated Clockify API client.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
32
|
+
static layer = Layer
|
|
33
|
+
.effect(ClockifyApiClient, Effect.gen(function* () {
|
|
33
34
|
const config = yield* ClockifyApiConfig;
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
96
|
-
params: {
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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;
|