chainflow 0.1.0
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 +21 -0
- package/README.md +414 -0
- package/dist/core/chainflow.d.ts +44 -0
- package/dist/core/chainflow.js +113 -0
- package/dist/core/chainflow.js.map +1 -0
- package/dist/core/inputNode.d.ts +41 -0
- package/dist/core/inputNode.js +228 -0
- package/dist/core/inputNode.js.map +1 -0
- package/dist/core/logger.d.ts +3 -0
- package/dist/core/logger.js +15 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/sourceNode.d.ts +24 -0
- package/dist/core/sourceNode.js +37 -0
- package/dist/core/sourceNode.js.map +1 -0
- package/dist/core/store.d.ts +14 -0
- package/dist/core/store.js +91 -0
- package/dist/core/store.js.map +1 -0
- package/dist/core/utils/constants.d.ts +2 -0
- package/dist/core/utils/constants.js +6 -0
- package/dist/core/utils/constants.js.map +1 -0
- package/dist/core/utils/initializers.d.ts +17 -0
- package/dist/core/utils/initializers.js +24 -0
- package/dist/core/utils/initializers.js.map +1 -0
- package/dist/core/utils/link.d.ts +24 -0
- package/dist/core/utils/link.js +35 -0
- package/dist/core/utils/link.js.map +1 -0
- package/dist/core/utils/source.d.ts +13 -0
- package/dist/core/utils/source.js +18 -0
- package/dist/core/utils/source.js.map +1 -0
- package/dist/core/utils/symbols.d.ts +8 -0
- package/dist/core/utils/symbols.js +12 -0
- package/dist/core/utils/symbols.js.map +1 -0
- package/dist/http/endpoint.d.ts +64 -0
- package/dist/http/endpoint.js +233 -0
- package/dist/http/endpoint.js.map +1 -0
- package/dist/http/errors.d.ts +11 -0
- package/dist/http/errors.js +27 -0
- package/dist/http/errors.js.map +1 -0
- package/dist/http/logger.d.ts +3 -0
- package/dist/http/logger.js +15 -0
- package/dist/http/logger.js.map +1 -0
- package/dist/http/originServer.d.ts +28 -0
- package/dist/http/originServer.js +60 -0
- package/dist/http/originServer.js.map +1 -0
- package/dist/http/reqBuilder.d.ts +14 -0
- package/dist/http/reqBuilder.js +50 -0
- package/dist/http/reqBuilder.js.map +1 -0
- package/dist/http/utils/client.d.ts +16 -0
- package/dist/http/utils/client.js +39 -0
- package/dist/http/utils/client.js.map +1 -0
- package/dist/http/utils/constants.d.ts +2 -0
- package/dist/http/utils/constants.js +12 -0
- package/dist/http/utils/constants.js.map +1 -0
- package/dist/http/utils/hash.d.ts +4 -0
- package/dist/http/utils/hash.js +8 -0
- package/dist/http/utils/hash.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 - present Edwin Lim (edwinlzs)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
<h1 align="center" style="border-bottom: none;">🌊hainflow</h1>
|
|
2
|
+
<h3 align="center">Create dynamic and composable API call workflows.</h3>
|
|
3
|
+
|
|
4
|
+
## Not Released Yet
|
|
5
|
+
|
|
6
|
+
Hi! If you are here, you're a bit early. I'm still setting up some stuff for the first release. Check back in later!
|
|
7
|
+
|
|
8
|
+
## Use Cases
|
|
9
|
+
|
|
10
|
+
1. Insert demo data via your app's APIs
|
|
11
|
+
2. Simulate frontend interactions with backend APIs
|
|
12
|
+
3. Test edge cases on endpoints with input variations
|
|
13
|
+
|
|
14
|
+
## Basic Usage
|
|
15
|
+
|
|
16
|
+
```console
|
|
17
|
+
npm install --save-dev chainflow
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Use `originServer` to define your endpoints and their request/response signatures with the `endpoint` method.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { originServer } from chainflow;
|
|
24
|
+
|
|
25
|
+
const origin = originServer('127.0.0.1:5000');
|
|
26
|
+
|
|
27
|
+
const createUser = origin.post('/user').body({
|
|
28
|
+
name: 'Tom',
|
|
29
|
+
details: {
|
|
30
|
+
age: 40,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const createRole = origin.post('/role').body({
|
|
35
|
+
type: 'Engineer',
|
|
36
|
+
userId: createUser.resp.body.id,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const getUser = origin.get('/user').query({
|
|
40
|
+
roleType: createRole.resp.body.type,
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Use a `chainflow` to define a sequence of endpoint calls that take advantage of the values and links provided above.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { chainflow } from Chainflow;
|
|
48
|
+
|
|
49
|
+
const flow = chainflow()
|
|
50
|
+
.call(createUser)
|
|
51
|
+
.call(createRole)
|
|
52
|
+
.call(getUser);
|
|
53
|
+
|
|
54
|
+
flow.run();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
\
|
|
60
|
+
The above setup will result in the following API calls:
|
|
61
|
+
|
|
62
|
+
1. `POST` Request to `/user` with body:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"name": "Tom",
|
|
67
|
+
"details": {
|
|
68
|
+
"age": 40
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
2. `POST` Request to `/role` with body:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"type": "Engineer",
|
|
78
|
+
"userId": "['userId' from response to step 1]"
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
3. `GET` Request to `/user?roleType=['type' from response to step 2]`
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
## More Features
|
|
87
|
+
|
|
88
|
+
### Path params
|
|
89
|
+
|
|
90
|
+
Define path params by wrapping a param name with `{}` in the endpoint path.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const getGroupsWithUser = origin.get('/groups/{userId}');
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Query params
|
|
97
|
+
|
|
98
|
+
Define query params with the `query` method on an endpoint.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const getUsersInGroup = origin.get('/user').query({ groupId: 'some-id' });
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Headers
|
|
105
|
+
|
|
106
|
+
Specify headers with `headers` method on endpoints.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
const getInfo = origin.get('/info').headers({ token: 'some-token' });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
You can also use `headers` on an `OriginServer` to have all endpoints made for that origin bear those headers.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const origin = originServer('127.0.0.1:3001').headers({ token: 'some-token' });
|
|
116
|
+
|
|
117
|
+
const getInfo = origin.get('/info'); // getInfo endpoint will have the headers defined above
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
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.
|
|
121
|
+
|
|
122
|
+
However, you can also use the following features to more flexibly define the values used in a request.
|
|
123
|
+
|
|
124
|
+
### `required`
|
|
125
|
+
|
|
126
|
+
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.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const createUser = origin.post('/user').body({
|
|
130
|
+
name: required(),
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### [_EXPERIMENTAL_] - `pool`
|
|
135
|
+
|
|
136
|
+
Provide a pool of values to take from when building requests. By default, Chainflow will randomly choose a value from the pool for each call in a non-exhaustive manner.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const createUser = origin.post('/user').body({
|
|
140
|
+
name: pool(['Tom', 'Harry', 'Jane']),
|
|
141
|
+
details: {
|
|
142
|
+
age: 40,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `gen`
|
|
148
|
+
|
|
149
|
+
Provide a callback that generates values for building requests.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
const randAge = () => Math.floor(Math.random() * 100);
|
|
153
|
+
|
|
154
|
+
const createUser = origin.post('/user').body({
|
|
155
|
+
name: 'Tom',
|
|
156
|
+
details: {
|
|
157
|
+
age: gen(randAge),
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### `source`
|
|
163
|
+
|
|
164
|
+
Specify a source node with a callback.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const addGreeting = (name: string) => `Hello ${name}`;
|
|
168
|
+
|
|
169
|
+
createNotification.body({
|
|
170
|
+
msg: source(getUser.resp.body.name, addGreeting);
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### `sources`
|
|
175
|
+
|
|
176
|
+
Specify multiple source nodes that a value can be taken from, with an optional callback.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
createNotification.body({
|
|
180
|
+
msg: sources([getUser.resp.body.name, createUser.resp.body.name], addGreeting);
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### `link`
|
|
185
|
+
|
|
186
|
+
Link a response values to a single request node.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
createNotification.set(({ body: { msg } }) => {
|
|
190
|
+
link(msg, getUser.resp.body.name);
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Optionally, you can pass a callback to transform the response value before it is passed to the node.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
createNotification.set(({ body: { msg } }) => {
|
|
198
|
+
link(msg, getUser.resp.body.name, addGreeting);
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### `linkMany`
|
|
203
|
+
|
|
204
|
+
Link multiple response values to a single request node, providing a callback to transform the values into a single output.
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
const mergeValues = ({ userName, favAnimal }: { userName: string; favAnimal: string }) =>
|
|
208
|
+
`${userName} likes ${favAnimal}.`;
|
|
209
|
+
|
|
210
|
+
createNotification.set(({ body: { msg } }) => {
|
|
211
|
+
linkMany(
|
|
212
|
+
msg, // the request node
|
|
213
|
+
// specify which response nodes to take values from and assigns them to a key
|
|
214
|
+
{
|
|
215
|
+
userName: getUser.resp.body.name,
|
|
216
|
+
favAnimal: getFavAnimal.resp.body.favAnimal,
|
|
217
|
+
},
|
|
218
|
+
// callback that takes the response values as its argument
|
|
219
|
+
// and returns a single output value for the request node
|
|
220
|
+
mergeValues,
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Call Options
|
|
226
|
+
|
|
227
|
+
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.
|
|
228
|
+
|
|
229
|
+
`body`, `pathParams`, `query` and `headers` can be set this way.
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
const createUser = origin.post('/user').body({
|
|
233
|
+
name: 'Tom',
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
chainflow()
|
|
237
|
+
.call(createUser, { body: { name: 'Harry' } })
|
|
238
|
+
.run();
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### `seed`
|
|
242
|
+
|
|
243
|
+
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.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { chainflow, link seed, } from 'chainflow';
|
|
247
|
+
|
|
248
|
+
const createUser = origin.post('/user').body({
|
|
249
|
+
name: required(),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
createUser.set(({ body: { name }}) => {
|
|
253
|
+
link(name, seed.username);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
chainflow()
|
|
257
|
+
.call()
|
|
258
|
+
.seed({ username: 'Tom' })
|
|
259
|
+
.run();
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Allow Undefined Sources Values
|
|
263
|
+
|
|
264
|
+
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 `allowUndefined` function, which modifies its properties to inform an input node to use its value regardless of whether the value is `undefined` or not.
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import { allowUndefined } from 'chainflow';
|
|
268
|
+
|
|
269
|
+
createUser.set(({ body: { name } }) => {
|
|
270
|
+
link(name, allowUndefined(seed.username));
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
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.
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
### `clone`
|
|
279
|
+
|
|
280
|
+
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.
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
const initialFlow = chainflow().call(endpoint1).call(endpoint2);
|
|
284
|
+
|
|
285
|
+
const clonedFlow = initialFlow.clone();
|
|
286
|
+
|
|
287
|
+
clonedFlow.call(endpoint3).run(); // calls endpoint 1, 2 and 3
|
|
288
|
+
initialFlow.call(endpoint4).run(); // calls endpoint 1, 2 and 4
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### `extend`
|
|
292
|
+
|
|
293
|
+
You can connect multiple different chainflows together into a longer chainflow using `extend`.
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
const flow1 = chainflow().call(endpoint1).call(endpoint2);
|
|
297
|
+
const flow2 = chainflow().call(endpoint3);
|
|
298
|
+
|
|
299
|
+
flow1.extend(flow2).run(); // calls endpoint 1, 2 and 3
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### `config`
|
|
303
|
+
|
|
304
|
+
`respParser`
|
|
305
|
+
By default, Chainflows will parse response bodies as JSON objects. To change this, 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:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
import { RespParser } from 'chainflow';
|
|
309
|
+
|
|
310
|
+
const getUser = origin.get('/user').config({
|
|
311
|
+
respParser: RespParser.Text,
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
or with camelcase in JavaScript:
|
|
316
|
+
|
|
317
|
+
```javascript
|
|
318
|
+
const getUser = origin.get('/user').config({
|
|
319
|
+
respParser: 'text',
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
There are 4 supported ways to parse response bodies (as provided by the underlying HTTP client, `undici`): `arrayBuffer`, `blob`, `json` and `text`.
|
|
324
|
+
|
|
325
|
+
`respValidator`
|
|
326
|
+
Another configuration option is how to validate the response to an endpoint. By default, Chainflow only accepts responses that have HTTP status codes in the 200-299 range, and rejects responses otherwise (meaning their values will not be stored). You can pass in a custom `respValidator` to change this behaviour.
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
const testEndpoint = origin.get('/user').config({
|
|
330
|
+
respValidator: (resp) => {
|
|
331
|
+
if (resp.statusCode !== 201) return { valid: false, msg: 'Failed to retrieve users.' };
|
|
332
|
+
return { valid: true };
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### `store`
|
|
338
|
+
|
|
339
|
+
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.
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import { store } from 'chainflow';
|
|
343
|
+
|
|
344
|
+
const createUser = origin.post('/user').body({
|
|
345
|
+
name: 'Tom',
|
|
346
|
+
}).store((resp) => ({
|
|
347
|
+
// this endpoint will store `id` from a response to `userId` in the store
|
|
348
|
+
userId: resp.body.id,
|
|
349
|
+
}));
|
|
350
|
+
|
|
351
|
+
const addRole = origin.post('/role').body({
|
|
352
|
+
// this endpoint will take `userId` from the store, if available
|
|
353
|
+
userId: store.userId,
|
|
354
|
+
role: 'Engineer',
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
chainflow().call(createUser).call(addRole).run();
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
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.
|
|
361
|
+
|
|
362
|
+
### `continuesFrom` - transferring Chainflow states
|
|
363
|
+
|
|
364
|
+
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.
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
const createGroup = origin.post('/group').headers({
|
|
368
|
+
Authorization: login.resp.body.authToken,
|
|
369
|
+
}).body({
|
|
370
|
+
groupName: seed.groupName,
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
// loggedInFlow will contain a response from the `login` endpoint
|
|
374
|
+
const loggedInFlow = chainflow()
|
|
375
|
+
.call(login)
|
|
376
|
+
.run();
|
|
377
|
+
|
|
378
|
+
// createGroupFlow will take the response that
|
|
379
|
+
// loggedInFlow received and carry on from there
|
|
380
|
+
const createGroupFlow = chainflow()
|
|
381
|
+
.call(createGroup)
|
|
382
|
+
.continuesFrom(loggedInFlow);
|
|
383
|
+
|
|
384
|
+
const groupNames = ['RapGPT', 'Averageexpedition', 'Shaky Osmosis'];
|
|
385
|
+
for (const groupName in groupNames) {
|
|
386
|
+
createGroupFlow.seed({ groupName }).run();
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
We run a chainflow that calls `login` first to get a response from the login endpoint.
|
|
391
|
+
|
|
392
|
+
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.
|
|
393
|
+
|
|
394
|
+
### `logging`
|
|
395
|
+
|
|
396
|
+
Enable logs from Chainflow by setting `ENABLE_CHAINFLOW_LOGS=true` in your environment variables.
|
|
397
|
+
|
|
398
|
+
## Future Updates
|
|
399
|
+
|
|
400
|
+
Below features are currently not yet supported but are planned in future releases.
|
|
401
|
+
|
|
402
|
+
1. More flexibility to log and return responses
|
|
403
|
+
2. API performance testing
|
|
404
|
+
3. (Exploratory) Possibly some sort of UI/diagram generation
|
|
405
|
+
|
|
406
|
+
## Development
|
|
407
|
+
|
|
408
|
+
Run specific test files:
|
|
409
|
+
|
|
410
|
+
`pnpm run test:file ./src/**/chainflow.test.ts`
|
|
411
|
+
|
|
412
|
+
### Trivia
|
|
413
|
+
|
|
414
|
+
> You probably noticed that I enjoy using the Builder pattern for its clarity.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { SourceValues } from './inputNode';
|
|
2
|
+
import { IStore } from './store';
|
|
3
|
+
export interface CallResult {
|
|
4
|
+
resp: any;
|
|
5
|
+
store: IStore<unknown>;
|
|
6
|
+
}
|
|
7
|
+
/** Defines an endpoint that a chainflow can call upon. */
|
|
8
|
+
export interface IEndpoint {
|
|
9
|
+
hash: string;
|
|
10
|
+
call: (sources: SourceValues, opts?: CallOpts) => Promise<CallResult>;
|
|
11
|
+
}
|
|
12
|
+
/** Options for configuring an endpoint call.
|
|
13
|
+
* @todo to decouple from chainflow in future versions. */
|
|
14
|
+
export interface CallOpts {
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
query?: Record<string, string>;
|
|
17
|
+
pathParams?: Record<string, string>;
|
|
18
|
+
body?: Record<string, any>;
|
|
19
|
+
}
|
|
20
|
+
/** Special object used to link an InputNode to a chainflow seed. */
|
|
21
|
+
export declare const seed: import("./sourceNode").SourceNode;
|
|
22
|
+
/** Special object that acts as a central "gateway" between input and source values. */
|
|
23
|
+
export declare const store: import("./sourceNode").SourceNode;
|
|
24
|
+
export declare class Chainflow {
|
|
25
|
+
#private;
|
|
26
|
+
/** Run the set up chain */
|
|
27
|
+
run(): Promise<this>;
|
|
28
|
+
/** Adds a seed to this chainflow. */
|
|
29
|
+
seed(seed: Record<string, any>): this;
|
|
30
|
+
/** Adds an endpoint call to the callchain. */
|
|
31
|
+
call(endpoint: IEndpoint, opts?: CallOpts): this;
|
|
32
|
+
/** Resets the chainflow's state by clearing its accumulated sources. */
|
|
33
|
+
reset(): void;
|
|
34
|
+
/** Creates a clone of this chainflow's callqueue and initial sources
|
|
35
|
+
* which can be extended and run independently. */
|
|
36
|
+
clone(): Chainflow;
|
|
37
|
+
/** Extends this chainflow's callqueue with that of another flow. */
|
|
38
|
+
extend(cf: Chainflow): this;
|
|
39
|
+
/** Causes this chainflow to continue from the state of
|
|
40
|
+
* sources values of another chainflow. */
|
|
41
|
+
continuesFrom(cf: Chainflow): this;
|
|
42
|
+
/** @todo Returns the accumulated responses of this chainflow. */ responses(): void;
|
|
43
|
+
}
|
|
44
|
+
export declare const chainflow: () => Chainflow;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
12
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
13
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
14
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
15
|
+
};
|
|
16
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
17
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
18
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
19
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
20
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
21
|
+
};
|
|
22
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
23
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
24
|
+
};
|
|
25
|
+
var _Chainflow_sources, _Chainflow_initSources, _Chainflow_callqueue;
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.chainflow = exports.Chainflow = exports.store = exports.seed = void 0;
|
|
28
|
+
const sourceNode_1 = require("./sourceNode");
|
|
29
|
+
const deepmerge_1 = __importDefault(require("@fastify/deepmerge"));
|
|
30
|
+
const logger_1 = require("./logger");
|
|
31
|
+
const constants_1 = require("./utils/constants");
|
|
32
|
+
const deepmerge = (0, deepmerge_1.default)();
|
|
33
|
+
/** Special object used to link an InputNode to a chainflow seed. */
|
|
34
|
+
exports.seed = (0, sourceNode_1.sourceNode)(constants_1.SEED_HASH);
|
|
35
|
+
/** Special object that acts as a central "gateway" between input and source values. */
|
|
36
|
+
exports.store = (0, sourceNode_1.sourceNode)(constants_1.STORE_HASH);
|
|
37
|
+
class Chainflow {
|
|
38
|
+
constructor() {
|
|
39
|
+
/** Stores sources such as the seed or values accumulated from
|
|
40
|
+
* endpoint calls in the current flow. */
|
|
41
|
+
_Chainflow_sources.set(this, {});
|
|
42
|
+
/** Stores the sources that this chainflow was initialized with. */
|
|
43
|
+
_Chainflow_initSources.set(this, {});
|
|
44
|
+
_Chainflow_callqueue.set(this, []);
|
|
45
|
+
}
|
|
46
|
+
/** Run the set up chain */
|
|
47
|
+
run() {
|
|
48
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
49
|
+
(0, logger_1.log)(`Running chainflow...`);
|
|
50
|
+
this.reset();
|
|
51
|
+
__classPrivateFieldSet(this, _Chainflow_sources, __classPrivateFieldGet(this, _Chainflow_initSources, "f"), "f");
|
|
52
|
+
__classPrivateFieldGet(this, _Chainflow_sources, "f")[constants_1.STORE_HASH] = [{}];
|
|
53
|
+
for (const { endpoint, opts } of __classPrivateFieldGet(this, _Chainflow_callqueue, "f")) {
|
|
54
|
+
// call endpoint
|
|
55
|
+
const hash = endpoint.hash;
|
|
56
|
+
(0, logger_1.log)(`Calling endpoint with hash "${hash}"`);
|
|
57
|
+
try {
|
|
58
|
+
const { resp, store } = yield endpoint.call(__classPrivateFieldGet(this, _Chainflow_sources, "f"), opts);
|
|
59
|
+
if (Object.keys(store).length > 0)
|
|
60
|
+
__classPrivateFieldGet(this, _Chainflow_sources, "f")[constants_1.STORE_HASH][0] = deepmerge(__classPrivateFieldGet(this, _Chainflow_sources, "f")[constants_1.STORE_HASH][0], store);
|
|
61
|
+
__classPrivateFieldGet(this, _Chainflow_sources, "f")[hash] = [resp];
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
(0, logger_1.warn)(`Chainflow stopped at endpoint with hash "${hash}" and error: ${e}`);
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
(0, logger_1.log)('Finished running chainflow.');
|
|
69
|
+
return this;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Adds a seed to this chainflow. */
|
|
73
|
+
seed(seed) {
|
|
74
|
+
__classPrivateFieldGet(this, _Chainflow_initSources, "f")[constants_1.SEED_HASH] = [seed];
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
/** Adds an endpoint call to the callchain. */
|
|
78
|
+
call(endpoint, opts) {
|
|
79
|
+
__classPrivateFieldGet(this, _Chainflow_callqueue, "f").push({ endpoint, opts });
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
/** Resets the chainflow's state by clearing its accumulated sources. */
|
|
83
|
+
reset() {
|
|
84
|
+
__classPrivateFieldSet(this, _Chainflow_sources, {}, "f");
|
|
85
|
+
}
|
|
86
|
+
/** Creates a clone of this chainflow's callqueue and initial sources
|
|
87
|
+
* which can be extended and run independently. */
|
|
88
|
+
clone() {
|
|
89
|
+
const clone = new Chainflow();
|
|
90
|
+
__classPrivateFieldSet(clone, _Chainflow_initSources, structuredClone(__classPrivateFieldGet(this, _Chainflow_initSources, "f")), "f");
|
|
91
|
+
__classPrivateFieldSet(clone, _Chainflow_callqueue, [...__classPrivateFieldGet(this, _Chainflow_callqueue, "f")], "f");
|
|
92
|
+
return clone;
|
|
93
|
+
}
|
|
94
|
+
/** Extends this chainflow's callqueue with that of another flow. */
|
|
95
|
+
extend(cf) {
|
|
96
|
+
__classPrivateFieldGet(this, _Chainflow_callqueue, "f").push(...__classPrivateFieldGet(cf, _Chainflow_callqueue, "f"));
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
/** Causes this chainflow to continue from the state of
|
|
100
|
+
* sources values of another chainflow. */
|
|
101
|
+
continuesFrom(cf) {
|
|
102
|
+
__classPrivateFieldSet(this, _Chainflow_initSources, Object.assign(Object.assign({}, __classPrivateFieldGet(this, _Chainflow_initSources, "f")), __classPrivateFieldGet(cf, _Chainflow_sources, "f")), "f");
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
/** @todo Returns the accumulated responses of this chainflow. */ responses() { }
|
|
106
|
+
}
|
|
107
|
+
exports.Chainflow = Chainflow;
|
|
108
|
+
_Chainflow_sources = new WeakMap(), _Chainflow_initSources = new WeakMap(), _Chainflow_callqueue = new WeakMap();
|
|
109
|
+
const chainflow = () => {
|
|
110
|
+
return new Chainflow();
|
|
111
|
+
};
|
|
112
|
+
exports.chainflow = chainflow;
|
|
113
|
+
//# sourceMappingURL=chainflow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chainflow.js","sourceRoot":"","sources":["../../src/core/chainflow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SourceNode } from './sourceNode';
|
|
2
|
+
import { getNodeValue, setSource, setSources, setValuePool } from './utils/symbols';
|
|
3
|
+
/** @experimental How a value pool should choose its values. */
|
|
4
|
+
export declare enum VALUE_POOL_SELECT {
|
|
5
|
+
UNIFORM = 0
|
|
6
|
+
}
|
|
7
|
+
export declare enum NodeValue {
|
|
8
|
+
ValuePool = 0,
|
|
9
|
+
Generator = 1,
|
|
10
|
+
Required = 2,
|
|
11
|
+
Source = 3,
|
|
12
|
+
SourceWithCallback = 4,
|
|
13
|
+
Sources = 5
|
|
14
|
+
}
|
|
15
|
+
type SourceValue = any;
|
|
16
|
+
export type SourceValues = {
|
|
17
|
+
[hash: string]: SourceValue[];
|
|
18
|
+
};
|
|
19
|
+
/** A data node for constructing an input object. */
|
|
20
|
+
export declare class InputNode {
|
|
21
|
+
#private;
|
|
22
|
+
/** Key-values under this node, if this node represents an object. */
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
constructor(val: any);
|
|
25
|
+
/** Sets a source node for this input node. */
|
|
26
|
+
[setSource](source: SourceNode, callback?: (val: any) => any): void;
|
|
27
|
+
/** Sets multiple source nodes to be combined into a single value for this input node */
|
|
28
|
+
[setSources](sources: {
|
|
29
|
+
[key: string]: SourceNode;
|
|
30
|
+
}, callback: (val: any) => any): void;
|
|
31
|
+
/** Sets the pool of values for this input node. */
|
|
32
|
+
[setValuePool](valuePool: any[]): void;
|
|
33
|
+
/** Retrieve value of a node. */
|
|
34
|
+
[getNodeValue](sourceValues: SourceValues, missingValues: string[][], currentPath: string[]): any;
|
|
35
|
+
/**
|
|
36
|
+
* Builds a key-value object from input node values and
|
|
37
|
+
* any available linked sources.
|
|
38
|
+
*/
|
|
39
|
+
buildKvObject(currentPath: string[], missingValues: string[][], sourceValues: SourceValues): any;
|
|
40
|
+
}
|
|
41
|
+
export {};
|