@knpkv/clockify-api-client 1.0.2 → 1.1.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/dist/index.d.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * @packageDocumentation
5
5
  */
6
- export { ClockifyApiClient, type ClockifyApiClientShape, type ClockifyClientError, layer, make } from "./ClockifyApiClient.js";
7
- export type { AuthenticatedClockifyApi, CreateTimeEntryParams, GetTimeEntriesParams, Project, StopTimeEntryParams, Tag, TimeEntry, TimeInterval, UpdateTimeEntryParams, User, Workspace } from "./ClockifyApiClient.js";
8
- export { ClockifyApiConfig, type ClockifyApiConfigShape } from "./ClockifyApiConfig.js";
6
+ export { ClockifyApiClient, type ClockifyApiClientContract, type ClockifyClientError, layer, make } from "./ClockifyApiClient.js";
7
+ export type { AuthenticatedClockifyApi, CreateTimeEntryParams, GetTimeEntriesParams, GetTimeEntryParams, Project, StopTimeEntryParams, Tag, TimeEntry, TimeInterval, UpdateTimeEntryParams, User, Workspace, WorkspaceUser } from "./ClockifyApiClient.js";
8
+ export { ClockifyApiConfig, type ClockifyApiConfigContract } from "./ClockifyApiConfig.js";
9
9
  export * as ClockifyApi from "./generated/ClockifyApi.js";
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,iBAAiB,EACjB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,KAAK,EACL,IAAI,EACL,MAAM,wBAAwB,CAAA;AAE/B,YAAY,EACV,wBAAwB,EACxB,qBAAqB,EACrB,oBAAoB,EACpB,OAAO,EACP,mBAAmB,EACnB,GAAG,EACH,SAAS,EACT,YAAY,EACZ,qBAAqB,EACrB,IAAI,EACJ,SAAS,EACV,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAEvF,OAAO,KAAK,WAAW,MAAM,4BAA4B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,iBAAiB,EACjB,KAAK,yBAAyB,EAC9B,KAAK,mBAAmB,EACxB,KAAK,EACL,IAAI,EACL,MAAM,wBAAwB,CAAA;AAE/B,YAAY,EACV,wBAAwB,EACxB,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,EAClB,OAAO,EACP,mBAAmB,EACnB,GAAG,EACH,SAAS,EACT,YAAY,EACZ,qBAAqB,EACrB,IAAI,EACJ,SAAS,EACT,aAAa,EACd,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,iBAAiB,EAAE,KAAK,yBAAyB,EAAE,MAAM,wBAAwB,CAAA;AAE1F,OAAO,KAAK,WAAW,MAAM,4BAA4B,CAAA"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,iBAAiB,EAGjB,KAAK,EACL,IAAI,EACL,MAAM,wBAAwB,CAAA;AAgB/B,OAAO,EAAE,iBAAiB,EAA+B,MAAM,wBAAwB,CAAA;AAEvF,OAAO,KAAK,WAAW,MAAM,4BAA4B,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,iBAAiB,EAGjB,KAAK,EACL,IAAI,EACL,MAAM,wBAAwB,CAAA;AAkB/B,OAAO,EAAE,iBAAiB,EAAkC,MAAM,wBAAwB,CAAA;AAE1F,OAAO,KAAK,WAAW,MAAM,4BAA4B,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knpkv/clockify-api-client",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
4
4
  "description": "Effect-based Clockify REST API client",
5
5
  "license": "MIT",
6
6
  "author": "knpkv",
@@ -35,17 +35,17 @@
35
35
  "access": "public"
36
36
  },
37
37
  "peerDependencies": {
38
- "effect": "4.0.0-beta.98"
38
+ "effect": "4.0.0-rc.109"
39
39
  },
