cliodot 1.1.0 → 1.2.1

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