@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/CHANGELOG.md +25 -0
- package/dist/ClockifyApiClient.d.ts +10 -3
- package/dist/ClockifyApiClient.d.ts.map +1 -1
- package/dist/ClockifyApiClient.js +35 -5
- package/dist/ClockifyApiClient.js.map +1 -1
- package/dist/ClockifyApiConfig.d.ts +2 -2
- package/dist/ClockifyApiConfig.d.ts.map +1 -1
- package/dist/ClockifyApiConfig.js.map +1 -1
- package/dist/generated/ClockifyApi.d.ts.map +1 -1
- package/dist/generated/ClockifyApi.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/ClockifyApiClient.ts +57 -8
- package/src/ClockifyApiConfig.ts +2 -2
- package/src/index.ts +5 -3
- package/test/ClockifyApiClient.test.ts +103 -2
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
|
-
export { ClockifyApiClient, type
|
|
7
|
-
export type { AuthenticatedClockifyApi, CreateTimeEntryParams, GetTimeEntriesParams, Project, StopTimeEntryParams, Tag, TimeEntry, TimeInterval, UpdateTimeEntryParams, User, Workspace } from "./ClockifyApiClient.js";
|
|
8
|
-
export { ClockifyApiConfig, type
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,iBAAiB,EACjB,KAAK,
|
|
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;
|
|
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.
|
|
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-
|
|
38
|
+
"effect": "4.0.0-rc.109"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@effect/openapi-generator": "4.0.0-
|
|
42
|
-
"@effect/platform-node": "4.0.0-
|
|
43
|
-
"@effect/vitest": "4.0.0-
|
|
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-
|
|
46
|
-
"prettier": "^3.9.
|
|
47
|
-
"tsx": "^4.23.
|
|
48
|
-
"typescript": "
|
|
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": [
|
package/src/ClockifyApiClient.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
148
|
-
...(params?.end !== undefined
|
|
149
|
-
...(params?.
|
|
150
|
-
...(params?.
|
|
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) =>
|
|
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 })
|
package/src/ClockifyApiConfig.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
})
|