@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.
- package/CHANGELOG.md +29 -0
- package/README.md +92 -80
- package/dist/ClockifyApiClient.d.ts +45 -46
- package/dist/ClockifyApiClient.d.ts.map +1 -1
- package/dist/ClockifyApiClient.js +53 -122
- package/dist/ClockifyApiClient.js.map +1 -1
- package/dist/ClockifyApiConfig.d.ts +1 -1
- package/dist/ClockifyApiConfig.d.ts.map +1 -1
- package/dist/ClockifyApiConfig.js +1 -1
- package/dist/ClockifyApiConfig.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 +16 -21
- package/scripts/regenerate.ts +208 -116
- package/scripts/tsconfig.json +13 -0
- package/src/ClockifyApiClient.ts +127 -228
- package/src/ClockifyApiConfig.ts +2 -2
- package/src/generated/ClockifyApi.ts +3020 -0
- package/src/index.ts +10 -14
- package/test/ClockifyApiClient.test.ts +129 -0
- package/vitest.config.ts +7 -0
- package/.specs/VERSION +0 -1
- package/.specs/clockify-v1.json +0 -31670
- package/.specs/clockify-v1.patch.json +0 -48
- package/dist/ClockifyApiError.d.ts +0 -11
- package/dist/ClockifyApiError.d.ts.map +0 -1
- package/dist/ClockifyApiError.js +0 -14
- package/dist/ClockifyApiError.js.map +0 -1
- package/dist/OpenApiFetchClient.d.ts +0 -55
- package/dist/OpenApiFetchClient.d.ts.map +0 -1
- package/dist/OpenApiFetchClient.js +0 -63
- package/dist/OpenApiFetchClient.js.map +0 -1
- package/dist/generated/index.d.ts +0 -2
- package/dist/generated/index.d.ts.map +0 -1
- package/dist/generated/index.js +0 -2
- package/dist/generated/index.js.map +0 -1
- package/dist/generated/schema.d.ts +0 -16204
- package/src/ClockifyApiError.ts +0 -17
- package/src/OpenApiFetchClient.ts +0 -92
- package/src/generated/index.ts +0 -1
- package/src/generated/schema.d.ts +0 -16204
- package/tsconfig.tsbuildinfo +0 -1
package/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
|
-
|
|
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
|
}
|
|
61
|
-
declare const
|
|
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":"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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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;
|