chainflow 0.1.6 → 0.1.8
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/LICENSE +20 -20
- package/README.md +565 -488
- package/dist/core/chainflow.d.ts +12 -3
- package/dist/core/chainflow.js +13 -11
- package/dist/core/chainflow.js.map +1 -1
- package/dist/core/inputNode.d.ts +1 -1
- package/dist/core/inputNode.js +31 -29
- package/dist/core/inputNode.js.map +1 -1
- package/dist/core/logger.d.ts +1 -0
- package/dist/core/logger.js +6 -2
- package/dist/core/logger.js.map +1 -1
- package/dist/core/sourceNode.d.ts +5 -5
- package/dist/core/sourceNode.js +5 -5
- package/dist/core/sourceNode.js.map +1 -1
- package/dist/core/utils/constants.d.ts +2 -2
- package/dist/core/utils/constants.js +3 -3
- package/dist/core/utils/constants.js.map +1 -1
- package/dist/core/utils/symbols.d.ts +1 -1
- package/dist/core/utils/symbols.js +2 -2
- package/dist/http/endpoint.d.ts +3 -2
- package/dist/http/endpoint.js +20 -13
- package/dist/http/endpoint.js.map +1 -1
- package/dist/http/errors.d.ts +1 -1
- package/dist/http/errors.js +2 -2
- package/dist/http/errors.js.map +1 -1
- package/dist/http/logger.d.ts +1 -0
- package/dist/http/logger.js +8 -2
- package/dist/http/logger.js.map +1 -1
- package/dist/http/utils/client.d.ts +11 -8
- package/dist/http/utils/client.js +23 -9
- package/dist/http/utils/client.js.map +1 -1
- package/dist/http/utils/id.d.ts +5 -0
- package/dist/http/utils/id.js +9 -0
- package/dist/http/utils/id.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/core/utils/source.d.ts +0 -14
- package/dist/core/utils/source.js +0 -19
- package/dist/core/utils/source.js.map +0 -1
- package/dist/http/utils/hash.d.ts +0 -4
- package/dist/http/utils/hash.js +0 -8
- package/dist/http/utils/hash.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,488 +1,565 @@
|
|
|
1
|
-
<h1 align="center" style="border-bottom: none;">🌊hainflow</h1>
|
|
2
|
-
<h3 align="center">
|
|
3
|
-
<div align="center">
|
|
4
|
-
|
|
5
|
-
[](https://github.com/edwinlzs/chainflow/blob/main/LICENSE)
|
|
6
|
-
|
|
7
|
-
[](https://www.npmjs.com/package/chainflow)
|
|
8
|
-
|
|
9
|
-
[](https://github.com/edwinlzs/chainflow/actions)
|
|
10
|
-
|
|
11
|
-
[](https://codecov.io/gh/edwinlzs/chainflow)
|
|
12
|
-
</div>
|
|
13
|
-
|
|
14
|
-
##
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
##
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
3.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.call(
|
|
69
|
-
.call(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
link(
|
|
205
|
-
});
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
### `
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
```typescript
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const createMessage = origin.post('message').body({
|
|
242
|
-
msg: linkMerge(
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
```
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
1
|
+
<h1 align="center" style="border-bottom: none;">🌊hainflow</h1>
|
|
2
|
+
<h3 align="center">An Open Source library to create dynamic and composable API call workflows.</h3>
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
[](https://github.com/edwinlzs/chainflow/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/chainflow)
|
|
8
|
+
|
|
9
|
+
[](https://github.com/edwinlzs/chainflow/actions)
|
|
10
|
+
|
|
11
|
+
[](https://codecov.io/gh/edwinlzs/chainflow)
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
## Documentation
|
|
15
|
+
|
|
16
|
+
Read the guides over at [Chainflow Docs](https://edwinlzs.github.io/chainflow-docs/) to get started!
|
|
17
|
+
|
|
18
|
+
## When might Chainflow be useful?
|
|
19
|
+
|
|
20
|
+
1. **_Setting up demo data_**
|
|
21
|
+
|
|
22
|
+
Say you have an application that you're developing new features for and you'd like to demonstrate those features. You may need your app to be in a certain context and hence your database in a specific state - perhaps a user has to be logged in with certain permissions, and to have already created a "group" in the app and added other users to that group. You may use raw SQL or other DB scripts to put your DB into that state by inserting users, roles, etc.. However, those scripts could miss out on important side effects relevant to the business context of your app that tend to be built into the services exposed by your backend server. Hence, you can use Chainflow to help compose API call workflows to setup the data in your app by calling the revelant service endpoints you have built e.g. `POST /user`, `POST /role`. You can then minimize your use of database scripts to mainly data that is not configurable with existing endpoints.
|
|
23
|
+
|
|
24
|
+
2. **_Speeding up development_**
|
|
25
|
+
|
|
26
|
+
Similar to setting up demo data, often while coding new features you may want to test out how they behave in your app, and again you may want your app to be in a specific state locally for that. You can write API call workflow scripts built with Chainflow to help move your app into those states quickly.
|
|
27
|
+
|
|
28
|
+
3. **_Testing your endpoints_**
|
|
29
|
+
|
|
30
|
+
An API call workflow could behave as if it were a frontend client calling the backend. In that way, you can create UI-agnostic end-to-end testing of backend endpoints by using API call workflows to simulate how a frontend would interact with the backend.
|
|
31
|
+
|
|
32
|
+
## Basic Usage
|
|
33
|
+
|
|
34
|
+
```console
|
|
35
|
+
npm install --save-dev chainflow
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Use `originServer` to define your endpoints and their request/response signatures with the `endpoint` method.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { originServer } from chainflow;
|
|
42
|
+
|
|
43
|
+
const origin = originServer('127.0.0.1:5000');
|
|
44
|
+
|
|
45
|
+
const createUser = origin.post('/user').body({
|
|
46
|
+
name: 'Tom',
|
|
47
|
+
details: {
|
|
48
|
+
age: 40,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const createRole = origin.post('/role').body({
|
|
53
|
+
type: 'Engineer',
|
|
54
|
+
userId: createUser.resp.body.id,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const getUser = origin.get('/user').query({
|
|
58
|
+
roleType: createRole.resp.body.type,
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Use a `chainflow` to define a sequence of endpoint calls that take advantage of the values and links provided above.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { chainflow } from Chainflow;
|
|
66
|
+
|
|
67
|
+
const flow = chainflow()
|
|
68
|
+
.call(createUser)
|
|
69
|
+
.call(createRole)
|
|
70
|
+
.call(getUser);
|
|
71
|
+
|
|
72
|
+
flow.run();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
\
|
|
78
|
+
The above setup will result in the following API calls:
|
|
79
|
+
|
|
80
|
+
1. `POST` Request to `/user` with body:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"name": "Tom",
|
|
85
|
+
"details": {
|
|
86
|
+
"age": 40
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
2. `POST` Request to `/role` with body:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"type": "Engineer",
|
|
96
|
+
"userId": "['userId' from response to step 1]"
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
3. `GET` Request to `/user?roleType=['type' from response to step 2]`
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
## More Features
|
|
105
|
+
|
|
106
|
+
### Query params
|
|
107
|
+
|
|
108
|
+
Define query params with the `query` method on an endpoint.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
const getUsersInGroup = origin.get('/user').query({ groupId: 'some-id' });
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Path params
|
|
115
|
+
|
|
116
|
+
Define path params by wrapping a param name with `{}` in the endpoint path.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const getGroupsWithUser = origin.get('/groups/{userId}');
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
You can specify values for your path params by calling `pathParams`. Note that path params which do not actually exist in the path will be discarded.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const getGroupsWithUser = origin.get('/groups/{userId}').pathParams({
|
|
126
|
+
userId: 'user123',
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Headers
|
|
131
|
+
|
|
132
|
+
Specify headers with `headers` method on endpoints.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const getInfo = origin.get('/info').headers({ token: 'some-token' });
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
You can also use `headers` on an `OriginServer` to have all endpoints made for that origin bear those headers.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
const origin = originServer('127.0.0.1:3001').headers({ token: 'some-token' });
|
|
142
|
+
|
|
143
|
+
const getInfo = origin.get('/info'); // getInfo endpoint will have the headers defined above
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Default headers
|
|
147
|
+
|
|
148
|
+
Chainflow attaches default headers to all requests made by any endpoint with the value:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
'content-type': 'application/json',
|
|
152
|
+
'User-Agent': 'Chainflow/[major.minor version number]',
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
If you'd like to change this, pass your default headers to the `defaultHeaders` util.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { defaultHeaders } from 'chainflow';
|
|
159
|
+
|
|
160
|
+
defaultHeaders({ 'content-type': 'application/xml' });
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Pass in `true` as the second argument if you want to replace the entire set of default headers. Otherwise, the example above only overwrites the `content-type` default header and keeps `User-Agent`.
|
|
164
|
+
|
|
165
|
+
### Initializing Values
|
|
166
|
+
|
|
167
|
+
The request payloads under `Basic Usage` are defined with only _default_ values - i.e. the values which a Chainflow use if there are no response values from other endpoint calls linked to it.
|
|
168
|
+
|
|
169
|
+
However, you can also use the following features to more flexibly define the values used in a request.
|
|
170
|
+
|
|
171
|
+
### `required`
|
|
172
|
+
|
|
173
|
+
Marks a value as required but without a default. The chainflow will expect this value to be sourced from another node. If no such source is available, the endpoint call will throw an error.
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
const createUser = origin.post('/user').body({
|
|
177
|
+
name: required(),
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### `gen`
|
|
182
|
+
|
|
183
|
+
Provide a callback that generates values for building requests.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
const randAge = () => Math.floor(Math.random() * 100);
|
|
187
|
+
|
|
188
|
+
const createUser = origin.post('/user').body({
|
|
189
|
+
name: 'Tom',
|
|
190
|
+
details: {
|
|
191
|
+
age: gen(randAge),
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `link`
|
|
197
|
+
|
|
198
|
+
You can use the `link` function to specify a callback to transform the response value before it is passed to the input node.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const addGreeting = (name: string) => `Hello ${name}`;
|
|
202
|
+
|
|
203
|
+
const createMessage = origin.post('message').body({
|
|
204
|
+
msg: link(getUser.resp.body.name, addGreeting);
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### `set`
|
|
209
|
+
|
|
210
|
+
The `link` has another function signature.
|
|
211
|
+
|
|
212
|
+
You can use the `set` method on an endpoint to expose its input nodes, then use the 2nd function signature of `link` as shown below: pass in the input node first (`msg`), then the source node second and optionally a callback third.
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
createMessage.set(({ body: { msg } }) => {
|
|
216
|
+
link(msg, getUser.resp.body.name);
|
|
217
|
+
link(msg, createUser.resp.body.name);
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
With a callback:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
createMessage.set(({ body: { msg } }) => {
|
|
225
|
+
link(msg, getUser.resp.body.name, addGreeting);
|
|
226
|
+
link(msg, createUser.resp.body.name, addGreeting);
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### `linkMerge`
|
|
231
|
+
|
|
232
|
+
Link multiple response values to a single request node with an optional callback to merge the values into a single input value. This has 4 function signatures:
|
|
233
|
+
|
|
234
|
+
For the argument containing the source nodes, you can either pass an _array_ of SourceNodes:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// note the callback has an array parameter
|
|
238
|
+
const mergeValues = ([name, favAnimal]: [string, string]) =>
|
|
239
|
+
`${name} likes ${favAnimal}.`;
|
|
240
|
+
|
|
241
|
+
const createMessage = origin.post('message').body({
|
|
242
|
+
msg: linkMerge(
|
|
243
|
+
// array of source nodes
|
|
244
|
+
[getUser.resp.body.name, getFavAnimal.resp.body.favAnimal],
|
|
245
|
+
mergeValues,
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
or you can pass an _object_ with SourceNodes as the values:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
// note the callback has an object parameter
|
|
254
|
+
const mergeValues = ({
|
|
255
|
+
userName,
|
|
256
|
+
favAnimal,
|
|
257
|
+
}: {
|
|
258
|
+
userName: string;
|
|
259
|
+
favAnimal: string;
|
|
260
|
+
}) => `${userName} likes ${favAnimal}.`;
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
const createMessage = origin.post('message').body({
|
|
264
|
+
msg: linkMerge(
|
|
265
|
+
// object of source nodes
|
|
266
|
+
{
|
|
267
|
+
userName: getUser.resp.body.name,
|
|
268
|
+
favAnimal: getFavAnimal.resp.body.favAnimal,
|
|
269
|
+
},
|
|
270
|
+
mergeValues,
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
alternatively, you can use the `set` method in addition with the other function signature of `linkMerge` (similar to how `link` above has overloads to work with `set`).
|
|
276
|
+
|
|
277
|
+
with array:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
createMessage.set(({ body: { msg } }) => {
|
|
281
|
+
linkMerge(
|
|
282
|
+
msg, // the input node
|
|
283
|
+
[getUser.resp.body.name, getFavAnimal.resp.body.favAnimal],
|
|
284
|
+
mergeValues,
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
with object:
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
createMessage.set(({ body: { msg } }) => {
|
|
293
|
+
linkMerge(
|
|
294
|
+
msg, // the input node
|
|
295
|
+
{
|
|
296
|
+
userName: getUser.resp.body.name,
|
|
297
|
+
favAnimal: getFavAnimal.resp.body.favAnimal,
|
|
298
|
+
},
|
|
299
|
+
mergeValues,
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Note that the merging link created by this method will only be used if ALL the source nodes specified are available i.e. if either one of `getUser.resp.body.name` or `getFavAnimal.resp.body.favAnimal` does not have a value, this link will not be used at all.
|
|
305
|
+
|
|
306
|
+
### Call Options
|
|
307
|
+
|
|
308
|
+
You can declare manual values for an endpoint call in the chainflow itself, should you need to do so, by passing in a Call Options object as a second argument in the `call` method.
|
|
309
|
+
|
|
310
|
+
`body`, `pathParams`, `query` and `headers` can be set this way.
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
const createUser = origin.post('/user').body({
|
|
314
|
+
name: 'Tom',
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
chainflow()
|
|
318
|
+
.call(createUser, { body: { name: 'Harry' } })
|
|
319
|
+
.run();
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### `seed`
|
|
323
|
+
|
|
324
|
+
You can specify request nodes to take values from the chainflow 'seed' by importing the `seed` object and linking nodes to it. Provide actual seed values by calling the `seed` method on a chainflow before you `run` it, like below.
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
import { chainflow, link seed, } from 'chainflow';
|
|
328
|
+
|
|
329
|
+
const createUser = origin.post('/user').body({
|
|
330
|
+
name: required(),
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
createUser.set(({ body: { name }}) => {
|
|
334
|
+
link(name, seed.username);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
chainflow()
|
|
338
|
+
.call()
|
|
339
|
+
.seed({ username: 'Tom' })
|
|
340
|
+
.run();
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Allow Undefined Sources Values
|
|
344
|
+
|
|
345
|
+
By default, an input node will reject and skip a source node's value if it is unavailable or `undefined`. However, you can change this by passing a source node into the `config` utility function and passing an options object as the second parameter like below. This informs an input node to use the source node's value regardless of whether the value is `undefined` or not.
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import { config } from 'chainflow';
|
|
349
|
+
|
|
350
|
+
createUser.set(({ body: { name } }) => {
|
|
351
|
+
link(name, config(seed.username, { allowUndefined: true }));
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
This has important implications - it means that as long as the source (e.g. a response from an endpoint call) is available, then the linked source node's value will be taken and used (even if that value is unavailable, which would be taken as `undefined`). Therefore, any other linked sources will not be used UNLESS 1. they have a higher priority or 2. the source providing the linked node that allows `undefined` is unavailable.
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
### `clone`
|
|
360
|
+
|
|
361
|
+
You can create chainflow "templates" with the use of `clone` to create a copy of a chainflow and its endpoint callqueue. The clone can have endpoint calls added to it without modifying the initial flow.
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
const initialFlow = chainflow().call(endpoint1).call(endpoint2);
|
|
365
|
+
|
|
366
|
+
const clonedFlow = initialFlow.clone();
|
|
367
|
+
|
|
368
|
+
clonedFlow.call(endpoint3).run(); // calls endpoint 1, 2 and 3
|
|
369
|
+
initialFlow.call(endpoint4).run(); // calls endpoint 1, 2 and 4
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### `extend`
|
|
373
|
+
|
|
374
|
+
You can connect multiple different chainflows together into a longer chainflow using `extend`.
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
const flow1 = chainflow().call(endpoint1).call(endpoint2);
|
|
378
|
+
const flow2 = chainflow().call(endpoint3);
|
|
379
|
+
|
|
380
|
+
flow1.extend(flow2).run(); // calls endpoint 1, 2 and 3
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### `config`
|
|
384
|
+
|
|
385
|
+
`respParser`
|
|
386
|
+
By default, a chainflow parses response bodies as JSON objects UNLESS the status code is `204` or the `content-type` header does not contain `application/json` (to avoid errors when parsing an empty body), upon which they will instead parse it as text.
|
|
387
|
+
|
|
388
|
+
To set a specific parsing format, you can call `.config` to change that configuration on an `endpoint` (or on an `OriginServer`, to apply it to all endpoints created from it) like so:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
import { RESP_PARSER } from 'chainflow';
|
|
392
|
+
|
|
393
|
+
const getUser = origin.get('/user').config({
|
|
394
|
+
respParser: RESP_PARSER.TEXT,
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
or with camelcase in JavaScript:
|
|
399
|
+
|
|
400
|
+
```javascript
|
|
401
|
+
const getUser = origin.get('/user').config({
|
|
402
|
+
respParser: 'text',
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
There are 4 supported ways to parse response bodies (as provided by the underlying HTTP client, `undici`): `arrayBuffer`, `blob`, `json` and `text`.
|
|
407
|
+
|
|
408
|
+
`respValidator`
|
|
409
|
+
Another configuration option is how to validate the response to an endpoint. By default, Chainflow rejects responses that have HTTP status code 400 and above and throws an error. You can pass in a custom `respValidator` to change when a response is rejected.
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
const getUser = origin.get('/user').config({
|
|
413
|
+
respValidator: (resp) => {
|
|
414
|
+
if (resp.statusCode !== 201) return { valid: false, msg: 'Failed to retrieve users.' };
|
|
415
|
+
if (!Object.keys(resp.body as Record<string, unknown>).includes('id'))
|
|
416
|
+
return { valid: false, msg: 'Response did not provide user ID.' };
|
|
417
|
+
return { valid: true };
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Your custom validator callback should have a return type:
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
{
|
|
426
|
+
valid: boolean; // false if response should be rejected
|
|
427
|
+
msg?: string; // error message
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### `store`
|
|
432
|
+
|
|
433
|
+
Instead of direct links between endpoints, you can use a central store to keep values from some endpoints and have other endpoints take from it via the special `store` object.
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
import { store } from 'chainflow';
|
|
437
|
+
|
|
438
|
+
const createUser = origin
|
|
439
|
+
.post('/user')
|
|
440
|
+
.body({
|
|
441
|
+
name: 'Tom',
|
|
442
|
+
})
|
|
443
|
+
.store((resp) => ({
|
|
444
|
+
// this endpoint will store `id` from a response to `userId` in the store
|
|
445
|
+
userId: resp.body.id,
|
|
446
|
+
}));
|
|
447
|
+
|
|
448
|
+
const addRole = origin.post('/role').body({
|
|
449
|
+
// this endpoint will take `userId` from the store, if available
|
|
450
|
+
userId: store.userId,
|
|
451
|
+
role: 'Engineer',
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
chainflow().call(createUser).call(addRole).run();
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
This is usually useful when you have endpoints that could take a value from any one of many other endpoints for the same input node. Having a store to centralise these many-to-many relationships (like an API Gateway) can improve the developer experience.
|
|
458
|
+
|
|
459
|
+
### `continuesFrom` - transferring Chainflow states
|
|
460
|
+
|
|
461
|
+
Say we have 2 endpoints, `login` and `createGroup`. We want to login as a user once, then proceed to proceed 3 groups as that same user without having to login 3 times.
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
const createGroup = origin
|
|
465
|
+
.post('/group')
|
|
466
|
+
.headers({
|
|
467
|
+
Authorization: login.resp.body.authToken,
|
|
468
|
+
})
|
|
469
|
+
.body({
|
|
470
|
+
groupName: seed.groupName,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// loggedInFlow will contain a response from the `login` endpoint
|
|
474
|
+
const loggedInFlow = chainflow().call(login).run();
|
|
475
|
+
|
|
476
|
+
// createGroupFlow will take the response that
|
|
477
|
+
// loggedInFlow received and carry on from there
|
|
478
|
+
const createGroupFlow = chainflow().call(createGroup).continuesFrom(loggedInFlow);
|
|
479
|
+
|
|
480
|
+
const groupNames = ['RapGPT', 'Averageexpedition', 'Shaky Osmosis'];
|
|
481
|
+
for (const groupName in groupNames) {
|
|
482
|
+
createGroupFlow.seed({ groupName }).run();
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
We run a chainflow that calls `login` first to get a response from the login endpoint.
|
|
487
|
+
|
|
488
|
+
Using the `continuesFrom` method, `createGroupFlow` will copy the state of source values (i.e. responses) from `loggedInFlow`. This means `createGroupFlow` will now have the logged in user's `authToken` received from calling `login`, and will use it when calling `createGroup` thrice for each group name in the `groupNames` array.
|
|
489
|
+
|
|
490
|
+
### `responses`
|
|
491
|
+
|
|
492
|
+
After running a chainflow, you can retrieve the responses received from endpoint calls via the `responses` property on that chainflow.
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
const flow = chainflow().run(createUser).run(getRoles);
|
|
496
|
+
|
|
497
|
+
const responses = flow.responses;
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
The responses will look something like:
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
[
|
|
504
|
+
{
|
|
505
|
+
details: '[POST] /user' // identifies the endpoint called
|
|
506
|
+
val: { // the response to createUser
|
|
507
|
+
statusCode: 200,
|
|
508
|
+
body: ...,
|
|
509
|
+
headers: ...,
|
|
510
|
+
...
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
details: '[GET] /roles'
|
|
515
|
+
val: ... // the response to getRoles
|
|
516
|
+
}
|
|
517
|
+
]
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
The responses in the array follow the order in which the respective endpoints are called.
|
|
521
|
+
|
|
522
|
+
### `logging`
|
|
523
|
+
|
|
524
|
+
Enable logs from Chainflow by setting `ENABLE_CHAINFLOW_LOGS=true` in your environment variables, or by simply importing and calling the `enableLogs` function.
|
|
525
|
+
|
|
526
|
+
### Misc Behaviors
|
|
527
|
+
|
|
528
|
+
- If you have multiple endpoint calls to the same endpoint on one chainflow and they are linked to other endpoints' input nodes further down the flow, the latest endpoint call's values will be used.
|
|
529
|
+
|
|
530
|
+
For example:
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
chainflow().call(getUser).call(addRole).call(getUser).call(createGroup);
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
If an input node on `createGroup` requires a value from a response to `getUser`, then `createGroup` will take that value from the last call to `getUser` (i.e. from the response to the 2nd call to `getUser` that happens _after_ the call to `addRole`).
|
|
537
|
+
|
|
538
|
+
## Future Updates
|
|
539
|
+
|
|
540
|
+
Below features are currently not yet supported but are planned in future releases.
|
|
541
|
+
|
|
542
|
+
1. More flexibility to log and return responses
|
|
543
|
+
2. Conditional calls - execute an endpoint call only if some condition is met.
|
|
544
|
+
3. (Exploratory) API performance measurement
|
|
545
|
+
4. (Exploratory) Possibly some sort of UI/diagram generation
|
|
546
|
+
|
|
547
|
+
## Development
|
|
548
|
+
|
|
549
|
+
### Areas that could be better (non-exhaustive)
|
|
550
|
+
|
|
551
|
+
#### _Encoding endpoint IDs_
|
|
552
|
+
|
|
553
|
+
- Currently assumes that URLs of endpoints do not contain unencoded `|` and `[]` characters. `[]` used to wrap around HTTP method in the encoded ID. Linkmerge uses `|` to separate different encoded IDs.
|
|
554
|
+
- Current implementation also leads to ID collision if multiple endpoints with the same method and path are created (but perhaps with different configuration) and are called on the same chainflow.
|
|
555
|
+
- Idea: Have a centralized service to issue unique IDs to deconflict endpoints - but still somehow encode the method/path info of an endpoint into it.
|
|
556
|
+
|
|
557
|
+
#### _Logging_
|
|
558
|
+
|
|
559
|
+
- Should further explore appropriate degree of detail for logging
|
|
560
|
+
- Truncation of requests/responses with extremely large payloads
|
|
561
|
+
|
|
562
|
+
### Trivia
|
|
563
|
+
|
|
564
|
+
- You probably noticed that I enjoy using the Builder pattern for its clarity.
|
|
565
|
+
- I'm praying the wave 🌊 emoji remains sufficiently shaped like a "C" to avoid confusion. Please let me know if there is some system where it does not!
|