40
40
  "devDependencies": {
41
- "@effect/openapi-generator": "4.0.0-beta.98",
42
- "@effect/platform-node": "4.0.0-beta.98",
43
- "@effect/vitest": "4.0.0-beta.98",
41
+ "@effect/openapi-generator": "4.0.0-rc.109",
42
+ "@effect/platform-node": "4.0.0-rc.109",
43
+ "@effect/vitest": "4.0.0-rc.109",
44
44
  "@types/node": "latest",
45
- "effect": "4.0.0-beta.98",
46
- "prettier": "^3.9.5",
47
- "tsx": "^4.23.0",
48
- "typescript": "~6.0.3",
45
+ "effect": "4.0.0-rc.109",
46
+ "prettier": "^3.9.6",
47
+ "tsx": "^4.23.12",
48
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
49
49
  "vitest": "^4.1.10"
50
50
  },
51
51
  "keywords": [
@@ -23,6 +23,7 @@ import * as Generated from "./generated/ClockifyApi.js"
23
23
  export type ClockifyClientError = HttpClientError.HttpClientError | SchemaError
24
24
 
25
25
  export type User = Generated.GetLoggedUser200
26
+ export type WorkspaceUser = Generated.GetUsersOfWorkspace200[number]
26
27
  export type Workspace = Generated.GetWorkspacesOfUser200[number]
27
28
  export type Project = Generated.GetProjects200[number]
28
29
  export type Tag = Generated.GetTags200[number]
@@ -39,12 +40,25 @@ export type AuthenticatedClockifyApi = Omit<Generated.ClockifyApi, "uploadImage"
39
40
  export interface GetTimeEntriesParams {
40
41
  readonly start?: string | undefined
41
42
  readonly end?: string | undefined
43
+ readonly hydrated?: boolean | undefined
42
44
  readonly page?: number | undefined
43
45
  readonly pageSize?: number | undefined
44
46
  }
45
47
 
46
- export interface ClockifyApiClientShape {
48
+ export interface GetTimeEntryParams {
49
+ readonly hydrated?: boolean | undefined
50
+ }
51
+
52
+ export interface ClockifyApiClientContract {
47
53
  readonly getUser: () => Effect.Effect<User, ClockifyClientError>
54
+ readonly getWorkspaceUsers?: (
55
+ workspaceId: string
56
+ ) => Effect.Effect<ReadonlyArray<WorkspaceUser>, ClockifyClientError>
57
+ readonly getWorkspaceUsersPage?: (
58
+ workspaceId: string,
59
+ page: number,
60
+ pageSize: number
61
+ ) => Effect.Effect<ReadonlyArray<WorkspaceUser>, ClockifyClientError>
48
62
  readonly getWorkspaces: () => Effect.Effect<ReadonlyArray<Workspace>, ClockifyClientError>
49
63
  readonly getProjects: (workspaceId: string) => Effect.Effect<ReadonlyArray<Project>, ClockifyClientError>
50
64
  readonly getProjectByName: (workspaceId: string, name: string) => Effect.Effect<Project | null, ClockifyClientError>
@@ -77,7 +91,8 @@ export interface ClockifyApiClientShape {
77
91
  ) => Effect.Effect<Generated.GetTags200[number] | Generated.CreateNewTag201, ClockifyClientError>
78
92
  readonly getTimeEntry: (
79
93
  workspaceId: string,
80
- timeEntryId: string
94
+ timeEntryId: string,
95
+ params?: GetTimeEntryParams
81
96
  ) => Effect.Effect<Generated.GetTimeEntry200, ClockifyClientError>
82
97
  readonly deleteTimeEntry: (workspaceId: string, timeEntryId: string) => Effect.Effect<void, ClockifyClientError>
83
98
  readonly updateTimeEntry: (
@@ -112,7 +127,7 @@ export const make = (
112
127
  }
113
128
  }
114
129
 
115
- export class ClockifyApiClient extends Context.Service<ClockifyApiClient, ClockifyApiClientShape>()(
130
+ export class ClockifyApiClient extends Context.Service<ClockifyApiClient, ClockifyApiClientContract>()(
116
131
  "@knpkv/clockify-api-client/ClockifyApiClient"
117
132
  ) {
118
133
  static readonly layer: Layer.Layer<ClockifyApiClient, never, ClockifyApiConfig | HttpClient.HttpClient> = Layer
@@ -129,8 +144,36 @@ export class ClockifyApiClient extends Context.Service<ClockifyApiClient, Clocki
129
144
  const getTags = (workspaceId: string) =>
130
145
  api.getTags(workspaceId, { params: { archived: false, "page-size": 200 } })
131
146
 
147
+ const getWorkspaceUsersPage = Effect.fn("ClockifyApiClient.getWorkspaceUsersPage")(function*(
148
+ workspaceId: string,
149
+ page: number,
150
+ pageSize: number
151
+ ) {
152
+ return yield* api.getUsersOfWorkspace(workspaceId, {
153
+ params: {
154
+ "account-statuses": "ACTIVE,PENDING_EMAIL_VERIFICATION,DELETED,NOT_REGISTERED,LIMITED,LIMITED_DELETED",
155
+ "include-roles": "false",
156
+ memberships: "ALL",
157
+ page,
158
+ "page-size": pageSize,
159
+ status: "ALL"
160
+ }
161
+ })
162
+ })
163
+ const getWorkspaceUsers = Effect.fn("ClockifyApiClient.getWorkspaceUsers")(function*(workspaceId: string) {
164
+ const users: Array<WorkspaceUser> = []
165
+ for (let page = 1;; page++) {
166
+ const batch = yield* getWorkspaceUsersPage(workspaceId, page, 500)
167
+ for (const user of batch) users.push(user)
168
+ if (batch.length < 500) break
169
+ }
170
+ return users
171
+ })
172
+
132
173
  return ClockifyApiClient.of({
133
174
  getUser: () => api.getLoggedUser(undefined),
175
+ getWorkspaceUsers,
176
+ getWorkspaceUsersPage,
134
177
  getWorkspaces: () => api.getWorkspacesOfUser(undefined),
135
178
  getProjects,
136
179
  getProjectByName: (workspaceId, name) =>
@@ -144,10 +187,11 @@ export class ClockifyApiClient extends Context.Service<ClockifyApiClient, Clocki
144
187
  getTimeEntries: (workspaceId, userId, params) =>
145
188
  api.getTimeEntries(workspaceId, userId, {
146
189
  params: {
147
- ...(params?.start !== undefined ? { start: params.start } : {}),
148
- ...(params?.end !== undefined ? { end: params.end } : {}),
149
- ...(params?.page !== undefined ? { page: params.page } : {}),
150
- ...(params?.pageSize !== undefined ? { "page-size": params.pageSize } : {})
190
+ ...((params?.start !== undefined) && { start: params.start }),
191
+ ...((params?.end !== undefined) && { end: params.end }),
192
+ ...(!(params?.hydrated === undefined) && { hydrated: params.hydrated }),
193
+ ...((params?.page !== undefined) && { page: params.page }),
194
+ ...((params?.pageSize !== undefined) && { "page-size": params.pageSize })
151
195
  }
152
196
  }),
153
197
  getRunningTimer: (workspaceId, userId) =>
@@ -165,7 +209,12 @@ export class ClockifyApiClient extends Context.Service<ClockifyApiClient, Clocki
165
209
  : Effect.succeed(existing)
166
210
  })
167
211
  ),
168
- getTimeEntry: (workspaceId, timeEntryId) => api.getTimeEntry(workspaceId, timeEntryId, undefined),
212
+ getTimeEntry: (workspaceId, timeEntryId, params) =>
213
+ api.getTimeEntry(workspaceId, timeEntryId, {
214
+ params: {
215
+ ...(!(params?.hydrated === undefined) && { hydrated: params.hydrated })
216
+ }
217
+ }),
169
218
  deleteTimeEntry: (workspaceId, timeEntryId) => api.deleteTimeEntry(workspaceId, timeEntryId, undefined),
170
219
  updateTimeEntry: (workspaceId, timeEntryId, payload) =>
171
220
  api.updateTimeEntry(workspaceId, timeEntryId, { payload })
@@ -13,13 +13,13 @@
13
13
  import * as Context from "effect/Context"
14
14
  import type * as Redacted from "effect/Redacted"
15
15
 
16
- export interface ClockifyApiConfigShape {
16
+ export interface ClockifyApiConfigContract {
17
17
  readonly apiKey: Redacted.Redacted<string>
18
18
  readonly workspaceId: string
19
19
  readonly userId: string
20
20
  readonly baseUrl: string
21
21
  }
22
22
 
23
- export class ClockifyApiConfig extends Context.Service<ClockifyApiConfig, ClockifyApiConfigShape>()(
23
+ export class ClockifyApiConfig extends Context.Service<ClockifyApiConfig, ClockifyApiConfigContract>()(
24
24
  "@knpkv/clockify-api-client/ClockifyApiConfig"
25
25
  ) {}
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  export {
8
8
  ClockifyApiClient,
9
- type ClockifyApiClientShape,
9
+ type ClockifyApiClientContract,
10
10
  type ClockifyClientError,
11
11
  layer,
12
12
  make
@@ -16,6 +16,7 @@ export type {
16
16
  AuthenticatedClockifyApi,
17
17
  CreateTimeEntryParams,
18
18
  GetTimeEntriesParams,
19
+ GetTimeEntryParams,
19
20
  Project,
20
21
  StopTimeEntryParams,
21
22
  Tag,
@@ -23,9 +24,10 @@ export type {
23
24
  TimeInterval,
24
25
  UpdateTimeEntryParams,
25
26
  User,
26
- Workspace
27
+ Workspace,
28
+ WorkspaceUser
27
29
  } from "./ClockifyApiClient.js"
28
30
 
29
- export { ClockifyApiConfig, type ClockifyApiConfigShape } from "./ClockifyApiConfig.js"
31
+ export { ClockifyApiConfig, type ClockifyApiConfigContract } from "./ClockifyApiConfig.js"
30
32
 
31
33
  export * as ClockifyApi from "./generated/ClockifyApi.js"
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from "@effect/vitest"
2
2
  import * as Effect from "effect/Effect"
3
3
  import * as Layer from "effect/Layer"
4
+ import * as Predicate from "effect/Predicate"
4
5
  import * as Redacted from "effect/Redacted"
5
6
  import * as HttpClient from "effect/unstable/http/HttpClient"
6
7
  import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
@@ -80,6 +81,107 @@ describe("ClockifyApiClient", () => {
80
81
  )
81
82
  })
82
83
 
84
+ it.effect("requests hydrated time-entry details when required by the caller", () => {
85
+ const requests: Array<HttpClientRequest.HttpClientRequest> = []
86
+ return Effect.gen(function*() {
87
+ const client = yield* ClockifyApiClient
88
+ const entry = yield* client.getTimeEntry("workspace-1", "entry-1", { hydrated: true })
89
+ yield* client.getTimeEntry("workspace-1", "entry-1")
90
+ yield* client.getTimeEntry("workspace-1", "entry-1", { hydrated: false })
91
+ expect(entry.id).toBe("entry-1")
92
+ expect(new Map(requests[0]?.urlParams ?? []).get("hydrated")).toBe("true")
93
+ expect(new Map(requests[1]?.urlParams ?? []).has("hydrated")).toBe(false)
94
+ expect(new Map(requests[2]?.urlParams ?? []).get("hydrated")).toBe("false")
95
+ }).pipe(
96
+ Effect.provide(clientLayer({
97
+ status: 200,
98
+ body: {
99
+ id: "entry-1",
100
+ workspaceId: "workspace-1",
101
+ userId: "user-1",
102
+ description: "Review payment safeguards",
103
+ billable: true,
104
+ customFieldValues: [],
105
+ projectId: "project-1",
106
+ taskId: "task-1",
107
+ tagIds: ["delivery"],
108
+ timeInterval: {
109
+ start: "2026-07-11T08:00:00Z",
110
+ end: "2026-07-11T09:00:00Z",
111
+ duration: "PT1H"
112
+ }
113
+ }
114
+ }, requests))
115
+ )
116
+ })
117
+
118
+ it.effect("forwards hydration when listing canonical time-entry snapshots", () => {
119
+ const requests: Array<HttpClientRequest.HttpClientRequest> = []
120
+ return Effect.gen(function*() {
121
+ const client = yield* ClockifyApiClient
122
+ yield* client.getTimeEntries("workspace-1", "user-1", {
123
+ hydrated: true,
124
+ page: 2,
125
+ pageSize: 10
126
+ })
127
+ expect(new Map(requests[0]?.urlParams ?? []).get("hydrated")).toBe("true")
128
+ expect(new Map(requests[0]?.urlParams ?? []).get("page")).toBe("2")
129
+ expect(new Map(requests[0]?.urlParams ?? []).get("page-size")).toBe("10")
130
+ }).pipe(
131
+ Effect.provide(clientLayer({ status: 200, body: [] }, requests))
132
+ )
133
+ })
134
+
135
+ it.effect("pages through the complete workspace user directory", () => {
136
+ const requests: Array<HttpClientRequest.HttpClientRequest> = []
137
+ const firstPage = Array.from({ length: 500 }, (_, index) => ({
138
+ id: `user-${index}`,
139
+ name: `User ${index}`,
140
+ email: `user-${index}@example.test`,
141
+ status: "ACTIVE"
142
+ }))
143
+ const layer = ClockifyApiClient.layer.pipe(
144
+ Layer.provide(Layer.succeed(ClockifyApiConfig, {
145
+ apiKey: Redacted.make("secret"),
146
+ workspaceId: "workspace-1",
147
+ userId: "user-1",
148
+ baseUrl: "https://clockify.test/api"
149
+ })),
150
+ Layer.provide(Layer.succeed(
151
+ HttpClient.HttpClient,
152
+ HttpClient.make((request) =>
153
+ Effect.sync(() => {
154
+ requests.push(request)
155
+ const body = requests.length <= 20
156
+ ? firstPage
157
+ : [{ id: "configured-user", name: "Configured User", email: "configured@example.test", status: "ACTIVE" }]
158
+ return HttpClientResponse.fromWeb(
159
+ request,
160
+ new Response(JSON.stringify(body), {
161
+ status: 200,
162
+ headers: { "content-type": "application/json" }
163
+ })
164
+ )
165
+ })
166
+ )
167
+ ))
168
+ )
169
+ return Effect.gen(function*() {
170
+ const client = yield* ClockifyApiClient
171
+ if (client.getWorkspaceUsers === undefined) return assert.fail("expected workspace user discovery")
172
+ const users = yield* client.getWorkspaceUsers("workspace-1")
173
+ expect(users).toHaveLength(10_001)
174
+ expect(users.at(-1)?.id).toBe("configured-user")
175
+ expect(requests).toHaveLength(21)
176
+ expect(new Map(requests[0]?.urlParams ?? []).get("page")).toBe("1")
177
+ expect(new Map(requests[20]?.urlParams ?? []).get("page")).toBe("21")
178
+ expect(new Map(requests[0]?.urlParams ?? []).get("account-statuses")).toBe(
179
+ "ACTIVE,PENDING_EMAIL_VERIFICATION,DELETED,NOT_REGISTERED,LIMITED,LIMITED_DELETED"
180
+ )
181
+ expect(new Map(requests[0]?.urlParams ?? []).get("memberships")).toBe("ALL")
182
+ }).pipe(Effect.provide(layer))
183
+ })
184
+
83
185
  it.effect("decodes a created time entry whose optional id references are null", () => {
84
186
  const requests: Array<HttpClientRequest.HttpClientRequest> = []
85
187
  return Effect.gen(function*() {
@@ -172,8 +274,7 @@ describe("ClockifyApiClient", () => {
172
274
  if (requests[0]?.body._tag !== "FormData") throw new Error("Expected a FormData request body")
173
275
  const file = requests[0].body.formData.get("file")
174
276
  expect(requests[0].body.formData).toBeInstanceOf(FormData)
175
- expect(typeof file).not.toBe("string")
176
- if (file === null || typeof file === "string") throw new Error("Expected a file field")
277
+ if (file === null || Predicate.isString(file)) throw new Error("Expected a file field")
177
278
  expect(yield* Effect.promise(() => file.text())).toBe("avatar bytes")
178
279
  expect(requests[0]?.headers["x-api-key"]).toBe("secret")
179
280
  })