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.
Files changed (3) hide show
  1. package/README.md +276 -16
  2. package/dist/index.js +1 -1
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -2,30 +2,53 @@
2
2
 
3
3
  ![logo](logo.png)
4
4
 
5
- Do notation pipes for Promise-based or pure functions which easy to mock <br/>
6
- Inspired by fp-ts/Effect `bind` Do-notation, but much more small and simple <br/>
7
- Primarly created for easy mocking of functions, which allows to write tons of unit tests using London school approach
5
+ [![npm version](https://badge.fury.io/js/klubok.svg)](https://badge.fury.io/js/klubok)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
7
 
9
- ## Example
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[] }) => { /* fetch cats from DB */ }),
33
+ eff('cats', async ({ ids }: { ids: number[] }) => {
34
+ /* fetch cats from DB */
35
+ }),
16
36
 
17
- pure('catsOneYearOld', ({ cats }) => cats.map(cat => ({ ...cat, age: cat.age + 1 })),
37
+ pure('catsOneYearOld', ({ cats }) =>
38
+ cats.map(cat => ({ ...cat, age: cat.age + 1 }))
39
+ ),
18
40
 
19
- eff('saved', async ({ catsOneYearOld }) => { /* save to DB */ })
41
+ eff('saved', async ({ catsOneYearOld }) => {
42
+ /* save to DB */
43
+ })
20
44
  )
21
45
 
22
- // production usage
46
+ // Production usage
23
47
  catsBirthdays({ ids: [1, 2, 3] })
24
48
 
25
- // in tests usage
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
- ) // Promise<{ ..., catsOneYearOld: [{ name: 'Barsik', age: 11 }, { name: 'Marfa', age: 8 }] }>
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
- ## See also
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
- * [Klubok-Gleam](https://github.com/darky/klubok-gleam) - Gleam implementation of Klubok
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.2",
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": "^24.9.1",
38
- "klubok": "0.5.1",
39
- "tinybench": "^5.0.1",
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
  }