klubok 0.5.2 โ 0.5.4
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/README.md +276 -16
- package/dist/index.js +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,30 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Primarly created for easy mocking of functions, which allows to write tons of unit tests using London school approach
|
|
5
|
+
[](https://badge.fury.io/js/klubok)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
**Do-notation pipes for Promise-based or pure functions with easy mocking.**
|
|
9
|
+
|
|
10
|
+
Inspired by fp-ts/Effect `bind` Do-notation, but smaller and simpler. Primarily created for easy mocking of functions, allowing you to write extensive unit tests using the London school approach.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- ๐ **Sequential composition** โ Chain pure and async functions with automatic context passing
|
|
15
|
+
- ๐งช **Easy mocking** โ Mock any step in the pipeline without complex dependency injection
|
|
16
|
+
- ๐ฏ **Selective execution** โ Run only specific functions using the `only` parameter
|
|
17
|
+
- ๐ก๏ธ **Type-safe** โ Full TypeScript support with inferred types for up to 20 functions
|
|
18
|
+
- ๐ **Debug-friendly** โ Automatic context attachment to errors for easier debugging
|
|
19
|
+
- โก **Mutable state** โ Support for mutable operations when needed
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install klubok
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
10
28
|
|
|
11
29
|
```ts
|
|
12
|
-
import { pure, eff, klubok } from 'klubok'
|
|
30
|
+
import { pure, eff, klubok } from 'klubok'
|
|
13
31
|
|
|
14
32
|
const catsBirthdays = klubok(
|
|
15
|
-
eff('cats', async ({ ids }: { ids: number[] }) => {
|
|
33
|
+
eff('cats', async ({ ids }: { ids: number[] }) => {
|
|
34
|
+
/* fetch cats from DB */
|
|
35
|
+
}),
|
|
16
36
|
|
|
17
|
-
pure('catsOneYearOld', ({ cats }) =>
|
|
37
|
+
pure('catsOneYearOld', ({ cats }) =>
|
|
38
|
+
cats.map(cat => ({ ...cat, age: cat.age + 1 }))
|
|
39
|
+
),
|
|
18
40
|
|
|
19
|
-
eff('saved', async ({ catsOneYearOld }) => {
|
|
41
|
+
eff('saved', async ({ catsOneYearOld }) => {
|
|
42
|
+
/* save to DB */
|
|
43
|
+
})
|
|
20
44
|
)
|
|
21
45
|
|
|
22
|
-
//
|
|
46
|
+
// Production usage
|
|
23
47
|
catsBirthdays({ ids: [1, 2, 3] })
|
|
24
48
|
|
|
25
|
-
//
|
|
49
|
+
// In tests: mock 'cats' and run only 'catsOneYearOld'
|
|
26
50
|
catsBirthdays(
|
|
27
51
|
{ ids: [1, 2, 3] },
|
|
28
|
-
|
|
29
52
|
{
|
|
30
53
|
// DB response mock
|
|
31
54
|
cats: () => [
|
|
@@ -33,14 +56,251 @@ catsBirthdays(
|
|
|
33
56
|
{ name: 'Marfa', age: 7 }
|
|
34
57
|
]
|
|
35
58
|
},
|
|
36
|
-
|
|
37
|
-
// call only this functions
|
|
59
|
+
// call only this function
|
|
38
60
|
['catsOneYearOld']
|
|
61
|
+
)
|
|
62
|
+
// Promise<{ cats: [...], catsOneYearOld: [{ name: 'Barsik', age: 11 }, ...] }>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API Reference
|
|
66
|
+
|
|
67
|
+
### `pure<K, C, R>(key: K, fn: (ctx: C) => R)`
|
|
68
|
+
|
|
69
|
+
Creates a **synchronous** keyed function for the pipeline.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { pure, klubok } from 'klubok'
|
|
73
|
+
|
|
74
|
+
const fn = klubok(
|
|
75
|
+
pure('inc', (ctx: { number: number }) => ctx.number + 1),
|
|
76
|
+
pure('str', ({ inc }) => inc.toString())
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
await fn({ number: 1 })
|
|
80
|
+
// { number: 1, inc: 2, str: '2' }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `eff<K, C, R>(key: K, fn: (ctx: C) => Promise<R>)`
|
|
84
|
+
|
|
85
|
+
Creates an **asynchronous** (Promise-based) keyed function for the pipeline.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { eff, klubok } from 'klubok'
|
|
89
|
+
|
|
90
|
+
const fn = klubok(
|
|
91
|
+
eff('fetchUser', async ({ id }: { id: number }) => {
|
|
92
|
+
return { id, name: 'Alice' }
|
|
93
|
+
}),
|
|
94
|
+
eff('fetchPosts', async ({ user }) => {
|
|
95
|
+
return [{ title: 'Hello' }]
|
|
96
|
+
})
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
await fn({ id: 1 })
|
|
100
|
+
// { id: 1, user: {...}, posts: [...] }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `mut(fn: KeyedFunction)`
|
|
104
|
+
|
|
105
|
+
Marks a function as **mutable**, allowing it to override a previous result with the same key.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { pure, mut, klubok } from 'klubok'
|
|
109
|
+
|
|
110
|
+
const fn = klubok(
|
|
111
|
+
pure('data', () => [1, 2, 3]),
|
|
112
|
+
mut(pure('data', ({ data }) => data.map(x => x * 2)))
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
await fn({})
|
|
116
|
+
// { data: [2, 4, 6] }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `klubok(...fns)`
|
|
120
|
+
|
|
121
|
+
Main function that composes keyed functions into a pipeline. Returns a function with the signature:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
(
|
|
125
|
+
ctx: C,
|
|
126
|
+
mock?: { [key: string]: value | ((ctx) => value | Promise<value>) },
|
|
127
|
+
only?: string[]
|
|
128
|
+
) => Promise<{ ...ctx, ...results }>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Parameters:**
|
|
132
|
+
|
|
133
|
+
| Parameter | Type | Description |
|
|
134
|
+
|-----------|------|-------------|
|
|
135
|
+
| `ctx` | `object` | Initial context (input data) |
|
|
136
|
+
| `mock` | `object` (optional) | Mock implementations for any step. Values can be static or functions |
|
|
137
|
+
| `only` | `string[]` (optional) | Execute only specified keys, skipping others |
|
|
138
|
+
|
|
139
|
+
## Mocking
|
|
140
|
+
|
|
141
|
+
### Static Mocks
|
|
142
|
+
|
|
143
|
+
Replace a function's result with a static value:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const fn = klubok(
|
|
147
|
+
eff('fetchData', async () => {
|
|
148
|
+
/* real API call */
|
|
149
|
+
}),
|
|
150
|
+
pure('process', ({ fetchData }) => fetchData.map(x => x * 2))
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
await fn({}, { fetchData: [1, 2, 3] })
|
|
154
|
+
// { fetchData: [1, 2, 3], process: [2, 4, 6] }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Function Mocks
|
|
158
|
+
|
|
159
|
+
Replace a function with a custom implementation (sync or async):
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
await fn(
|
|
163
|
+
{},
|
|
164
|
+
{
|
|
165
|
+
fetchData: () => [1, 2, 3],
|
|
166
|
+
process: ({ fetchData }) => fetchData.filter(x => x > 1)
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Mocks with Context Access
|
|
172
|
+
|
|
173
|
+
Mock functions receive the accumulated context:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
await fn(
|
|
177
|
+
{ userId: 42 },
|
|
178
|
+
{
|
|
179
|
+
fetchData: ({ userId }) => {
|
|
180
|
+
console.log('Mock called with userId:', userId)
|
|
181
|
+
return [{ id: userId, value: 100 }]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Selective Execution (`only`)
|
|
188
|
+
|
|
189
|
+
Run only specific functions in the pipeline:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const fn = klubok(
|
|
193
|
+
eff('step1', async () => { /* ... */ }),
|
|
194
|
+
pure('step2', ({ step1 }) => step1 * 2),
|
|
195
|
+
eff('step3', async ({ step2 }) => { /* ... */ })
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
// Execute only step1 and step2
|
|
199
|
+
await fn({}, {}, ['step1', 'step2'])
|
|
200
|
+
// { step1: ..., step2: ... } โ step3 is skipped
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Useful for unit testing individual transformations without running the entire pipeline.
|
|
204
|
+
|
|
205
|
+
## Error Handling
|
|
206
|
+
|
|
207
|
+
### Automatic Context Attachment
|
|
208
|
+
|
|
209
|
+
When an error is thrown, Klubok automatically attaches the current context to the error stack:
|
|
39
210
|
|
|
40
|
-
|
|
211
|
+
```ts
|
|
212
|
+
const fn = klubok(
|
|
213
|
+
pure('step1', () => 42),
|
|
214
|
+
eff('step2', async ({ step1 }) => {
|
|
215
|
+
throw new Error('Failed!')
|
|
216
|
+
})
|
|
217
|
+
)
|
|
41
218
|
|
|
219
|
+
try {
|
|
220
|
+
await fn({})
|
|
221
|
+
} catch (e) {
|
|
222
|
+
console.log(e.stack)
|
|
223
|
+
// Error: Failed!
|
|
224
|
+
// at ...
|
|
225
|
+
// context: { step1: 42 }
|
|
226
|
+
}
|
|
42
227
|
```
|
|
43
228
|
|
|
44
|
-
|
|
229
|
+
### Approved Errors
|
|
230
|
+
|
|
231
|
+
Errors with `isApproved = true` property won't have context attached (useful for expected business logic errors):
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
class ApprovedError extends Error {
|
|
235
|
+
isApproved = true
|
|
236
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
237
|
+
super(message, options)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const fn = klubok(
|
|
242
|
+
eff('validate', async () => {
|
|
243
|
+
throw new ApprovedError('Invalid input')
|
|
244
|
+
})
|
|
245
|
+
)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Type Safety
|
|
249
|
+
|
|
250
|
+
Klubok provides full TypeScript inference for pipelines up to 20 functions. Each step's output type is automatically added to the context type for subsequent steps:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
const fn = klubok(
|
|
254
|
+
pure('a', (ctx: { x: number }) => ctx.x + 1), // ctx: { x: number }
|
|
255
|
+
pure('b', ({ a }) => a.toString()), // ctx: { x: number, a: number }
|
|
256
|
+
pure('c', ({ a, b }) => a + b.length) // ctx: { x: number, a: number, b: string }
|
|
257
|
+
)
|
|
258
|
+
// Result: Promise<{ x: number, a: number, b: string, c: number }>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Use Cases
|
|
262
|
+
|
|
263
|
+
### Testing with London School Approach
|
|
264
|
+
|
|
265
|
+
Mock external dependencies at any level without changing production code:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
// production.ts
|
|
269
|
+
export const processOrder = klubok(
|
|
270
|
+
eff('fetchOrder', async ({ orderId }) => db.orders.find(orderId)),
|
|
271
|
+
eff('validateStock', async ({ fetchOrder }) => warehouse.check(fetchOrder)),
|
|
272
|
+
eff('chargePayment', async ({ fetchOrder }) => paymentGateway.charge(fetchOrder)),
|
|
273
|
+
eff('shipItems', async ({ fetchOrder, chargePayment }) => shipping.create(fetchOrder))
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
// test.ts
|
|
277
|
+
await processOrder(
|
|
278
|
+
{ orderId: 123 },
|
|
279
|
+
{
|
|
280
|
+
fetchOrder: () => ({ id: 123, items: ['item1'] }),
|
|
281
|
+
validateStock: () => true
|
|
282
|
+
},
|
|
283
|
+
['chargePayment'] // Test only payment logic
|
|
284
|
+
)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Data Transformation Pipelines
|
|
288
|
+
|
|
289
|
+
Chain pure transformations with clear, testable steps:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
const transformData = klubok(
|
|
293
|
+
pure('parsed', ({ raw }: { raw: string }) => JSON.parse(raw)),
|
|
294
|
+
pure('validated', ({ parsed }) => schema.parse(parsed)),
|
|
295
|
+
pure('normalized', ({ validated }) => normalize(validated)),
|
|
296
|
+
pure('enriched', ({ normalized }) => addMetadata(normalized))
|
|
297
|
+
)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## See Also
|
|
301
|
+
|
|
302
|
+
- [Klubok-Gleam](https://github.com/darky/klubok-gleam) โ Gleam implementation of Klubok
|
|
303
|
+
|
|
304
|
+
## License
|
|
45
305
|
|
|
46
|
-
|
|
306
|
+
MIT ยฉ [Vladislav Botvin](https://github.com/darky)
|
package/dist/index.js
CHANGED
|
@@ -45,7 +45,7 @@ function klubok(...fns) {
|
|
|
45
45
|
}
|
|
46
46
|
catch (error) {
|
|
47
47
|
await onError?.({ ...ctx, $error: error });
|
|
48
|
-
if (error instanceof Error) {
|
|
48
|
+
if (error instanceof Error && !error.isApproved) {
|
|
49
49
|
error.stack += '\ncontext: ' + (0, node_util_1.inspect)(ctx);
|
|
50
50
|
}
|
|
51
51
|
throw error;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klubok",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Do notation pipes for Promise-based or pure functions which easy to mock",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"author": "Vladislav Botvin",
|
|
35
35
|
"license": "MIT",
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@types/node": "^
|
|
38
|
-
"klubok": "0.5.
|
|
39
|
-
"tinybench": "^
|
|
37
|
+
"@types/node": "^25.5.0",
|
|
38
|
+
"klubok": "0.5.3",
|
|
39
|
+
"tinybench": "^6.0.0",
|
|
40
40
|
"tsd": "^0.33.0",
|
|
41
41
|
"typescript": "^5.9.3"
|
|
42
42
|
}
|