cliodot 1.2.0 → 1.2.2
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 +27 -1628
- package/dist/AuthAppClient.d.ts +29 -0
- package/dist/AuthAppClient.d.ts.map +1 -0
- package/dist/AuthAppClient.js +1 -0
- package/dist/ConnectorBuilder.js +1 -1
- package/dist/Events.d.ts +18 -0
- package/dist/Events.d.ts.map +1 -0
- package/dist/Events.js +1 -0
- package/dist/Flosync.js +1 -1
- package/dist/FlosyncClient.js +1 -1
- package/dist/FunctionBuilder.js +1 -1
- package/dist/OAuthAppClient.d.ts.map +1 -1
- package/dist/OAuthAppClient.js +1 -1
- package/dist/Surface.d.ts +43 -0
- package/dist/Surface.d.ts.map +1 -0
- package/dist/Surface.js +1 -0
- package/dist/ValidatorBuilder.js +1 -1
- package/dist/WorkflowBuilder.js +1 -1
- package/dist/__tests__/Events.test.d.ts +2 -0
- package/dist/__tests__/Events.test.d.ts.map +1 -0
- package/dist/__tests__/Events.test.js +1 -0
- package/dist/__tests__/OAuthAppClient.test.js +1 -1
- package/dist/__tests__/Surface.test.d.ts +2 -0
- package/dist/__tests__/Surface.test.d.ts.map +1 -0
- package/dist/__tests__/Surface.test.js +1 -0
- package/dist/__tests__/errors.test.d.ts +2 -0
- package/dist/__tests__/errors.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test.js +1 -0
- package/dist/__tests__/function.test.js +1 -1
- package/dist/__tests__/parse-api-error.test.js +1 -1
- package/dist/__tests__/runner.test.js +1 -1
- package/dist/connectors/builtin.js +1 -1
- package/dist/connectors/registry.js +1 -1
- package/dist/decorators/build.js +1 -1
- package/dist/decorators/connector.js +1 -1
- package/dist/decorators/index.js +1 -1
- package/dist/decorators/metadata.js +1 -1
- package/dist/decorators/steps.js +1 -1
- package/dist/decorators/workflow.js +1 -1
- package/dist/errors.d.ts +9 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/http/parse-api-error.js +1 -1
- package/dist/http/sanitize-execution-headers.js +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/runner/ProcessorEngine.d.ts.map +1 -1
- package/dist/runner/ProcessorEngine.js +1 -1
- package/dist/runner/connector.executor.d.ts.map +1 -1
- package/dist/runner/connector.executor.js +1 -1
- package/dist/runner/utilities/math.executor.js +1 -1
- package/dist/runner/utilities/random.executor.js +1 -1
- package/dist/runner/utilities/string.executor.js +1 -1
- package/dist/template.js +1 -1
- package/dist/transformers/StepsToNodesTransformer.js +1 -1
- package/dist/transformers/WorkflowTransformer.js +1 -1
- package/dist/types/auth-app.api.d.ts +171 -0
- package/dist/types/auth-app.api.d.ts.map +1 -0
- package/dist/types/auth-app.api.js +1 -0
- package/dist/types/builtin.js +1 -1
- package/dist/types/client.api.js +1 -1
- package/dist/types/connector.js +1 -1
- package/dist/types/context.js +1 -1
- package/dist/types/event-app.api.d.ts +69 -0
- package/dist/types/event-app.api.d.ts.map +1 -0
- package/dist/types/event-app.api.js +1 -0
- package/dist/types/function.js +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/oauth-app.api.d.ts +102 -2
- package/dist/types/oauth-app.api.d.ts.map +1 -1
- package/dist/types/oauth-app.api.js +1 -1
- package/dist/types/validator.js +1 -1
- package/dist/types/workflow.js +1 -1
- package/dist/validators/validators.js +1 -1
- package/dist/variable.js +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# cliodot
|
|
2
2
|
|
|
3
|
-
SDK for building workflows, APIs, and functions with Cliodot Flowsync. Define workflows and functions in code, run them locally, and
|
|
3
|
+
SDK for building workflows, APIs, and functions with Cliodot Flowsync. Define workflows and functions in code, run them locally, and sync with your Cliodot account.
|
|
4
|
+
|
|
5
|
+
**Full documentation:** [docs.cliodot.com](https://docs.cliodot.com)
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
@@ -8,49 +10,25 @@ SDK for building workflows, APIs, and functions with Cliodot Flowsync. Define wo
|
|
|
8
10
|
npm install cliodot
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Cliodot supports both **ES Modules** (recommended for Node.js 16+, TypeScript, and modern frameworks) and **CommonJS**.
|
|
13
|
+
**ES Modules / TypeScript**
|
|
14
14
|
|
|
15
|
-
**ES Modules (ESM) / TypeScript**
|
|
16
15
|
```javascript
|
|
17
16
|
import { flosync, v } from 'cliodot';
|
|
18
|
-
// or: import { flosync, variable as v } from 'cliodot';
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
**CommonJS (CJS)**
|
|
22
|
-
```javascript
|
|
23
|
-
const { flosync, v } = require('cliodot');
|
|
24
|
-
// or: const { flosync, variable: v } = require('cliodot');
|
|
25
17
|
```
|
|
26
18
|
|
|
27
|
-
**
|
|
28
|
-
If you encounter `SyntaxError: Named export 'flosync' not found`, use the default import instead:
|
|
19
|
+
**CommonJS**
|
|
29
20
|
|
|
30
21
|
```javascript
|
|
31
|
-
|
|
32
|
-
const { flosync, v } = pkg;
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Optional peer dependencies:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
npm install mongodb mysql2
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
For PostgreSQL and Redis:
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
npm install pg ioredis
|
|
22
|
+
const { flosync, v } = require('cliodot');
|
|
45
23
|
```
|
|
46
24
|
|
|
47
|
-
|
|
25
|
+
Optional peer dependencies (install only what you use):
|
|
48
26
|
|
|
49
27
|
```bash
|
|
50
|
-
npm install argon2 bcrypt
|
|
28
|
+
npm install mongodb mysql2 pg ioredis argon2 bcrypt
|
|
51
29
|
```
|
|
52
30
|
|
|
53
|
-
## Quick
|
|
31
|
+
## Quick start
|
|
54
32
|
|
|
55
33
|
```javascript
|
|
56
34
|
import { flosync, v } from 'cliodot';
|
|
@@ -72,1619 +50,40 @@ flosync.register(w);
|
|
|
72
50
|
const result = await flosync.run('create-order', { body: { item: 'Widget', qty: 2 } });
|
|
73
51
|
```
|
|
74
52
|
|
|
75
|
-
### Run Payload
|
|
76
|
-
|
|
77
|
-
Pass a `body` (or plain object) as the trigger; the engine treats it as `trigger.body` and `trigger.data`. You can also pass:
|
|
78
|
-
|
|
79
|
-
| Field | Description |
|
|
80
|
-
|-------|-------------|
|
|
81
|
-
| `body` / `data` | Request body; use `v.body('account')`, `v.body('email')` |
|
|
82
|
-
| `headers` | Simulated HTTP headers for the run (`v.header('x-api-key')`); merged with `executionHeaders` (see below) |
|
|
83
|
-
| `executionHeaders` | Extra headers merged after `headers` (same sanitization); useful to separate trigger data from auth headers |
|
|
84
|
-
| `pathParams` | Route params (`v.pathParam('id')`) |
|
|
85
|
-
| `params` | Extra params (`v.param('page')`) |
|
|
86
|
-
| `query` | Query string params (`v.query('filter')`) |
|
|
87
|
-
| `vars` | Override workflow vars (`v.vars('apiKey')`) |
|
|
88
|
-
| `envVars` / `env` | Environment overrides (`v.env('MONGO_URI')`) |
|
|
89
|
-
|
|
90
|
-
```javascript
|
|
91
|
-
flosync.run('my-workflow', { body: { id: 1 } });
|
|
92
|
-
flosync.run('my-workflow', { body: { id: 1 }, headers: { 'x-request-id': 'abc' }, pathParams: { id: '123' }, query: { page: 1 } });
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### Execution headers (local and remote)
|
|
96
|
-
|
|
97
|
-
Workflow steps read `state.headers` (for example `v.header('authorization')`). You control what appears there via the run payload and, for remote runs, the client options below.
|
|
98
|
-
|
|
99
|
-
Local runs (`flosync.run` with a registered workflow): `headers` and `executionHeaders` on the payload are merged (after sanitization) into `state.headers`. You can also pass `executionHeaders` as the third argument to `flosync.run`, which merges on top of the payload.
|
|
100
|
-
|
|
101
|
-
```javascript
|
|
102
|
-
await flosync.run('my-workflow', { body: { id: 1 }, headers: { 'x-request-id': 'a' } }, {
|
|
103
|
-
executionHeaders: { authorization: `Bearer ${integrationToken}` },
|
|
104
|
-
});
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Remote runs (`flosync.run` / `flosync.runById` with `apiKey` and `apiSecret`, or `FlosyncClient`): the SDK sends its own `Authorization` header to the Cliodot API (session JWT). That value is not copied into the workflow `state.headers`, since it is not the same credential your integration steps expect.
|
|
108
|
-
|
|
109
|
-
To give the server workflow the bearer or other headers it was designed for, pass them explicitly:
|
|
110
|
-
|
|
111
|
-
- In the payload: `headers` or `executionHeaders` on the object you pass to `run` / `runById` (both are merged into the test payload the API receives).
|
|
112
|
-
- On the client: `client.workflows.run(groupId, triggerId, { payload, executionHeaders })` — `executionHeaders` is sent in the JSON body and merged last on the server.
|
|
113
|
-
- Webhook test: `client.workflows.runByWebhook(path, method, { body, executionHeaders })`.
|
|
114
|
-
|
|
115
|
-
Merge order on the server is: sanitized real incoming HTTP headers (transport), then `payload.headers`, then `executionHeaders` from the body. Later keys win. Cookies, hop-by-hop headers, `sec-*`, `proxy-*`, and `proxy-authorization` are dropped from user-supplied maps. `authorization` is allowed in `headers` / `executionHeaders` so you can supply an integration bearer; it is still stripped from the raw request line so the Cliodot session token does not overwrite your value.
|
|
116
|
-
|
|
117
|
-
`sanitizeExecutionHeaders` is exported if you want to preview what will be sent; the same rules apply on the server.
|
|
118
|
-
|
|
119
|
-
Public webhooks (unauthenticated URL): the server does not apply an `executionHeaders` field from the JSON body for merging. Use normal request headers or put values in the webhook payload and map them in the workflow.
|
|
120
|
-
|
|
121
53
|
## Configuration
|
|
122
54
|
|
|
123
55
|
```javascript
|
|
124
56
|
flosync.configure({
|
|
125
57
|
apiKey: process.env.FLOSYNC_API_KEY,
|
|
126
58
|
apiSecret: process.env.FLOSYNC_API_SECRET,
|
|
127
|
-
baseUrl:
|
|
59
|
+
baseUrl: process.env.CLIODOT_BASE_URL,
|
|
128
60
|
projectId: process.env.FLOSYNC_PROJECT_ID,
|
|
129
|
-
debug: false, // Enable to include stepResults in every response
|
|
130
61
|
connectors: {
|
|
131
62
|
'mongodb.system': { uri: process.env.MONGO_URI, database: 'myapp' },
|
|
132
|
-
'mysql.system': { uri: process.env.MYSQL_URI },
|
|
133
|
-
'postgres.system': { uri: process.env.PG_URI, database: 'myapp' },
|
|
134
|
-
'redis.system': { url: process.env.REDIS_URL },
|
|
135
|
-
'cliodot': { apiKey: process.env.CLIODOT_API_KEY },
|
|
136
|
-
},
|
|
137
|
-
cliodot: { baseUrl: 'https://api.example.com', apiKey: process.env.CLIODOT_API_KEY },
|
|
138
|
-
});
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
`baseUrl` defaults to `http://localhost:8080` when omitted. For remote runs (workflows, functions, connectors), only `apiKey` and `apiSecret` are required. Set `CLIODOT_BASE_URL` (or `baseUrl`) to match your main Cliodot API server port.
|
|
142
|
-
|
|
143
|
-
## Workflows
|
|
144
|
-
|
|
145
|
-
### Triggers (optional)
|
|
146
|
-
|
|
147
|
-
Triggers are optional. If you wrap workflows in your own HTTP handler, omit `.http()` and call `flosync.run()` with your payload.
|
|
148
|
-
|
|
149
|
-
| Method | Description |
|
|
150
|
-
|--------|-------------|
|
|
151
|
-
| `.http(method, path)` | HTTP trigger (e.g. `POST /orders`) |
|
|
152
|
-
| `.job(cron?, timezone?)` | Job/cron trigger (e.g. `'0 * * * *'`, `'UTC'`) |
|
|
153
|
-
| `.schedule(cron, timezone?)` | Alias for scheduled jobs |
|
|
154
|
-
| `.webhook(method, path)` | Alias for `.http()` |
|
|
155
|
-
|
|
156
|
-
```javascript
|
|
157
|
-
flosync.workflow('api-order').http('POST', '/orders').step(...).build();
|
|
158
|
-
flosync.workflow('daily-job').job('0 0 * * *', 'UTC').step(...).build();
|
|
159
|
-
flosync.workflow('manual').step(...).build();
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### Variable references
|
|
163
|
-
|
|
164
|
-
Use `variable` (or `v`) instead of typing `{{ }}`:
|
|
165
|
-
|
|
166
|
-
| Method | Example | Resolves to |
|
|
167
|
-
|--------|---------|-------------|
|
|
168
|
-
| `v.body(...path)` | `v.body('amount')`, `v.body('user', 'email')` | `{{ body.amount }}`, `{{ body.user.email }}` |
|
|
169
|
-
| `v.trigger(...path)` | `v.trigger('email')` | `{{ trigger.email }}` |
|
|
170
|
-
| `v.stepResult(stepId, ...path)` | `v.stepResult('charge', 'id')` | `{{ stepResults.charge.id }}` |
|
|
171
|
-
| `v.header(key)` | `v.header('x-api-key')` | `{{ headers["x-api-key"] }}` |
|
|
172
|
-
| `v.param(key)` | `v.param('page')` | `{{ params["page"] }}` |
|
|
173
|
-
| `v.pathParam(key)` | `v.pathParam('id')` | `{{ pathParams["id"] }}` |
|
|
174
|
-
| `v.query(key)` | `v.query('filter')` | `{{ query["filter"] }}` |
|
|
175
|
-
| `v.vars(key)` | `v.vars('apiKey')` | `{{ vars["apiKey"] }}` |
|
|
176
|
-
| `v.env(key)` | `v.env('MONGO_URI')` | `{{ env["MONGO_URI"] }}` |
|
|
177
|
-
| `v.args(...path)` | `v.args('amount')` | `{{ args.amount }}` |
|
|
178
|
-
| `v.expr(expression)` | `v.expr('length(body.items) * 10')` | `{{ length(body.items) * 10 }}` |
|
|
179
|
-
|
|
180
|
-
```javascript
|
|
181
|
-
import { flosync, v } from 'cliodot';
|
|
182
|
-
|
|
183
|
-
s.validator({ amount: v.body('amount'), email: v.body('email') });
|
|
184
|
-
s.transform({ total: v.stepResult('calculate', 'total'), id: v.stepResult('charge', 'id') });
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### Typed `stepResult` paths (`defineStepVars`)
|
|
188
|
-
|
|
189
|
-
`v.stepResult(stepId, ...pathParts)` treats every segment as a plain `string`, so your editor cannot suggest valid fields from a connector response or from another step’s output.
|
|
190
|
-
|
|
191
|
-
Use **`defineStepVars`** to describe, once per workflow file, a map from **step id** (the decorated **method name**) to that step’s **result type**. The returned **`stepResult`** then constrains the first argument to those keys and each following argument to the next key along a valid path into that type (nested objects supported up to a fixed depth). Import **`ConnectorResponse`** (and your **`defineTypedConnector`** value) so connector steps reuse the same response shape you already declared for typing.
|
|
192
|
-
|
|
193
|
-
```typescript
|
|
194
|
-
import { defineStepVars, v, defineTypedConnector } from "cliodot";
|
|
195
|
-
import type { ConnectorResponse } from "cliodot";
|
|
196
|
-
|
|
197
|
-
const Api = defineTypedConnector("my-api", { pay: "pay" }, {
|
|
198
|
-
pay: {
|
|
199
|
-
body: {} as { amount: number },
|
|
200
|
-
response: { status: "", data: { id: "", link: "" } },
|
|
201
|
-
},
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
const steps = defineStepVars<{
|
|
205
|
-
pay: ConnectorResponse<typeof Api, "pay">;
|
|
206
|
-
validate: { errors?: Record<string, unknown> };
|
|
207
|
-
}>();
|
|
208
|
-
|
|
209
|
-
steps.stepResult("pay", "data", "link");
|
|
210
|
-
steps.stepResult("validate", "errors");
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
The type **`StepKeyPath<T>`** is exported if you want to reuse the path-tuple logic elsewhere. Plain **`v.stepResult`** remains available for ad hoc strings and for code that does not need completions.
|
|
214
|
-
|
|
215
|
-
### Class decorators (`buildWorkflowFromClass`)
|
|
216
|
-
|
|
217
|
-
For `@Connector`, you can target a connector that exists only on Flowsync in three ways: pass the published connector id and action as strings (`@Connector('my-connector', 'charge', { installation_id: '...', connector_version: '1.2.0' })`); use `defineCustomConnector` / `defineCustomConnectorFromList` with that same id; or use `defineRemoteConnectorRef('my-connector', { pay: 'charge' })` and `@Connector(MyRef, 'pay', config)` for a clearer name. With `apiKey` and `apiSecret` set, the local runner loads REST definitions through `client.connectors.get` and respects `connector_version` on the step when fetching.
|
|
218
|
-
|
|
219
|
-
**`@CallWorkflow` by webhook path** — Use `@CallWorkflow({ webhookPath: '/orders', webhookMethod: 'POST' }, payload)` where `webhookPath` and `webhookMethod` match the child workflow’s HTTP trigger (same values as `@Http('POST', '/orders')` on that workflow). You do **not** pass a workflow group id for this mode: the platform finds the child by path and method for the tenant. When the local runner calls Flowsync with `apiKey` / `apiSecret`, it uses **`POST /workflows/test/webhook`** with a JSON body `{ webhookPath, webhookMethod, payload, ... }` (`client.workflows.runByWebhook`). On the server, `call_workflow` resolves the child the same way as a real webhook (including route patterns) and merges path params into the child’s `pathParams`. The second decorator argument is the child payload; optional fields on that object include `environment` (`dev` / `prod`), `executionHeaders`, and `remote: true` to skip the in-memory registry and always hit the API. `webhookPath` / `webhookMethod` may be templated with `{{ }}` and are rendered before lookup.
|
|
220
|
-
|
|
221
|
-
**`@CallWorkflow` by group id** — `@CallWorkflow('my-group-id', payload)` runs the child via **`POST /workflows/:groupId/test/trigger/:triggerId`** (see `client.workflows.run`). Use this when you target a specific group and trigger explicitly. You can combine `workflow_id` with a webhook path in the serialized step only when you need both; the path-only flow above stays independent of group ids.
|
|
222
|
-
|
|
223
|
-
### Step Types
|
|
224
|
-
|
|
225
|
-
- `connector(id, action, config)` - REST/API call (optional `connector_version` in `config` for versioned tenant connectors when syncing workflows to Flowsync)
|
|
226
|
-
- `db(engine, action, config)` - MongoDB, MySQL, PostgreSQL, or Redis (optional `connector_version` when relevant)
|
|
227
|
-
- `validator(fields)` - Validate input
|
|
228
|
-
- `condition(expr)` - Branch on expression (use `.then()` and `.else()`)
|
|
229
|
-
- `transform(mapping)` - Map or transform data
|
|
230
|
-
- `call(slug, args)` - Invoke a function
|
|
231
|
-
- `auth(connectorId, action, config)` - Authentication
|
|
232
|
-
- `util(connectorId, action, config)` - Utility (see [Utilities](#utilities))
|
|
233
|
-
- `encrypt(connectorId, action, config)` - Encryption (see [Encryption](#encryption))
|
|
234
|
-
- `responder(type, config)` - Return response (json, http, raw, redirect, empty)
|
|
235
|
-
- `log(message)` - Log message
|
|
236
|
-
- `delay(ms)` - Delay execution
|
|
237
|
-
|
|
238
|
-
### Branching
|
|
239
|
-
|
|
240
|
-
Steps that can succeed or fail support `.then()` and `.else()`:
|
|
241
|
-
|
|
242
|
-
```javascript
|
|
243
|
-
.step('validate', s => s.validator({ amount: '{{ trigger.body.amount }}' })
|
|
244
|
-
.then('process')
|
|
245
|
-
.else('error'))
|
|
246
|
-
.step('check', s => s.condition('{{ stepResults.validate.amount > 0 }}')
|
|
247
|
-
.then('create')
|
|
248
|
-
.else('invalid'))
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
### Decorator Workflows
|
|
252
|
-
|
|
253
|
-
You can define workflows using TypeScript decorators instead of the fluent `.workflow().step()` builder.
|
|
254
|
-
|
|
255
|
-
1. Enable decorators in your consuming project's `tsconfig.json`:
|
|
256
|
-
|
|
257
|
-
```json
|
|
258
|
-
{
|
|
259
|
-
"compilerOptions": {
|
|
260
|
-
"experimentalDecorators": true
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
2. Define a workflow class and decorate it with `@Workflow()` and `@Http()` (or `@Job()` / `@Schedule()`).
|
|
266
|
-
|
|
267
|
-
3. Decorate class methods to create steps. By default, the step ID is the method name.
|
|
268
|
-
|
|
269
|
-
4. Build and register the workflow with `buildWorkflowFromClass()` or `flosync.registerFromClass()`.
|
|
270
|
-
|
|
271
|
-
5. Optional: add JavaScript inside the decorated method body.
|
|
272
|
-
|
|
273
|
-
If a decorated method contains non-empty code, the SDK uses it based on the step decorator option `stage`:
|
|
274
|
-
|
|
275
|
-
- `stage: "pre"` (default): injects an extra `code` step right before the decorated step. The injected code receives `ctx` with:
|
|
276
|
-
|
|
277
|
-
- `ctx.input` (previous step output, or trigger data for the first step)
|
|
278
|
-
- `ctx.trigger` (trigger payload)
|
|
279
|
-
- `ctx.steps` (all previous `stepResults`)
|
|
280
|
-
- `ctx.vars` (workflow vars, including anything set via `setVars`)
|
|
281
|
-
|
|
282
|
-
To pass values to later steps, return an object with `setVars`:
|
|
283
|
-
|
|
284
|
-
```typescript
|
|
285
|
-
return { setVars: { limit: 10 } };
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
Later templates can read them via `v.vars("limit")` (resolves to `{{ vars["limit"] }}`) or `{{ vars.limit }}`.
|
|
289
|
-
|
|
290
|
-
- `stage: "post"`: executes the method body after the decorated step finishes, and allows overriding the decorated step output directly.
|
|
291
|
-
|
|
292
|
-
In this mode, return:
|
|
293
|
-
|
|
294
|
-
```typescript
|
|
295
|
-
return { out: <newOutput>, setVars?: { ... } };
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
The overridden output becomes the value of `v.stepResult("<stepId>")` for subsequent steps.
|
|
299
|
-
|
|
300
|
-
In `stage: "post"`, `ctx.input` is the decorated step output produced by the connector/function call.
|
|
301
|
-
|
|
302
|
-
#### `stage: "post"` in depth
|
|
303
|
-
|
|
304
|
-
`post` is the hook that runs **after** the decorated step’s primary work finishes. For `@Connector`, `@Db`, and other steps that call out to a runtime implementation, the sequence is: resolve inputs → run the connector or database action (or built-in executor) → take the returned payload as the step output → **then** run your method body with `ctx.input` set to that output. Anything you return as `{ out }` replaces what gets stored in `stepResults` for that step id, so the rest of the workflow sees your transformed value. Use `setVars` in the same return value to publish workflow variables without changing the step output. Failures and HTTP error handling still apply to the primary step; the post hook is for normalization (mapping provider DTOs to your domain shape), redacting fields, enriching derived properties, or turning awkward API responses into a stable contract for later steps and responders. `@Step` defaults to `stage: "post"` so standalone code steps behave like “compute output from prior context” without injecting an extra predecessor code step.
|
|
305
|
-
|
|
306
|
-
### `WorkflowContext<TInput, TSteps, TVars>`
|
|
307
|
-
|
|
308
|
-
Instead of using `ctx: any`, you can type the context object passed to step methods. Import `WorkflowContext` from `cliodot`:
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
|
-
import type { WorkflowContext } from "cliodot";
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
`WorkflowContext` is generic with three optional type parameters:
|
|
315
|
-
|
|
316
|
-
| Parameter | Default | Description |
|
|
317
|
-
|-----------|---------|-------------|
|
|
318
|
-
| `TInput` | `Record<string, any>` | Shape of `ctx.input` (previous step output or trigger data) |
|
|
319
|
-
| `TSteps` | `Record<string, any>` | Shape of `ctx.steps` (accumulated step results) |
|
|
320
|
-
| `TVars` | `Record<string, any>` | Shape of `ctx.vars` (workflow-level variables) |
|
|
321
|
-
|
|
322
|
-
The full context object includes:
|
|
323
|
-
|
|
324
|
-
| Property | Type | Description |
|
|
325
|
-
|----------|------|-------------|
|
|
326
|
-
| `input` | `TInput` | Input data for this step |
|
|
327
|
-
| `trigger` | `any` | Raw trigger payload |
|
|
328
|
-
| `steps` | `TSteps` | All step results keyed by step ID |
|
|
329
|
-
| `vars` | `TVars` | Workflow variables (read/write via `setVars`) |
|
|
330
|
-
| `headers` | `Record<string, string>` | HTTP headers from the request |
|
|
331
|
-
| `params` | `Record<string, any>` | Merged query + route params |
|
|
332
|
-
| `pathParams` | `Record<string, any>` | URL path parameters |
|
|
333
|
-
| `query` | `Record<string, any>` | Query string parameters |
|
|
334
|
-
| `body` | `any` | Raw request body |
|
|
335
|
-
| `env` | `Record<string, string>` | Environment variables |
|
|
336
|
-
|
|
337
|
-
The interface also has an index signature (`[key: string]: any`) so users can attach custom properties.
|
|
338
|
-
|
|
339
|
-
### `StepOutput<T>`
|
|
340
|
-
|
|
341
|
-
The return type for step methods that produce output. Import from `cliodot`:
|
|
342
|
-
|
|
343
|
-
```typescript
|
|
344
|
-
import type { StepOutput } from "cliodot";
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
```typescript
|
|
348
|
-
export interface StepOutput<T = any> {
|
|
349
|
-
/** The output payload — stored in stepResults[stepId] */
|
|
350
|
-
out: T;
|
|
351
|
-
/** Optional: update workflow variables */
|
|
352
|
-
setVars?: Record<string, any>;
|
|
353
|
-
}
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
Usage:
|
|
357
|
-
|
|
358
|
-
```typescript
|
|
359
|
-
@Connector(MyStore.id, MyStore.actions.list, {}, { order: 0, stage: "post" })
|
|
360
|
-
list(ctx: WorkflowContext<{ items: Item[] }>): StepOutput<{ sorted: Item[] }> {
|
|
361
|
-
const sorted = [...ctx.input.items].sort((a, b) => a.id - b.id);
|
|
362
|
-
return { out: { sorted } };
|
|
363
|
-
}
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
> **Note:** `StepOutput` is different from the `StepResult` exported by `WorkflowBuilder` (which is the builder-pattern return type with `.then()` / `.else()` chaining). Use `StepOutput` for decorator workflow method return types.
|
|
367
|
-
|
|
368
|
-
### `@Step` — Custom Step Decorator
|
|
369
|
-
|
|
370
|
-
`@Step` is a generic decorator for writing custom workflow step logic without needing to pair it with a connector, database, or other built-in step type. The method body is extracted at build time and executed as a code step with access to the full `WorkflowContext`.
|
|
371
|
-
|
|
372
|
-
```typescript
|
|
373
|
-
import { Workflow, Http, Step, Responder, buildWorkflowFromClass, v } from "cliodot";
|
|
374
|
-
import type { WorkflowContext, StepOutput } from "cliodot";
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
**Options** (same as other step decorators):
|
|
378
|
-
|
|
379
|
-
| Option | Type | Default | Description |
|
|
380
|
-
|--------|------|---------|-------------|
|
|
381
|
-
| `id` | `string` | method name | Custom step ID |
|
|
382
|
-
| `order` | `number` | auto | Execution order |
|
|
383
|
-
| `stage` | `"pre" \| "post"` | `"post"` | When the method body runs |
|
|
384
|
-
| `then` | `string` | — | Step ID to jump to on success |
|
|
385
|
-
| `else` | `string` | — | Step ID to jump to on failure |
|
|
386
|
-
|
|
387
|
-
**Basic example:**
|
|
388
|
-
|
|
389
|
-
```typescript
|
|
390
|
-
@Workflow("greet-user", "Greet User")
|
|
391
|
-
@Http("POST", "/greet")
|
|
392
|
-
class GreetWorkflow {
|
|
393
|
-
@Step({ order: 0 })
|
|
394
|
-
greet(ctx: WorkflowContext<{ name: string }>): StepOutput<{ greeting: string }> {
|
|
395
|
-
const name = ctx.input.name || "World";
|
|
396
|
-
return { out: { greeting: `Hello, ${name}!` } };
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
@Responder("json", { body: { message: v.stepResult("greet", "greeting") } }, { order: 1 })
|
|
400
|
-
respond() {}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
export const greetWorkflow = buildWorkflowFromClass(GreetWorkflow);
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
**With `setVars` and branching:**
|
|
407
|
-
|
|
408
|
-
```typescript
|
|
409
|
-
@Workflow("process-order", "Process Order")
|
|
410
|
-
@Http("POST", "/orders/process")
|
|
411
|
-
class ProcessOrderWorkflow {
|
|
412
|
-
@Step({ order: 0, then: "finalize", else: "reject" })
|
|
413
|
-
validate(ctx: WorkflowContext<{ amount: number; currency: string }>): StepOutput<{ valid: boolean }> {
|
|
414
|
-
const { amount, currency } = ctx.input;
|
|
415
|
-
const valid = amount > 0 && ["USD", "EUR", "GBP"].includes(currency);
|
|
416
|
-
return {
|
|
417
|
-
out: { valid },
|
|
418
|
-
setVars: { processedAt: new Date().toISOString() },
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
@Responder("json", { body: { ok: true, processedAt: v.vars("processedAt") } }, { order: 1 })
|
|
423
|
-
finalize() {}
|
|
424
|
-
|
|
425
|
-
@Responder("json", { statusCode: 400, body: { ok: false, error: "Invalid order" } }, { order: 2 })
|
|
426
|
-
reject() {}
|
|
427
|
-
}
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
**Combining `@Step` with other decorators:**
|
|
431
|
-
|
|
432
|
-
`@Step` can be freely combined with `@Connector`, `@Db`, `@Condition`, `@Responder`, etc. in the same workflow class:
|
|
433
|
-
|
|
434
|
-
```typescript
|
|
435
|
-
@Workflow("mixed-workflow", "Mixed Workflow")
|
|
436
|
-
@Http("POST", "/mixed")
|
|
437
|
-
class MixedWorkflow {
|
|
438
|
-
@Step({ order: 0 })
|
|
439
|
-
prepare(ctx: WorkflowContext<{ email: string }>): StepOutput<{ normalized: string }> {
|
|
440
|
-
return { out: { normalized: ctx.input.email.toLowerCase().trim() } };
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
@Db("mongodb", "findOne", { collection: "users", query: { email: v.stepResult("prepare", "normalized") } }, { order: 1 })
|
|
444
|
-
findUser() {}
|
|
445
|
-
|
|
446
|
-
@Condition(v.expr("stepResults.findUser != null"), { then: "found", else: "notFound", order: 2 })
|
|
447
|
-
check() {}
|
|
448
|
-
|
|
449
|
-
@Responder("json", { body: { user: v.stepResult("findUser") } }, { order: 3 })
|
|
450
|
-
found() {}
|
|
451
|
-
|
|
452
|
-
@Responder("json", { statusCode: 404, body: { error: "User not found" } }, { order: 4 })
|
|
453
|
-
notFound() {}
|
|
454
|
-
}
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
### Decorator example: list todos with typed context
|
|
458
|
-
|
|
459
|
-
```typescript
|
|
460
|
-
import { flosync, v, Workflow, Http, Connector, Condition, Responder, buildWorkflowFromClass, defineCustomConnector } from "cliodot";
|
|
461
|
-
import type { WorkflowContext, StepOutput } from "cliodot";
|
|
462
|
-
|
|
463
|
-
const CarrierStore = defineCustomConnector("carrier.store", {
|
|
464
|
-
listTodos: "listTodos",
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
interface TodoItem {
|
|
468
|
-
title?: string;
|
|
469
|
-
completed?: boolean;
|
|
470
|
-
createdAt?: string;
|
|
471
|
-
[key: string]: any;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
interface ListInput {
|
|
475
|
-
todos?: TodoItem[];
|
|
476
|
-
[key: string]: any;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
interface ListOutput {
|
|
480
|
-
todos: Array<TodoItem & { title_upper: string; completed_label: string }>;
|
|
481
|
-
completedTodos: TodoItem[];
|
|
482
|
-
pendingTodos: TodoItem[];
|
|
483
|
-
stats: { total: number; completed: number; pending: number };
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
@Workflow("todos-list-decorators", "Todos List (Decorators)")
|
|
487
|
-
@Http("GET", "/todos")
|
|
488
|
-
class TodosListDecoratorsWorkflow {
|
|
489
|
-
@Connector(
|
|
490
|
-
CarrierStore.id,
|
|
491
|
-
CarrierStore.actions.listTodos,
|
|
492
|
-
{ params: { limit: 10 } },
|
|
493
|
-
{ order: 0, stage: "post" }
|
|
494
|
-
)
|
|
495
|
-
list(ctx: WorkflowContext<ListInput>): StepOutput<ListOutput> {
|
|
496
|
-
const input = ctx?.input ?? {};
|
|
497
|
-
const todos = Array.isArray(input.todos) ? input.todos : [];
|
|
498
|
-
const sorted = [...todos].sort((a, b) =>
|
|
499
|
-
new Date(b?.createdAt ?? 0).getTime() - new Date(a?.createdAt ?? 0).getTime()
|
|
500
|
-
);
|
|
501
|
-
const completedTodos = sorted.filter((t) => Boolean(t?.completed));
|
|
502
|
-
const pendingTodos = sorted.filter((t) => !Boolean(t?.completed));
|
|
503
|
-
|
|
504
|
-
const decoratedTodos = sorted.map((t) => {
|
|
505
|
-
const title = String(t?.title ?? "").trim();
|
|
506
|
-
return {
|
|
507
|
-
...t,
|
|
508
|
-
title,
|
|
509
|
-
title_upper: title.toUpperCase(),
|
|
510
|
-
completed_label: t?.completed ? "done" as const : "todo" as const,
|
|
511
|
-
};
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
return {
|
|
515
|
-
out: {
|
|
516
|
-
...input,
|
|
517
|
-
todos: decoratedTodos,
|
|
518
|
-
completedTodos,
|
|
519
|
-
pendingTodos,
|
|
520
|
-
stats: {
|
|
521
|
-
total: sorted.length,
|
|
522
|
-
completed: completedTodos.length,
|
|
523
|
-
pending: pendingTodos.length,
|
|
524
|
-
},
|
|
525
|
-
},
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
@Condition(v.stepResult("list", "valid"), { then: "ok", else: "unauth", order: 1 })
|
|
530
|
-
check() {}
|
|
531
|
-
|
|
532
|
-
@Responder("json", { body: { todos: v.stepResult("list", "todos") } }, { order: 2 })
|
|
533
|
-
ok() {}
|
|
534
|
-
|
|
535
|
-
@Responder("json", { statusCode: 401, body: { error: "Unauthorized" } }, { order: 3 })
|
|
536
|
-
unauth() {}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const workflow = buildWorkflowFromClass(TodosListDecoratorsWorkflow);
|
|
540
|
-
flosync.register(workflow);
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
If you prefer direct registration:
|
|
544
|
-
|
|
545
|
-
```typescript
|
|
546
|
-
flosync.registerFromClass(TodosListDecoratorsWorkflow);
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
### Decorator workflow: validation, MongoDB, string utilities, shaped JSON
|
|
550
|
-
|
|
551
|
-
This pattern matches what many APIs do: validate input, derive a slug with `utility.string`, write to MongoDB, return a single JSON object with nested `note` metadata (not a flat dump of raw step outputs).
|
|
552
|
-
|
|
553
|
-
Configure the database once:
|
|
554
|
-
|
|
555
|
-
```javascript
|
|
556
|
-
flosync.configure({
|
|
557
|
-
connectors: {
|
|
558
|
-
"mongodb.system": { uri: process.env.MONGO_URI, database: "myapp" },
|
|
559
63
|
},
|
|
560
64
|
});
|
|
561
65
|
```
|
|
562
66
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
```typescript
|
|
566
|
-
import {
|
|
567
|
-
flosync,
|
|
568
|
-
v,
|
|
569
|
-
Workflow,
|
|
570
|
-
Http,
|
|
571
|
-
Validator,
|
|
572
|
-
Util,
|
|
573
|
-
Db,
|
|
574
|
-
Responder,
|
|
575
|
-
buildWorkflowFromClass,
|
|
576
|
-
ConnectorId,
|
|
577
|
-
} from "cliodot";
|
|
578
|
-
|
|
579
|
-
@Workflow("notes-create-decorators", "Notes create (decorators)")
|
|
580
|
-
@Http("POST", "/decorators/notes")
|
|
581
|
-
class NotesCreateDecoratorsWorkflow {
|
|
582
|
-
@Validator(
|
|
583
|
-
{ title: v.body("title"), body: v.body("body") },
|
|
584
|
-
{ order: 0, then: "slugify", else: "validationError" }
|
|
585
|
-
)
|
|
586
|
-
validate() {}
|
|
587
|
-
|
|
588
|
-
@Util(
|
|
589
|
-
ConnectorId.Utility.String,
|
|
590
|
-
"slugify",
|
|
591
|
-
{ string: v.body("title") },
|
|
592
|
-
{ order: 1 }
|
|
593
|
-
)
|
|
594
|
-
slugify() {}
|
|
595
|
-
|
|
596
|
-
@Db(
|
|
597
|
-
"mongodb",
|
|
598
|
-
"insertOne",
|
|
599
|
-
{
|
|
600
|
-
collection: "notes",
|
|
601
|
-
document: {
|
|
602
|
-
title: v.body("title"),
|
|
603
|
-
body: v.body("body"),
|
|
604
|
-
slug: v.stepResult("slugify", "value"),
|
|
605
|
-
},
|
|
606
|
-
},
|
|
607
|
-
{ order: 2 }
|
|
608
|
-
)
|
|
609
|
-
persist() {}
|
|
610
|
-
|
|
611
|
-
@Responder(
|
|
612
|
-
"json",
|
|
613
|
-
{
|
|
614
|
-
statusCode: 201,
|
|
615
|
-
body: {
|
|
616
|
-
ok: true,
|
|
617
|
-
note: {
|
|
618
|
-
id: v.stepResult("persist", "insertedId"),
|
|
619
|
-
title: v.body("title"),
|
|
620
|
-
slug: v.stepResult("slugify", "value"),
|
|
621
|
-
body: v.body("body"),
|
|
622
|
-
},
|
|
623
|
-
},
|
|
624
|
-
},
|
|
625
|
-
{ order: 3 }
|
|
626
|
-
)
|
|
627
|
-
created() {}
|
|
628
|
-
|
|
629
|
-
@Responder(
|
|
630
|
-
"json",
|
|
631
|
-
{
|
|
632
|
-
statusCode: 400,
|
|
633
|
-
body: {
|
|
634
|
-
ok: false,
|
|
635
|
-
error: "validation_failed",
|
|
636
|
-
details: v.stepResult("validate", "errors"),
|
|
637
|
-
},
|
|
638
|
-
},
|
|
639
|
-
{ order: 4 }
|
|
640
|
-
)
|
|
641
|
-
validationError() {}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
const notesDecoratorsWorkflow = buildWorkflowFromClass(NotesCreateDecoratorsWorkflow);
|
|
645
|
-
flosync.register(notesDecoratorsWorkflow);
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
Example request:
|
|
649
|
-
|
|
650
|
-
```http
|
|
651
|
-
POST /decorators/notes
|
|
652
|
-
Content-Type: application/json
|
|
653
|
-
|
|
654
|
-
{
|
|
655
|
-
"title": "Ship checklist",
|
|
656
|
-
"body": "Pack boxes, print labels, hand off to carrier."
|
|
657
|
-
}
|
|
658
|
-
```
|
|
659
|
-
|
|
660
|
-
Example success response body:
|
|
661
|
-
|
|
662
|
-
```json
|
|
663
|
-
{
|
|
664
|
-
"ok": true,
|
|
665
|
-
"note": {
|
|
666
|
-
"id": "65a1b2c3d4e5f6789012345",
|
|
667
|
-
"title": "Ship checklist",
|
|
668
|
-
"slug": "ship-checklist",
|
|
669
|
-
"body": "Pack boxes, print labels, hand off to carrier."
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
```
|
|
673
|
-
|
|
674
|
-
Example validation failure response body:
|
|
675
|
-
|
|
676
|
-
```json
|
|
677
|
-
{
|
|
678
|
-
"ok": false,
|
|
679
|
-
"error": "validation_failed",
|
|
680
|
-
"details": {
|
|
681
|
-
"title": "Field is required"
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
`@Util` is typed the same way as `s.util(ConnectorId.Utility.String, "slugify", { ... })` in the fluent builder. `@Db` supports the `mongodb` and `mysql` engines (see [Database](#database)). Step IDs in `v.stepResult("slugify", "value")` refer to the **method name** you put on each decorated step (after sanitization, dots and hyphens in custom IDs become underscores).
|
|
687
|
-
|
|
688
|
-
### Decorator Connectors
|
|
689
|
-
|
|
690
|
-
For ultimate flexibility, you can define your own **Connectors** using class-based decorators instead of using the generic `defineCustomConnector` pattern.
|
|
691
|
-
|
|
692
|
-
Use the `@ConnectorClass` decorator and map endpoints instantly without rewriting HTTP loops or complex REST logic!
|
|
693
|
-
|
|
694
|
-
```typescript
|
|
695
|
-
import { ConnectorClass, ActionEndpoint, ActionCustom, buildConnectorFromClass, flosync } from "cliodot";
|
|
696
|
-
|
|
697
|
-
// You can pass configuration mimicking flosync REST definition payloads directly into the class decorator:
|
|
698
|
-
@ConnectorClass("paystack", "Paystack Integrations", {
|
|
699
|
-
type: "REST",
|
|
700
|
-
base_url: "https://api.paystack.co",
|
|
701
|
-
auth: { type: "bearer", token: process.env.PAYSTACK_SECRET_KEY }
|
|
702
|
-
})
|
|
703
|
-
export class Paystack {
|
|
704
|
-
|
|
705
|
-
// Handled entirely by Flosync's underlying REST connector automatically!
|
|
706
|
-
@ActionEndpoint("initializeTransaction", "POST", "/transaction/initialize", {
|
|
707
|
-
headers: { "Content-Type": "application/json" }
|
|
708
|
-
})
|
|
709
|
-
initializeTransaction() {}
|
|
710
|
-
|
|
711
|
-
// Paths parameter resolution happens completely implicitly via the REST connector.
|
|
712
|
-
@ActionEndpoint("verifyTransaction", "GET", "/transaction/verify/:reference")
|
|
713
|
-
verifyTransaction() {}
|
|
714
|
-
|
|
715
|
-
// Or, run a completely custom code execution if you need SDKs or Database integrations
|
|
716
|
-
@ActionCustom("generateSignature")
|
|
717
|
-
async generateSignature(payload: any) {
|
|
718
|
-
const crypto = require("crypto");
|
|
719
|
-
return { signature: crypto.createHmac('sha512', process.env.SECRET).update(payload.body.data).digest('hex') };
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// Convert class into standard flosync connector schema
|
|
724
|
-
export const paystackConfig = buildConnectorFromClass(Paystack);
|
|
725
|
-
|
|
726
|
-
export const PaystackConnector = {
|
|
727
|
-
id: PaystackImplementation.id,
|
|
728
|
-
actions: PaystackImplementation.actions
|
|
729
|
-
};
|
|
730
|
-
|
|
731
|
-
// Register it natively just like any built-in module
|
|
732
|
-
flosync.registerConnector(paystackConfig.id, paystackConfig);
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
#### Authentication Options
|
|
736
|
-
|
|
737
|
-
Flosync supports built-in, natively executed REST authentication blocks inside the `@ConnectorClass` config object. You do not need to write boilerplate header injection code!
|
|
738
|
-
|
|
739
|
-
1. **Bearer Token (*Default*)**
|
|
740
|
-
```typescript
|
|
741
|
-
auth: { type: "bearer", token: "your-token", header_name: "Authorization", prefix: "Bearer " }
|
|
742
|
-
```
|
|
743
|
-
2. **API Key**
|
|
744
|
-
```typescript
|
|
745
|
-
auth: { type: "api_key", api_key: "your-api-key", key_name: "x-api-key" }
|
|
746
|
-
```
|
|
747
|
-
3. **No Auth**
|
|
748
|
-
```typescript
|
|
749
|
-
auth: { type: "none" }
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
Then hook it right into your workflows! This example uses `@Validator` for type-safe input checking, generates a unique payment reference using the built-in `utility.random` connector, and passes it to the Paystack API:
|
|
753
|
-
|
|
754
|
-
```typescript
|
|
755
|
-
import { Workflow, Http, Connector, Responder, Validator, v } from "cliodot";
|
|
756
|
-
|
|
757
|
-
@Workflow("payment-initialize", "Initialize Payment")
|
|
758
|
-
@Http("POST", "/payment/initialize")
|
|
759
|
-
export class PaymentInitializeWorkflow {
|
|
760
|
-
|
|
761
|
-
@Validator([
|
|
762
|
-
{
|
|
763
|
-
fields: [v.body("email")],
|
|
764
|
-
validators: [{ name: "required" }, { name: "is_email" }]
|
|
765
|
-
},
|
|
766
|
-
{
|
|
767
|
-
fields: [v.body("amount")],
|
|
768
|
-
validators: [{ name: "required" }]
|
|
769
|
-
}
|
|
770
|
-
], { order: 0, then: "generateReference", else: "invalid" })
|
|
771
|
-
validate() {}
|
|
772
|
-
|
|
773
|
-
@Responder("json", {
|
|
774
|
-
statusCode: 400,
|
|
775
|
-
body: {
|
|
776
|
-
ok: false,
|
|
777
|
-
message: "Validation failed",
|
|
778
|
-
errors: v.stepResult("validate", "errors")
|
|
779
|
-
}
|
|
780
|
-
}, { order: 1 })
|
|
781
|
-
invalid() {}
|
|
782
|
-
|
|
783
|
-
@Connector("utility.random", "random_uuid", {}, { order: 2 })
|
|
784
|
-
generateReference() {}
|
|
785
|
-
|
|
786
|
-
@Connector(
|
|
787
|
-
PaystackConnector.id,
|
|
788
|
-
PaystackConnector.actions.initializeTransaction,
|
|
789
|
-
{
|
|
790
|
-
body: {
|
|
791
|
-
email: v.body("email"),
|
|
792
|
-
amount: v.body("amount"),
|
|
793
|
-
reference: v.stepResult("generateReference", "value"),
|
|
794
|
-
}
|
|
795
|
-
},
|
|
796
|
-
{ order: 3 }
|
|
797
|
-
)
|
|
798
|
-
initialize() {}
|
|
799
|
-
|
|
800
|
-
@Responder("json", { body: { message: "Payment initialized successfully", data: v.stepResult("initialize") } }, { order: 4 })
|
|
801
|
-
respond() {}
|
|
802
|
-
}
|
|
803
|
-
```
|
|
804
|
-
|
|
805
|
-
### Custom `execute` connector plus decorators (catalog sample)
|
|
806
|
-
|
|
807
|
-
For behavior that is not a REST template (in-memory demo data, your own SDK, legacy SOAP adapter, etc.), register a connector with an `execute` function, then call it from decorators with `@Connector` exactly like a built-in connector. This mirrors the `carrier.store` pattern: actions such as `listProducts` and `createProduct`, `options.body` / `options.params` / `options.pathParams` passed from the step config.
|
|
808
|
-
|
|
809
|
-
Define typed action names and the connector:
|
|
810
|
-
|
|
811
|
-
```typescript
|
|
812
|
-
import { flosync, defineCustomConnector } from "cliodot";
|
|
813
|
-
|
|
814
|
-
type CatalogProduct = { id: string; name: string; price: number };
|
|
815
|
-
|
|
816
|
-
const Catalog = defineCustomConnector("acme.catalog", {
|
|
817
|
-
listProducts: "listProducts",
|
|
818
|
-
createProduct: "createProduct",
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
const catalogProducts: CatalogProduct[] = [
|
|
822
|
-
{ id: "p_static_1", name: "Starter", price: 0 },
|
|
823
|
-
{ id: "p_static_2", name: "Pro", price: 49 },
|
|
824
|
-
];
|
|
825
|
-
|
|
826
|
-
flosync.registerConnector(Catalog.id, {
|
|
827
|
-
_id: Catalog.id,
|
|
828
|
-
type: "system",
|
|
829
|
-
name: "Acme Catalog",
|
|
830
|
-
meta: { category: "catalog" },
|
|
831
|
-
auth: { type: "none" },
|
|
832
|
-
execute: async (action, options) => {
|
|
833
|
-
const body = (options.body ?? {}) as Record<string, unknown>;
|
|
834
|
-
const params = (options.params ?? {}) as Record<string, unknown>;
|
|
835
|
-
const pathParams = (options.pathParams ?? {}) as Record<string, unknown>;
|
|
836
|
-
const merged = { ...params, ...pathParams };
|
|
837
|
-
|
|
838
|
-
if (action === Catalog.actions.listProducts) {
|
|
839
|
-
const limitRaw = merged.limit;
|
|
840
|
-
const limit =
|
|
841
|
-
limitRaw === undefined || limitRaw === ""
|
|
842
|
-
? catalogProducts.length
|
|
843
|
-
: Math.max(0, Number(limitRaw));
|
|
844
|
-
const slice = catalogProducts.slice(0, Number.isFinite(limit) ? limit : catalogProducts.length);
|
|
845
|
-
return { ok: true, products: slice, count: slice.length };
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
if (action === Catalog.actions.createProduct) {
|
|
849
|
-
const name = String(body.name ?? "").trim();
|
|
850
|
-
const price = Number(body.price);
|
|
851
|
-
if (!name || Number.isNaN(price)) {
|
|
852
|
-
return { ok: false, error: "name_and_price_required" };
|
|
853
|
-
}
|
|
854
|
-
const id = `p_${Date.now()}`;
|
|
855
|
-
const product: CatalogProduct = { id, name, price };
|
|
856
|
-
catalogProducts.push(product);
|
|
857
|
-
return { ok: true, product };
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
return { ok: false, error: "unknown_action" };
|
|
861
|
-
},
|
|
862
|
-
});
|
|
863
|
-
```
|
|
864
|
-
|
|
865
|
-
List products with decorator `stage: "post"` to shape the payload (add `price_display`, clamp list) before the responder runs:
|
|
866
|
-
|
|
867
|
-
```typescript
|
|
868
|
-
import {
|
|
869
|
-
flosync,
|
|
870
|
-
v,
|
|
871
|
-
Workflow,
|
|
872
|
-
Http,
|
|
873
|
-
Connector,
|
|
874
|
-
Responder,
|
|
875
|
-
buildWorkflowFromClass,
|
|
876
|
-
} from "cliodot";
|
|
877
|
-
|
|
878
|
-
@Workflow("acme-catalog-list-decorators", "Catalog list (decorators)")
|
|
879
|
-
@Http("GET", "/decorators/catalog/products")
|
|
880
|
-
class CatalogListDecoratorsWorkflow {
|
|
881
|
-
@Connector(
|
|
882
|
-
Catalog.id,
|
|
883
|
-
Catalog.actions.listProducts,
|
|
884
|
-
{ params: { limit: v.query("limit") } },
|
|
885
|
-
{ order: 0, stage: "post" }
|
|
886
|
-
)
|
|
887
|
-
list(ctx: WorkflowContext<{ products?: CatalogProduct[]; [k: string]: any }>): StepOutput<{ products: any[]; count: number }> {
|
|
888
|
-
const input = ctx?.input ?? {};
|
|
889
|
-
const items = Array.isArray(input.products) ? input.products : [];
|
|
890
|
-
const withLabels = items.map((p) => ({
|
|
891
|
-
...p,
|
|
892
|
-
price_display: `$${Number(p.price).toFixed(2)}`,
|
|
893
|
-
}));
|
|
894
|
-
return { out: { ...input, products: withLabels, count: withLabels.length } };
|
|
895
|
-
}
|
|
67
|
+
For remote runs against your Cliodot account, set `apiKey` and `apiSecret`. `baseUrl` defaults to `http://localhost:8080` when omitted.
|
|
896
68
|
|
|
897
|
-
|
|
898
|
-
"json",
|
|
899
|
-
{
|
|
900
|
-
body: {
|
|
901
|
-
ok: true,
|
|
902
|
-
count: v.stepResult("list", "count"),
|
|
903
|
-
products: v.stepResult("list", "products"),
|
|
904
|
-
},
|
|
905
|
-
},
|
|
906
|
-
{ order: 1 }
|
|
907
|
-
)
|
|
908
|
-
respond() {}
|
|
909
|
-
}
|
|
910
|
-
```
|
|
911
|
-
|
|
912
|
-
Create product with the same connector; the method body flags bad input and normalizes the successful response:
|
|
913
|
-
|
|
914
|
-
```typescript
|
|
915
|
-
@Workflow("acme-catalog-create-decorators", "Catalog create (decorators)")
|
|
916
|
-
@Http("POST", "/decorators/catalog/products")
|
|
917
|
-
class CatalogCreateDecoratorsWorkflow {
|
|
918
|
-
@Connector(
|
|
919
|
-
Catalog.id,
|
|
920
|
-
Catalog.actions.createProduct,
|
|
921
|
-
{ body: { name: v.body("name"), price: v.body("price") } },
|
|
922
|
-
{ order: 0, stage: "post" }
|
|
923
|
-
)
|
|
924
|
-
save(ctx: WorkflowContext<{ ok?: boolean; product?: CatalogProduct; [k: string]: any }>): StepOutput<any> {
|
|
925
|
-
const input = ctx?.input ?? {};
|
|
926
|
-
if (!input.ok) return { out: input };
|
|
927
|
-
const p = input.product as CatalogProduct;
|
|
928
|
-
return {
|
|
929
|
-
out: {
|
|
930
|
-
...input,
|
|
931
|
-
product: { ...p, price_display: `$${Number(p.price).toFixed(2)}` },
|
|
932
|
-
},
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
@Responder(
|
|
937
|
-
"json",
|
|
938
|
-
{
|
|
939
|
-
statusCode: 201,
|
|
940
|
-
body: {
|
|
941
|
-
ok: true,
|
|
942
|
-
product: v.stepResult("save", "product"),
|
|
943
|
-
},
|
|
944
|
-
},
|
|
945
|
-
{ order: 1 }
|
|
946
|
-
)
|
|
947
|
-
respond() {}
|
|
948
|
-
}
|
|
949
|
-
```
|
|
950
|
-
|
|
951
|
-
Example `POST /decorators/catalog/products` JSON body:
|
|
952
|
-
|
|
953
|
-
```json
|
|
954
|
-
{
|
|
955
|
-
"name": "Enterprise",
|
|
956
|
-
"price": 199
|
|
957
|
-
}
|
|
958
|
-
```
|
|
959
|
-
|
|
960
|
-
Example `201` response body:
|
|
961
|
-
|
|
962
|
-
```json
|
|
963
|
-
{
|
|
964
|
-
"ok": true,
|
|
965
|
-
"product": {
|
|
966
|
-
"id": "p_1711286400000",
|
|
967
|
-
"name": "Enterprise",
|
|
968
|
-
"price": 199,
|
|
969
|
-
"price_display": "$199.00"
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
```
|
|
973
|
-
|
|
974
|
-
Example `GET /decorators/catalog/products?limit=1` response body:
|
|
975
|
-
|
|
976
|
-
```json
|
|
977
|
-
{
|
|
978
|
-
"ok": true,
|
|
979
|
-
"count": 1,
|
|
980
|
-
"products": [
|
|
981
|
-
{ "id": "p_static_1", "name": "Starter", "price": 0, "price_display": "$0.00" }
|
|
982
|
-
]
|
|
983
|
-
}
|
|
984
|
-
```
|
|
985
|
-
|
|
986
|
-
Register both workflows with `buildWorkflowFromClass` / `flosync.register` or `flosync.registerFromClass`. Use `flosync.run("<workflow-id>", { body: { ... } })` with the `@Workflow` string ID (the engine also registers the same workflow under `__rawId` when you use kebab-case IDs).
|
|
987
|
-
|
|
988
|
-
## Functions
|
|
989
|
-
|
|
990
|
-
```javascript
|
|
991
|
-
const validatePayment = flosync.function('validate-payment')
|
|
992
|
-
.input({ amount: 'number', currency: 'string' })
|
|
993
|
-
.step('validate', s => s.validator({ amount: '{{ args.amount }}', currency: '{{ args.currency }}' }))
|
|
994
|
-
.step('format', s => s.transform({ valid: '{{ stepResults.validate.valid }}', amount: '{{ args.amount }}' }))
|
|
995
|
-
.outputFrom('format')
|
|
996
|
-
.build();
|
|
997
|
-
|
|
998
|
-
flosync.registerFunction(validatePayment);
|
|
999
|
-
|
|
1000
|
-
const result = await flosync.runFunction('validate-payment', { amount: 100, currency: 'USD' });
|
|
1001
|
-
```
|
|
1002
|
-
|
|
1003
|
-
## Run by ID (remote)
|
|
1004
|
-
|
|
1005
|
-
When configured with `apiKey`, `apiSecret`, and `baseUrl`, you can run workflows and functions by ID without registering them locally. They are assumed to exist on your account.
|
|
1006
|
-
|
|
1007
|
-
```javascript
|
|
1008
|
-
flosync.configure({ apiKey, apiSecret });
|
|
1009
|
-
|
|
1010
|
-
await flosync.run('workflow-id-from-account', { body: { amount: 100 } });
|
|
1011
|
-
await flosync.runById('workflow-id', { body: { ... } }, { triggerId: 'trigger' });
|
|
1012
|
-
await flosync.runByWebhook('/orders', 'POST', { body: { ... } });
|
|
1013
|
-
|
|
1014
|
-
await flosync.runFunction('function-id-from-account', { amount: 100 });
|
|
1015
|
-
await flosync.runFunctionById('function-id', { amount: 100 });
|
|
1016
|
-
```
|
|
1017
|
-
|
|
1018
|
-
If a workflow/function is registered locally, `run` / `runFunction` use the local version. If not found and the client is configured, they run remotely. Pass `{ remote: false }` to disable remote fallback.
|
|
1019
|
-
|
|
1020
|
-
### Connector versioning (REST connectors)
|
|
1021
|
-
|
|
1022
|
-
Tenant REST connectors can have **published semver snapshots**. The SDK and API use that version when loading connector definitions for workflow runs and for direct execute calls.
|
|
1023
|
-
|
|
1024
|
-
**Workflow steps (fluent builder and `@Connector` config)**
|
|
1025
|
-
Include an optional semver string on the step config so pushed workflows pin the same snapshot the runner expects:
|
|
1026
|
-
|
|
1027
|
-
```typescript
|
|
1028
|
-
s.connector("paystack_conn_id", "initializeTransaction", {
|
|
1029
|
-
connector_version: "1.2.0",
|
|
1030
|
-
body: { email: v.body("email"), amount: v.body("amount") },
|
|
1031
|
-
});
|
|
1032
|
-
```
|
|
1033
|
-
|
|
1034
|
-
Event triggers can pin the source connector the same way on the workflow object: `trigger.source_connector_id` and `trigger.source_connector_version` (see types in `IWorkflowTrigger`).
|
|
1035
|
-
|
|
1036
|
-
**`flosync.runConnector` (remote)**
|
|
1037
|
-
Pass `connector_version` in the options object. Non-owners must match their **installed** version; owners may use other published versions or `draft` per API rules.
|
|
1038
|
-
|
|
1039
|
-
```typescript
|
|
1040
|
-
await flosync.runConnector(Paystack, "initializeTransaction", {
|
|
1041
|
-
connector_version: "1.2.0",
|
|
1042
|
-
body: { email: "a@b.com", amount: 100 },
|
|
1043
|
-
});
|
|
1044
|
-
```
|
|
1045
|
-
|
|
1046
|
-
**`FlosyncClient`** (after `configure` with `apiKey` / `apiSecret`):
|
|
1047
|
-
|
|
1048
|
-
- `client.connectors.get(connectorId, { semver: "1.2.0" })` or `{ version: "1.2.0" }` — same query params as the Flowsync GET connector API.
|
|
1049
|
-
- `client.connectors.install(connectorId, { auth, base_url, version: "latest" })` — `version` is optional (body or query on the server).
|
|
1050
|
-
- `client.connectors.execute(connectorId, action, { connector_version: "1.2.0", body, params, ... })` — sent as `connector_version` on the execute request body.
|
|
1051
|
-
|
|
1052
|
-
Workflows built with `WorkflowBuilder` / decorators serialize `connector_version` onto nodes when you `workflows.push`, so pulls round-trip the field.
|
|
1053
|
-
|
|
1054
|
-
## Database
|
|
1055
|
-
|
|
1056
|
-
Use `s.db(engine, action, config)` or `s.connector(connectorId, action, config)` with built-in database connectors.
|
|
1057
|
-
|
|
1058
|
-
| Connector | Engine | Actions |
|
|
1059
|
-
|-----------|--------|---------|
|
|
1060
|
-
| `mongodb.system` | mongodb | insertOne, insertMany, findOne, find, updateOne, deleteOne |
|
|
1061
|
-
| `mysql.system` | mysql | query |
|
|
1062
|
-
| `postgres.system` | postgres | query |
|
|
1063
|
-
| `redis.system` | redis | redis.get, redis.set, redis.delete, redis.hset, redis.hget, redis.lpush, redis.lpop, redis.zadd, redis.create_hash, redis.delete_key |
|
|
1064
|
-
|
|
1065
|
-
```javascript
|
|
1066
|
-
s.db('mongodb', 'insertOne', { collection: 'orders', document: v.body() });
|
|
1067
|
-
s.db('mysql', 'query', { query: 'SELECT * FROM users WHERE id = ?', params: [v.body('id')] });
|
|
1068
|
-
s.db('postgres', 'query', { query: 'SELECT * FROM users', params: [] });
|
|
1069
|
-
s.db('redis', 'redis.set', { key: 'cache:user:1', value: v.stepResult('fetch'), ttl: 3600 });
|
|
1070
|
-
```
|
|
1071
|
-
|
|
1072
|
-
Configure in `flosync.configure({ connectors: { 'postgres.system': { uri, database }, 'redis.system': { url } } })`.
|
|
1073
|
-
|
|
1074
|
-
## Utilities
|
|
1075
|
-
|
|
1076
|
-
Use `s.util(connectorId, action, config)` with built-in utility connectors. Use `ConnectorId.Utility` and `ConnectorActions` or `getConnectorActions(connectorId)` for autocomplete and suggestions.
|
|
1077
|
-
|
|
1078
|
-
| Connector | Actions |
|
|
1079
|
-
|-----------|---------|
|
|
1080
|
-
| `utility.date_time` | get_current_date, get_current_time, get_current_datetime, format, format_date, parse_date, add_time, subtract_time, diff_between_dates, start_of_day, end_of_day, start_of_week, end_of_week, start_of_month, end_of_month, start_of_year, end_of_year, convert_timezone, get_timezone, list_timezones, is_leap_year, get_unix_timestamp, from_unix_timestamp |
|
|
1081
|
-
| `utility.string` | concat, split, join, replace, replace_all, trim, trim_start, trim_end, to_uppercase, to_lowercase, capitalize, substring, length, contains, starts_with, ends_with, remove_whitespace, slugify, pad_start, pad_end, reverse, encode_base64, decode_base64, encode_uri, decode_uri, after, after_last, before, before_last, between, between_first, camel, char_at, chop_start, chop_end, contains_all, doesnt_contain, doesnt_start_with, doesnt_end_with, deduplicate, excerpt, finish, start, headline, is, is_ascii, is_json, is_ulid, is_url, is_uuid, is_empty, is_match, kebab, snake, studly, title, lcfirst, ucfirst, ucwords, ucsplit, limit, mask, match, match_all, pad_both, plural, singular, position, random, remove, repeat, replace_array, replace_first, replace_last, replace_matches, replace_start, replace_end, squish, substr, substr_count, substr_replace, swap, take, word_count, word_wrap, words, wrap, unwrap, uuid, ulid, strip_tags, ascii, apa, class_basename |
|
|
1082
|
-
| `utility.math` | add, subtract, multiply, divide, modulo, power, round, floor, ceil, abs, min, max, average, sum, clamp, random_between, percentage, percentage_of, sqrt, log |
|
|
1083
|
-
| `utility.random` | random_int, random_float, random_string, random_uuid, uuid, random_boolean, random_choice, random_password, random_hex, random_alphanumeric, random_date, random_color, shuffle_array |
|
|
1084
|
-
| `utility.compare` | diff_objects, diff_arrays, compare_strings, compare_numbers, compare_dates, is_changed, is_equal, is_not_equal, is_greater_than, is_less_than, is_between, is_empty, is_null_or_undefined, is_same_type, deep_equal, shallow_equal, similarity_score, array_intersection, array_union, array_difference |
|
|
1085
|
-
| `utility.geo` | calculate_distance, point_in_polygon, get_bounding_box, reverse_geocode, geocode, convert_coordinates |
|
|
1086
|
-
|
|
1087
|
-
```javascript
|
|
1088
|
-
import { ConnectorId, ConnectorActions, getConnectorActions } from 'cliodot';
|
|
1089
|
-
|
|
1090
|
-
s.util(ConnectorId.Utility.String, 'slugify', { string: 'Hello World' });
|
|
1091
|
-
s.util(ConnectorId.Utility.Math, 'clamp', { number: 150, min: 0, max: 100 });
|
|
1092
|
-
s.util(ConnectorId.Utility.Compare, 'diff_objects', { object1: v.stepResult('prev'), object2: v.body() });
|
|
1093
|
-
s.util(ConnectorId.Utility.Geo, 'calculate_distance', { lat1: 0, lon1: 0, lat2: 1, lon2: 1 });
|
|
1094
|
-
|
|
1095
|
-
const stringActions = getConnectorActions(ConnectorId.Utility.String);
|
|
1096
|
-
```
|
|
1097
|
-
|
|
1098
|
-
## Responders
|
|
1099
|
-
|
|
1100
|
-
Use `s.responder(type, config)` with built-in responder types.
|
|
1101
|
-
|
|
1102
|
-
| Type | Description |
|
|
1103
|
-
|------|-------------|
|
|
1104
|
-
| json | JSON response (default content-type: application/json) |
|
|
1105
|
-
| http | Same as json, supports custom headers |
|
|
1106
|
-
| raw | Plain text with custom contentType |
|
|
1107
|
-
| redirect | 302/301 redirect with Location header |
|
|
1108
|
-
| empty | 202 with no body |
|
|
1109
|
-
|
|
1110
|
-
```javascript
|
|
1111
|
-
s.responder('json', { body: v.stepResult('data') });
|
|
1112
|
-
s.responder('raw', { body: 'OK', contentType: 'text/plain' });
|
|
1113
|
-
s.responder('redirect', { location: 'https://example.com', statusCode: 301 });
|
|
1114
|
-
s.responder('empty', { statusCode: 202 });
|
|
1115
|
-
```
|
|
1116
|
-
|
|
1117
|
-
## Validation
|
|
1118
|
-
|
|
1119
|
-
The `@Validator` decorator provides type-safe input validation for workflow steps. Validators use a `ValidationRule` union type that gives you full IDE auto-completion for all available rules and their configuration options.
|
|
1120
|
-
|
|
1121
|
-
### Basic usage
|
|
1122
|
-
|
|
1123
|
-
```typescript
|
|
1124
|
-
import { Validator, v } from "cliodot";
|
|
1125
|
-
|
|
1126
|
-
@Validator([
|
|
1127
|
-
{
|
|
1128
|
-
fields: [v.body("email")],
|
|
1129
|
-
validators: [{ name: "required" }, { name: "is_email" }]
|
|
1130
|
-
},
|
|
1131
|
-
{
|
|
1132
|
-
fields: [v.body("amount")],
|
|
1133
|
-
validators: [{ name: "required" }, { name: "is_number" }]
|
|
1134
|
-
}
|
|
1135
|
-
], { order: 0, then: "process", else: "error" })
|
|
1136
|
-
validate() {}
|
|
1137
|
-
```
|
|
1138
|
-
|
|
1139
|
-
### Available validators
|
|
1140
|
-
|
|
1141
|
-
| Validator | Config | Description |
|
|
1142
|
-
|-----------|--------|-------------|
|
|
1143
|
-
| `required` | `{ message? }` | Field must be present and non-null |
|
|
1144
|
-
| `not_empty` | `{ message? }` | Field must not be empty |
|
|
1145
|
-
| `is_empty` | `{ message? }` | Field must be empty |
|
|
1146
|
-
| `is_email` | `{ message? }` | Valid email format |
|
|
1147
|
-
| `is_number` | `{ message? }` | Must be a number |
|
|
1148
|
-
| `is_string` | `{ message? }` | Must be a string |
|
|
1149
|
-
| `is_boolean` | `{ message? }` | Must be a boolean |
|
|
1150
|
-
| `is_array` | `{ message? }` | Must be an array |
|
|
1151
|
-
| `is_object` | `{ message? }` | Must be an object |
|
|
1152
|
-
| `is_url` | `{ message? }` | Valid URL format |
|
|
1153
|
-
| `is_date` | `{ message? }` | Valid date format |
|
|
1154
|
-
| `is_uuid` | `{ message? }` | Valid UUID (v1-v5) |
|
|
1155
|
-
| `is_cuid` | `{ message? }` | Valid CUID |
|
|
1156
|
-
| `is_jwt` | `{ message? }` | Valid JWT structure (`header.payload.signature`) |
|
|
1157
|
-
| `is_json` | `{ message? }` | Valid parseable JSON string |
|
|
1158
|
-
| `is_alphanumeric` | `{ message? }` | Letters and numbers only |
|
|
1159
|
-
| `is_alpha` | `{ message? }` | Letters only |
|
|
1160
|
-
| `is_numeric_string` | `{ message? }` | Numeric digits only |
|
|
1161
|
-
| `is_hex_color` | `{ message? }` | Valid hex color (`#FFF` or `#123456`) |
|
|
1162
|
-
| `is_base64` | `{ message? }` | Valid base64 encoded string |
|
|
1163
|
-
| `is_credit_card` | `{ message? }` | Valid credit card (Luhn algorithm) |
|
|
1164
|
-
| `is_lowercase` | `{ message? }` | Must be all lowercase |
|
|
1165
|
-
| `is_uppercase` | `{ message? }` | Must be all uppercase |
|
|
1166
|
-
| `is_slug` | `{ message? }` | Valid URL slug (`my-cool-slug`) |
|
|
1167
|
-
| `is_mac_address` | `{ message? }` | Valid MAC address |
|
|
1168
|
-
| `is_port` | `{ message? }` | Valid TCP/UDP port (0-65535) |
|
|
1169
|
-
| `is_currency` | `{ message? }` | Valid currency format |
|
|
1170
|
-
| `is_latitude` | `{ message? }` | Valid latitude (-90 to 90) |
|
|
1171
|
-
| `is_longitude` | `{ message? }` | Valid longitude (-180 to 180) |
|
|
1172
|
-
| `is_strong_password` | `{ minLength?, minLowercase?, minUppercase?, minNumbers?, minSymbols?, message? }` | Configurable password strength |
|
|
1173
|
-
| `is_ip` | `{ version?: 4 \| 6, message? }` | Valid IPv4 or IPv6 address |
|
|
1174
|
-
| `is_phone` | `{ pattern?, message? }` | Phone number format |
|
|
1175
|
-
| `is_address` | `{ minLength?, maxLength?, message? }` | Address string checks |
|
|
1176
|
-
| `contains` / `not_contains` | `{ search, caseSensitive?, message? }` | String contains/excludes a value |
|
|
1177
|
-
| `begins_with` | `{ prefix, caseSensitive?, message? }` | String starts with prefix |
|
|
1178
|
-
| `ends_with` | `{ suffix, caseSensitive?, message? }` | String ends with suffix |
|
|
1179
|
-
| `is_in` / `not_in` | `{ values: any[], caseSensitive?, message? }` | Value is/isn't in a list |
|
|
1180
|
-
| `length` | `{ min?, max?, exact?, message? }` | String/array length bounds |
|
|
1181
|
-
| `range` | `{ min?, max?, message? }` | Numeric range bounds |
|
|
1182
|
-
| `matches` | `{ pattern, flags?, message? }` | Regex pattern match |
|
|
1183
|
-
| `equals` / `not_equals` | `{ value, message? }` | Strict equality check |
|
|
1184
|
-
| `greater_than` / `less_than` | `{ value: number, message? }` | Numeric comparison |
|
|
1185
|
-
| `greater_than_or_equal` / `less_than_or_equal` | `{ value: number, message? }` | Inclusive numeric comparison |
|
|
1186
|
-
|
|
1187
|
-
### Configurable validators
|
|
1188
|
-
|
|
1189
|
-
Some validators accept rich configuration objects with IDE autocomplete:
|
|
1190
|
-
|
|
1191
|
-
```typescript
|
|
1192
|
-
// Strong password with custom constraints
|
|
1193
|
-
@Validator([
|
|
1194
|
-
{
|
|
1195
|
-
fields: [v.body("password")],
|
|
1196
|
-
validators: [{
|
|
1197
|
-
name: "is_strong_password",
|
|
1198
|
-
config: { minLength: 12, minUppercase: 2, minSymbols: 1 }
|
|
1199
|
-
}]
|
|
1200
|
-
}
|
|
1201
|
-
], { order: 0, then: "register", else: "invalid" })
|
|
1202
|
-
validatePassword() {}
|
|
1203
|
-
|
|
1204
|
-
// IP address with version lock
|
|
1205
|
-
@Validator([
|
|
1206
|
-
{
|
|
1207
|
-
fields: [v.body("server_ip")],
|
|
1208
|
-
validators: [{ name: "is_ip", config: { version: 4 } }]
|
|
1209
|
-
}
|
|
1210
|
-
], { order: 0, then: "connect", else: "invalid" })
|
|
1211
|
-
validateIP() {}
|
|
1212
|
-
|
|
1213
|
-
// Range + custom message
|
|
1214
|
-
@Validator([
|
|
1215
|
-
{
|
|
1216
|
-
fields: [v.body("age")],
|
|
1217
|
-
validators: [{
|
|
1218
|
-
name: "range",
|
|
1219
|
-
config: { min: 18, max: 120, message: "Age must be between 18 and 120" }
|
|
1220
|
-
}]
|
|
1221
|
-
}
|
|
1222
|
-
], { order: 0, then: "proceed", else: "invalid" })
|
|
1223
|
-
validateAge() {}
|
|
1224
|
-
```
|
|
1225
|
-
|
|
1226
|
-
### Validation branching
|
|
1227
|
-
|
|
1228
|
-
Use `then` and `else` to route the workflow based on validation results:
|
|
1229
|
-
|
|
1230
|
-
```typescript
|
|
1231
|
-
@Validator([
|
|
1232
|
-
{ fields: [v.body("email")], validators: [{ name: "required" }, { name: "is_email" }] }
|
|
1233
|
-
], { order: 0, then: "process", else: "validationError" })
|
|
1234
|
-
validate() {}
|
|
1235
|
-
|
|
1236
|
-
@Responder("json", {
|
|
1237
|
-
statusCode: 400,
|
|
1238
|
-
body: { ok: false, errors: v.stepResult("validate", "errors") }
|
|
1239
|
-
}, { order: 1 })
|
|
1240
|
-
validationError() {}
|
|
1241
|
-
```
|
|
1242
|
-
|
|
1243
|
-
When validation fails, the `errors` object is populated with field-level error messages that the responder can return to the client.
|
|
1244
|
-
|
|
1245
|
-
## Debug Mode
|
|
1246
|
-
|
|
1247
|
-
Flosync includes a built-in debug mode that injects a `_debug` object into workflow responses, containing the full `stepResults` map of every step that executed. This is invaluable for development and troubleshooting without adding `console.log` statements.
|
|
1248
|
-
|
|
1249
|
-
### Enabling debug
|
|
1250
|
-
|
|
1251
|
-
**1. Global (via config)** — applies to all workflow runs:
|
|
1252
|
-
|
|
1253
|
-
```typescript
|
|
1254
|
-
flosync.configure({
|
|
1255
|
-
debug: true,
|
|
1256
|
-
connectors: { ... },
|
|
1257
|
-
});
|
|
1258
|
-
```
|
|
1259
|
-
|
|
1260
|
-
**2. Per-request (via header)** — enable for a single call:
|
|
1261
|
-
|
|
1262
|
-
```bash
|
|
1263
|
-
curl -X POST http://localhost:3384/payment/initialize \
|
|
1264
|
-
-H "Content-Type: application/json" \
|
|
1265
|
-
-H "x-flosync-debug: true" \
|
|
1266
|
-
-d '{"email": "user@example.com", "amount": 5000}'
|
|
1267
|
-
```
|
|
1268
|
-
|
|
1269
|
-
**3. Per-request (via query param):**
|
|
1270
|
-
|
|
1271
|
-
```bash
|
|
1272
|
-
curl "http://localhost:3384/books?debug=true"
|
|
1273
|
-
```
|
|
1274
|
-
|
|
1275
|
-
### Debug output
|
|
1276
|
-
|
|
1277
|
-
When debug mode is active, a `_debug` key is appended to the response body:
|
|
1278
|
-
|
|
1279
|
-
```json
|
|
1280
|
-
{
|
|
1281
|
-
"message": "Payment initialized successfully",
|
|
1282
|
-
"data": {
|
|
1283
|
-
"authorization_url": "https://checkout.paystack.com/abc123",
|
|
1284
|
-
"reference": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
1285
|
-
},
|
|
1286
|
-
"_debug": {
|
|
1287
|
-
"stepResults": {
|
|
1288
|
-
"validate": { "valid": true },
|
|
1289
|
-
"generateReference": { "value": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" },
|
|
1290
|
-
"initialize": {
|
|
1291
|
-
"authorization_url": "https://checkout.paystack.com/abc123",
|
|
1292
|
-
"reference": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
```
|
|
1298
|
-
|
|
1299
|
-
When debug mode is **off** (default), the `_debug` key is completely absent — zero overhead and no payload pollution.
|
|
1300
|
-
|
|
1301
|
-
> **Note:** Debug mode works for both local workflow execution (`flosync.run()`) and remote execution via `client.workflows.run()` / `client.workflows.runByWebhook()`.
|
|
1302
|
-
|
|
1303
|
-
## Encryption
|
|
1304
|
-
|
|
1305
|
-
Use `s.encrypt(connectorId, action, config)` with built-in encryption connectors. All use Node `crypto`; Argon2 and bcrypt require optional packages.
|
|
1306
|
-
|
|
1307
|
-
| Connector | Actions |
|
|
1308
|
-
|-----------|---------|
|
|
1309
|
-
| `base64.encryption` | `base64.encode`, `base64.decode` |
|
|
1310
|
-
| `hash.encryption` | `hash`, `hash.md5`, `hash.sha1`, `hash.sha256`, `hash.sha512` |
|
|
1311
|
-
| `aes.encryption` | `aes.encrypt`, `aes.decrypt`, `aes.generate_key`, `aes.generate_iv` |
|
|
1312
|
-
| `rsa.encryption` | `rsa.generate_keypair`, `rsa.encrypt`, `rsa.decrypt`, `rsa.sign`, `rsa.verify` |
|
|
1313
|
-
| `hmac.encryption` | `hmac.create`, `hmac.verify` |
|
|
1314
|
-
| `password.encryption` | `password.argon2_hash`, `password.argon2_verify`, `password.pbkdf2_hash`, `password.pbkdf2_verify`, `password.scrypt_hash`, `password.scrypt_verify`, `password.bcrypt_hash`, `password.bcrypt_verify`, `password.generate_salt` |
|
|
1315
|
-
|
|
1316
|
-
**Examples:**
|
|
1317
|
-
|
|
1318
|
-
```javascript
|
|
1319
|
-
s.encrypt('hash.encryption', 'hash', { data: v.body('payload'), algorithm: 'sha256' });
|
|
1320
|
-
s.encrypt('base64.encryption', 'base64.encode', { data: v.stepResult('prev') });
|
|
1321
|
-
s.encrypt('aes.encryption', 'aes.encrypt', { data: v.body('secret'), key: v.vars('key') });
|
|
1322
|
-
s.encrypt('rsa.encryption', 'rsa.sign', { data: v.body('payload'), private_key: v.vars('privateKey') });
|
|
1323
|
-
s.encrypt('hmac.encryption', 'hmac.create', { data: v.body('message'), key: v.vars('secret') });
|
|
1324
|
-
s.encrypt('password.encryption', 'password.argon2_hash', { password: v.body('password') });
|
|
1325
|
-
```
|
|
1326
|
-
|
|
1327
|
-
**Optional packages:** `password.argon2_hash` / `password.argon2_verify` need `argon2`; `password.bcrypt_hash` / `password.bcrypt_verify` need `bcrypt`. Install with `npm install argon2 bcrypt` if you use those actions.
|
|
1328
|
-
|
|
1329
|
-
## Connector Registry
|
|
1330
|
-
|
|
1331
|
-
Use `ConnectorId` and `ConnectorActions` for built-in connectors, or `defineCustomConnector` for your own. This gives you autocomplete and avoids typos.
|
|
1332
|
-
|
|
1333
|
-
### Built-in connectors
|
|
1334
|
-
|
|
1335
|
-
```javascript
|
|
1336
|
-
import { flosync, ConnectorId, ConnectorActions, getConnectorActions, listBuiltInConnectors } from 'cliodot';
|
|
1337
|
-
|
|
1338
|
-
// Database
|
|
1339
|
-
s.db('mongodb', 'insertOne', { collection: 'orders', document: {} });
|
|
1340
|
-
s.db('mysql', 'query', { query: 'SELECT 1', params: [] });
|
|
1341
|
-
s.db('postgres', 'query', { query: 'SELECT 1', params: [] });
|
|
1342
|
-
s.db('redis', 'redis.set', { key: 'key', value: 'value' });
|
|
1343
|
-
|
|
1344
|
-
// Auth
|
|
1345
|
-
s.auth(ConnectorId.Auth.Bearer, ConnectorActions[ConnectorId.Auth.Bearer][0], { token: '{{ headers.authorization }}' });
|
|
1346
|
-
|
|
1347
|
-
// Utility - use ConnectorActions or getConnectorActions for autocomplete
|
|
1348
|
-
s.util(ConnectorId.Utility.DateTime, 'get_current_datetime', {});
|
|
1349
|
-
s.util(ConnectorId.Utility.String, 'slugify', { string: 'Hello World' });
|
|
1350
|
-
s.util(ConnectorId.Utility.Math, 'add', { numbers: [10, 20] });
|
|
1351
|
-
s.util(ConnectorId.Utility.Random, 'random_uuid', {});
|
|
1352
|
-
s.util(ConnectorId.Utility.Compare, 'diff_objects', { object1: {}, object2: {} });
|
|
1353
|
-
s.util(ConnectorId.Utility.Geo, 'calculate_distance', { lat1: 0, lon1: 0, lat2: 1, lon2: 1 });
|
|
1354
|
-
|
|
1355
|
-
// Get actions for a connector (for suggestions/autocomplete)
|
|
1356
|
-
const stringActions = getConnectorActions(ConnectorId.Utility.String);
|
|
1357
|
-
|
|
1358
|
-
// List all built-in connectors and their actions
|
|
1359
|
-
console.log(listBuiltInConnectors());
|
|
1360
|
-
```
|
|
1361
|
-
|
|
1362
|
-
### Custom connectors
|
|
1363
|
-
|
|
1364
|
-
Define your connector with typed actions for autocomplete:
|
|
1365
|
-
|
|
1366
|
-
```javascript
|
|
1367
|
-
import { defineCustomConnector } from 'cliodot';
|
|
1368
|
-
|
|
1369
|
-
const MyStore = defineCustomConnector('my.store', {
|
|
1370
|
-
login: 'login',
|
|
1371
|
-
listItems: 'listItems',
|
|
1372
|
-
createItem: 'createItem',
|
|
1373
|
-
});
|
|
1374
|
-
|
|
1375
|
-
flosync.registerConnector(MyStore.id, myStoreConnector);
|
|
1376
|
-
|
|
1377
|
-
s.connector(MyStore.id, MyStore.actions.login, { body: { email: '{{ body.email }}' } });
|
|
1378
|
-
s.connector(MyStore.id, MyStore.actions.listItems, {});
|
|
1379
|
-
```
|
|
1380
|
-
|
|
1381
|
-
For a simple list of action names:
|
|
1382
|
-
|
|
1383
|
-
```javascript
|
|
1384
|
-
import { defineCustomConnectorFromList } from 'cliodot';
|
|
1385
|
-
|
|
1386
|
-
const MyStore = defineCustomConnectorFromList('my.store', ['login', 'listItems', 'createItem']);
|
|
1387
|
-
```
|
|
1388
|
-
|
|
1389
|
-
### Typed connectors (for remote connectors)
|
|
1390
|
-
|
|
1391
|
-
When using remote connectors, you do not have the implementation locally. `defineTypedConnector` lets you define per-action schema so TypeScript can infer request/response shapes across decorators, builder calls, and `runConnector`.
|
|
1392
|
-
|
|
1393
|
-
```typescript
|
|
1394
|
-
import { defineTypedConnector } from "cliodot";
|
|
1395
|
-
|
|
1396
|
-
const Paystack = defineTypedConnector("paystack", {
|
|
1397
|
-
initializeTransaction: "Initialize Transaction",
|
|
1398
|
-
verifyTransaction: "Verify Transaction",
|
|
1399
|
-
listTransactions: "List Transactions",
|
|
1400
|
-
}, {
|
|
1401
|
-
initializeTransaction: {
|
|
1402
|
-
body: {} as { email: string; amount: number; reference?: string; callback_url?: string },
|
|
1403
|
-
headers: {} as { "Idempotency-Key"?: string },
|
|
1404
|
-
vars: {} as { traceId?: string },
|
|
1405
|
-
response: {} as { authorization_url: string; access_code: string; reference: string },
|
|
1406
|
-
},
|
|
1407
|
-
verifyTransaction: {
|
|
1408
|
-
pathParams: {} as { reference: string },
|
|
1409
|
-
response: {} as { status: string; amount: number; currency: string; paid_at: string },
|
|
1410
|
-
},
|
|
1411
|
-
listTransactions: {
|
|
1412
|
-
params: {} as { page?: number; perPage?: number; status?: string },
|
|
1413
|
-
response: {} as { data: Array<{ reference: string; amount: number; status: string }> },
|
|
1414
|
-
},
|
|
1415
|
-
});
|
|
1416
|
-
```
|
|
1417
|
-
|
|
1418
|
-
Schema fields:
|
|
1419
|
-
|
|
1420
|
-
| Field | Description |
|
|
1421
|
-
|-------|-------------|
|
|
1422
|
-
| `body` | Shape of request body |
|
|
1423
|
-
| `params` | Shape of query/params |
|
|
1424
|
-
| `pathParams` | Shape of URL path params |
|
|
1425
|
-
| `headers` | Shape of custom headers |
|
|
1426
|
-
| `vars` | Shape of step-level vars merged before execution |
|
|
1427
|
-
| `response` | Shape of expected response |
|
|
1428
|
-
|
|
1429
|
-
Use `{} as YourType` to define each shape without runtime values.
|
|
1430
|
-
|
|
1431
|
-
**Extracting types with helper utilities:**
|
|
1432
|
-
|
|
1433
|
-
```typescript
|
|
1434
|
-
import type {
|
|
1435
|
-
ConnectorBody,
|
|
1436
|
-
ConnectorParams,
|
|
1437
|
-
ConnectorPathParams,
|
|
1438
|
-
ConnectorHeaders,
|
|
1439
|
-
ConnectorVars,
|
|
1440
|
-
ConnectorResponse,
|
|
1441
|
-
} from "cliodot";
|
|
1442
|
-
|
|
1443
|
-
type InitBody = ConnectorBody<typeof Paystack, "initializeTransaction">;
|
|
1444
|
-
type InitHeaders = ConnectorHeaders<typeof Paystack, "initializeTransaction">;
|
|
1445
|
-
type InitVars = ConnectorVars<typeof Paystack, "initializeTransaction">;
|
|
1446
|
-
type VerifyPath = ConnectorPathParams<typeof Paystack, "verifyTransaction">;
|
|
1447
|
-
type ListParams = ConnectorParams<typeof Paystack, "listTransactions">;
|
|
1448
|
-
type VerifyResponse = ConnectorResponse<typeof Paystack, "verifyTransaction">;
|
|
1449
|
-
```
|
|
1450
|
-
|
|
1451
|
-
**Using in decorators (typed action key + typed config):**
|
|
1452
|
-
|
|
1453
|
-
```typescript
|
|
1454
|
-
import { Workflow, Http, Connector, Responder, v } from "cliodot";
|
|
1455
|
-
import type { WorkflowContext, StepOutput, ConnectorResponse } from "cliodot";
|
|
1456
|
-
|
|
1457
|
-
type InitResponse = ConnectorResponse<typeof Paystack, "initializeTransaction">;
|
|
1458
|
-
|
|
1459
|
-
@Workflow("paystack-init", "Initialize Paystack Payment")
|
|
1460
|
-
@Http("POST", "/pay")
|
|
1461
|
-
class PaystackInitWorkflow {
|
|
1462
|
-
@Connector(
|
|
1463
|
-
Paystack,
|
|
1464
|
-
"initializeTransaction",
|
|
1465
|
-
{
|
|
1466
|
-
body: { email: v.body("email"), amount: v.body("amount"), callback_url: v.body("callback_url") },
|
|
1467
|
-
headers: { "Idempotency-Key": "{{ trigger.idempotencyKey }}" },
|
|
1468
|
-
vars: { traceId: "{{ trigger.traceId }}" },
|
|
1469
|
-
},
|
|
1470
|
-
{ order: 0, stage: "post" }
|
|
1471
|
-
)
|
|
1472
|
-
init(ctx: WorkflowContext<InitResponse>): StepOutput<{ url: string; ref: string }> {
|
|
1473
|
-
return {
|
|
1474
|
-
out: {
|
|
1475
|
-
url: ctx.input.authorization_url,
|
|
1476
|
-
ref: ctx.input.reference,
|
|
1477
|
-
},
|
|
1478
|
-
};
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
@Responder("json", {
|
|
1482
|
-
body: { url: v.stepResult("init", "url"), reference: v.stepResult("init", "ref") },
|
|
1483
|
-
}, { order: 1 })
|
|
1484
|
-
respond() {}
|
|
1485
|
-
}
|
|
1486
|
-
```
|
|
1487
|
-
|
|
1488
|
-
You can still use the classic style (`Paystack.id` + `Paystack.actions.someAction`) if you prefer.
|
|
1489
|
-
|
|
1490
|
-
**Typed connectors also work in builder and direct runs:**
|
|
1491
|
-
|
|
1492
|
-
```typescript
|
|
1493
|
-
// Builder
|
|
1494
|
-
workflow.step("init", (s) =>
|
|
1495
|
-
s.connector(Paystack, "initializeTransaction", {
|
|
1496
|
-
body: { email: "{{ trigger.email }}", amount: "{{ trigger.amount }}" },
|
|
1497
|
-
headers: { "Idempotency-Key": "{{ trigger.idempotencyKey }}" },
|
|
1498
|
-
})
|
|
1499
|
-
);
|
|
1500
|
-
|
|
1501
|
-
// Direct connector execution
|
|
1502
|
-
await flosync.runConnector(Paystack, "verifyTransaction", {
|
|
1503
|
-
pathParams: { reference: "ref_123" },
|
|
1504
|
-
});
|
|
1505
|
-
```
|
|
1506
|
-
|
|
1507
|
-
Template strings are accepted in typed request fields (`body`, `params`, `pathParams`, `headers`, `vars`) so workflow expressions continue to work with strong typing.
|
|
1508
|
-
|
|
1509
|
-
Available type helpers:
|
|
1510
|
-
|
|
1511
|
-
| Helper | Description |
|
|
1512
|
-
|--------|-------------|
|
|
1513
|
-
| `ConnectorBody<TDef, TAction>` | Extracts `body` type |
|
|
1514
|
-
| `ConnectorParams<TDef, TAction>` | Extracts `params` type |
|
|
1515
|
-
| `ConnectorPathParams<TDef, TAction>` | Extracts `pathParams` type |
|
|
1516
|
-
| `ConnectorHeaders<TDef, TAction>` | Extracts `headers` type |
|
|
1517
|
-
| `ConnectorVars<TDef, TAction>` | Extracts `vars` type |
|
|
1518
|
-
| `ConnectorResponse<TDef, TAction>` | Extracts `response` type |
|
|
1519
|
-
| `ConnectorActionRequest<TDef, TAction>` | Extracts full request shape |
|
|
1520
|
-
| `ConnectorTypedRequestConfig<TDef, TAction>` | Typed decorator/builder request config |
|
|
1521
|
-
|
|
1522
|
-
Examples-first guide:
|
|
1523
|
-
|
|
1524
|
-
- [`Typed Connector End-to-End Samples`](./TYPED_CONNECTOR_EXAMPLES.md)
|
|
1525
|
-
|
|
1526
|
-
## Connectors by ID
|
|
1527
|
-
|
|
1528
|
-
Use `s.connector(connectorId, action, config)` with any connector ID. For remote runs, connectors are resolved from your account. For local runs, register connectors first with `flosync.registerConnector(id, connector)` or pass config in `configure({ connectors: { 'connector-id': { ... } } })`.
|
|
1529
|
-
|
|
1530
|
-
## API Client
|
|
1531
|
-
|
|
1532
|
-
When configured with `apiKey`, `apiSecret`, and `baseUrl`:
|
|
1533
|
-
|
|
1534
|
-
```javascript
|
|
1535
|
-
const client = flosync.getClient();
|
|
1536
|
-
|
|
1537
|
-
await flosync.push(workflow);
|
|
1538
|
-
const wf = await flosync.pull(groupId);
|
|
1539
|
-
|
|
1540
|
-
const { workflowGroups, pagination } = await client.workflows.list({ status: 'active', page: 1, limit: 20 });
|
|
1541
|
-
await client.workflows.run(groupId, triggerId, { payload: { ... } });
|
|
1542
|
-
await client.workflows.runByWebhook('/orders', 'POST', { body: { ... } });
|
|
1543
|
-
|
|
1544
|
-
const functions = await client.functions.list();
|
|
1545
|
-
await client.functions.push(fn);
|
|
1546
|
-
const fn = await client.functions.pull(functionId);
|
|
1547
|
-
await client.functions.invoke(functionId, { args: { ... } });
|
|
1548
|
-
|
|
1549
|
-
await client.projects.create({ name: 'My Project' });
|
|
1550
|
-
const projects = await client.projects.list();
|
|
1551
|
-
```
|
|
1552
|
-
|
|
1553
|
-
### Run connector from online
|
|
1554
|
-
|
|
1555
|
-
Execute connectors stored in your Cliodot account:
|
|
1556
|
-
|
|
1557
|
-
```javascript
|
|
1558
|
-
const result = await flosync.runConnector('paystack', 'Initialize Transaction', {
|
|
1559
|
-
body: { email: 'user@example.com', amount: 5000, reference: 'ref_123' },
|
|
1560
|
-
});
|
|
1561
|
-
|
|
1562
|
-
const { connectors, pagination } = await client.connectors.list();
|
|
1563
|
-
const { installations } = await client.connectors.installed();
|
|
1564
|
-
const connector = await client.connectors.get('paystack');
|
|
1565
|
-
const data = await client.connectors.execute('paystack', 'Verify Transaction', {
|
|
1566
|
-
pathParams: { reference: 'ref_123' },
|
|
1567
|
-
});
|
|
1568
|
-
```
|
|
1569
|
-
|
|
1570
|
-
| Method | Description |
|
|
1571
|
-
|--------|-------------|
|
|
1572
|
-
| `flosync.runConnector(id, action, options, { remote? })` | Run connector (local if registered, else remote) |
|
|
1573
|
-
| `client.connectors.get(id)` | Get connector definition |
|
|
1574
|
-
| `client.connectors.list(options?)` | List connectors (type, status, category, page, limit) |
|
|
1575
|
-
| `client.connectors.search(options?)` | Search public connectors |
|
|
1576
|
-
| `client.connectors.push(connector)` | Create or update connector (by `_id` or `slug`) |
|
|
1577
|
-
| `client.connectors.installed(options?)` | List installed connectors |
|
|
1578
|
-
| `client.connectors.install(id, { auth?, base_url? })` | Install connector |
|
|
1579
|
-
| `client.connectors.uninstall(id)` | Uninstall connector |
|
|
1580
|
-
| `client.connectors.execute(id, action, options)` | Execute connector action remotely |
|
|
1581
|
-
|
|
1582
|
-
### Workflows
|
|
1583
|
-
|
|
1584
|
-
| Method | Description |
|
|
1585
|
-
|--------|-------------|
|
|
1586
|
-
| `client.workflows.push(workflow)` | Create or update workflow |
|
|
1587
|
-
| `client.workflows.pull(groupId)` | Fetch workflow as IWorkflow |
|
|
1588
|
-
| `client.workflows.list(options?)` | List workflow groups |
|
|
1589
|
-
| `client.workflows.run(groupId, triggerId, payload)` | Run by trigger ID (payload may include `environment: "dev" \| "prod"`) |
|
|
1590
|
-
| `client.workflows.runByWebhook(path, method, payload)` | **`POST /workflows/test/webhook`** — run the workflow whose HTTP trigger matches `path` and `method` (no group id in the request). Payload may include `environment`, `executionHeaders`, and the child body fields (merged into `payload` for the API). |
|
|
1591
|
-
| `client.workflows.promote(groupId)` | Promote to production |
|
|
1592
|
-
|
|
1593
|
-
See [Workflows](../docs/WORKFLOWS.md) for full documentation. See [Building and Pushing Custom Connectors](../docs/BUILDING_CONNECTORS.md) for connectors.
|
|
1594
|
-
|
|
1595
|
-
## Custom Connectors
|
|
1596
|
-
|
|
1597
|
-
See [Building and Pushing Custom Connectors](../docs/BUILDING_CONNECTORS.md) for full samples of JSON, ConnectorBuilder, and custom execute connectors.
|
|
1598
|
-
|
|
1599
|
-
### REST connectors
|
|
1600
|
-
|
|
1601
|
-
```javascript
|
|
1602
|
-
flosync.registerConnector('my-connector', {
|
|
1603
|
-
_id: 'my-connector',
|
|
1604
|
-
type: 'REST',
|
|
1605
|
-
base_url: 'https://api.example.com',
|
|
1606
|
-
auth: { type: 'bearer', token: process.env.MY_API_KEY },
|
|
1607
|
-
endpoints: [
|
|
1608
|
-
{ name: 'GetUser', method: 'GET', path: '/users/:id' },
|
|
1609
|
-
],
|
|
1610
|
-
});
|
|
1611
|
-
```
|
|
1612
|
-
|
|
1613
|
-
### Custom execute connector
|
|
1614
|
-
|
|
1615
|
-
For full control, provide an `execute` function. The engine calls it with `(action, options, context)`.
|
|
1616
|
-
|
|
1617
|
-
**Connector definition**
|
|
1618
|
-
|
|
1619
|
-
```javascript
|
|
1620
|
-
flosync.registerConnector('my.auth', {
|
|
1621
|
-
_id: 'my.auth',
|
|
1622
|
-
type: 'system',
|
|
1623
|
-
name: 'My Auth',
|
|
1624
|
-
meta: { category: 'authentication' },
|
|
1625
|
-
auth: { type: 'none' },
|
|
1626
|
-
execute: async (action, options, context) => { ... },
|
|
1627
|
-
});
|
|
1628
|
-
```
|
|
1629
|
-
|
|
1630
|
-
Import types for TypeScript: `ConnectorExecuteOptions`, `ConnectorContext`, `ConnectorExecuteFn` from `cliodot`.
|
|
1631
|
-
|
|
1632
|
-
**Execute signature**
|
|
1633
|
-
|
|
1634
|
-
```typescript
|
|
1635
|
-
execute(
|
|
1636
|
-
action: string,
|
|
1637
|
-
options: ConnectorExecuteOptions,
|
|
1638
|
-
context: ConnectorContext
|
|
1639
|
-
): Promise<unknown>
|
|
1640
|
-
```
|
|
1641
|
-
|
|
1642
|
-
**Options** (from workflow step config):
|
|
1643
|
-
|
|
1644
|
-
| Field | When used | Description |
|
|
1645
|
-
|-------|-----------|-------------|
|
|
1646
|
-
| `body` | API, auth, db, encrypt | Request body or payload |
|
|
1647
|
-
| `params` | API | Query params |
|
|
1648
|
-
| `pathParams` | API | Path params (e.g. `:id` in `/users/:id`) |
|
|
1649
|
-
| `headers` | API | Extra headers |
|
|
1650
|
-
| `database` | DB | Database name override |
|
|
1651
|
-
| `connection_id` | DB | Connection ID override |
|
|
1652
|
-
|
|
1653
|
-
**Context** (from run state):
|
|
1654
|
-
|
|
1655
|
-
| Field | Description |
|
|
1656
|
-
|-------|-------------|
|
|
1657
|
-
| `headers` | HTTP headers from the trigger |
|
|
1658
|
-
| `connectorConfig` | Per-connector config from `configure({ connectors: { 'id': {...} } })` |
|
|
1659
|
-
| `env` | Environment overrides |
|
|
1660
|
-
| `trigger` | Trigger payload (body, pathParams, params, etc.) |
|
|
1661
|
-
| `stepResults` | Results of previous steps |
|
|
1662
|
-
| `vars` | Workflow variables |
|
|
1663
|
-
|
|
1664
|
-
**Discovering IDs and actions**
|
|
1665
|
-
|
|
1666
|
-
- Built-in: `ConnectorId`, `ConnectorActions`, `getConnectorActions(connectorId)`, `listBuiltInConnectors()`
|
|
1667
|
-
- Custom: `defineCustomConnector(id, actions)` or `defineCustomConnectorFromList(id, ['action1', 'action2'])`
|
|
1668
|
-
|
|
1669
|
-
**REST endpoint shape**
|
|
1670
|
-
|
|
1671
|
-
```typescript
|
|
1672
|
-
{
|
|
1673
|
-
name: string; // Action name (matches s.connector(id, name, config))
|
|
1674
|
-
action?: string; // Alias for name
|
|
1675
|
-
method: string; // GET, POST, PUT, DELETE, etc.
|
|
1676
|
-
path: string; // e.g. '/users/:id' or '/orders'
|
|
1677
|
-
headers?: Record<string, string>;
|
|
1678
|
-
body_schema?: object;
|
|
1679
|
-
response_mapping?: Record<string, string>; // { "userId": "data.id" }
|
|
1680
|
-
timeout_ms?: number;
|
|
1681
|
-
}
|
|
1682
|
-
```
|
|
69
|
+
## Documentation
|
|
1683
70
|
|
|
1684
|
-
|
|
71
|
+
All guides, API references, and examples live on **[docs.cliodot.com](https://docs.cliodot.com)**:
|
|
1685
72
|
|
|
1686
|
-
|
|
1687
|
-
|
|
73
|
+
| Topic | Docs |
|
|
74
|
+
|-------|------|
|
|
75
|
+
| Getting started | [docs.cliodot.com/getting-started](https://docs.cliodot.com/getting-started) |
|
|
76
|
+
| SDK overview | [docs.cliodot.com/sdk](https://docs.cliodot.com/sdk) |
|
|
77
|
+
| Workflows | [docs.cliodot.com/workflows](https://docs.cliodot.com/workflows) |
|
|
78
|
+
| Functions | [docs.cliodot.com/functions](https://docs.cliodot.com/functions) |
|
|
79
|
+
| Connectors | [docs.cliodot.com/connectors](https://docs.cliodot.com/connectors) |
|
|
80
|
+
| Gateway Surface | [SURFACE.md](./SURFACE.md) |
|
|
81
|
+
| Surface internals | [SURFACE_INTERNALS.md](./SURFACE_INTERNALS.md) |
|
|
82
|
+
| Typed connectors | [TYPED_CONNECTOR_EXAMPLES.md](./TYPED_CONNECTOR_EXAMPLES.md) |
|
|
83
|
+
| API client | [docs.cliodot.com/api-client](https://docs.cliodot.com/api-client) |
|
|
84
|
+
| OAuth apps | [docs.cliodot.com/oauth-apps](https://docs.cliodot.com/oauth-apps) |
|
|
85
|
+
| Auth apps (MFA) | [docs.cliodot.com/auth-apps](https://docs.cliodot.com/auth-apps) |
|
|
86
|
+
| Event Apps | [EVENTS.md](./EVENTS.md) |
|
|
1688
87
|
|
|
1689
88
|
## License
|
|
1690
89
|
|