cliodot 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +646 -9
- package/dist/ConnectorBuilder.js +1 -1
- package/dist/Flosync.d.ts +5 -0
- package/dist/Flosync.d.ts.map +1 -1
- package/dist/Flosync.js +1 -1
- package/dist/FlosyncClient.d.ts +2 -0
- package/dist/FlosyncClient.d.ts.map +1 -1
- package/dist/FlosyncClient.js +1 -1
- package/dist/FunctionBuilder.js +1 -1
- package/dist/ValidatorBuilder.js +1 -1
- package/dist/WorkflowBuilder.d.ts +6 -3
- package/dist/WorkflowBuilder.d.ts.map +1 -1
- package/dist/WorkflowBuilder.js +1 -1
- package/dist/__tests__/function.test.js +1 -1
- package/dist/__tests__/runner.test.js +1 -1
- package/dist/connectors/builtin.d.ts +30 -0
- package/dist/connectors/builtin.d.ts.map +1 -1
- package/dist/connectors/builtin.js +1 -1
- package/dist/connectors/registry.d.ts +393 -18
- package/dist/connectors/registry.d.ts.map +1 -1
- package/dist/connectors/registry.js +1 -1
- package/dist/decorators/build.d.ts +1 -0
- package/dist/decorators/build.d.ts.map +1 -1
- package/dist/decorators/build.js +1 -1
- package/dist/decorators/connector.d.ts +14 -0
- package/dist/decorators/connector.d.ts.map +1 -0
- package/dist/decorators/connector.js +1 -0
- package/dist/decorators/index.d.ts +4 -3
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +1 -1
- package/dist/decorators/metadata.d.ts +39 -6
- package/dist/decorators/metadata.d.ts.map +1 -1
- package/dist/decorators/metadata.js +1 -1
- package/dist/decorators/steps.d.ts +26 -10
- package/dist/decorators/steps.d.ts.map +1 -1
- package/dist/decorators/steps.js +1 -1
- package/dist/decorators/workflow.js +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.d.ts +2 -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/builtin.d.ts +92 -3
- package/dist/types/builtin.d.ts.map +1 -1
- 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.d.ts +75 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/context.js +1 -0
- package/dist/types/function.js +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/validator.js +1 -1
- package/dist/types/workflow.d.ts +117 -6
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +1 -1
- package/dist/validators/validators.d.ts.map +1 -1
- package/dist/validators/validators.js +1 -1
- package/dist/variable.js +1 -1
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -99,6 +99,7 @@ flosync.configure({
|
|
|
99
99
|
apiSecret: process.env.FLOSYNC_API_SECRET,
|
|
100
100
|
baseUrl: 'http://localhost:8080' //optional,
|
|
101
101
|
projectId: process.env.FLOSYNC_PROJECT_ID,
|
|
102
|
+
debug: false, // Enable to include stepResults in every response
|
|
102
103
|
connectors: {
|
|
103
104
|
'mongodb.system': { uri: process.env.MONGO_URI, database: 'myapp' },
|
|
104
105
|
'mysql.system': { uri: process.env.MYSQL_URI },
|
|
@@ -235,15 +236,186 @@ The overridden output becomes the value of `v.stepResult("<stepId>")` for subseq
|
|
|
235
236
|
|
|
236
237
|
In `stage: "post"`, `ctx.input` is the decorated step output produced by the connector/function call.
|
|
237
238
|
|
|
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
|
|
239
391
|
|
|
240
392
|
```typescript
|
|
241
393
|
import { flosync, v, Workflow, Http, Connector, Condition, Responder, buildWorkflowFromClass, defineCustomConnector } from "cliodot";
|
|
394
|
+
import type { WorkflowContext, StepOutput } from "cliodot";
|
|
242
395
|
|
|
243
396
|
const CarrierStore = defineCustomConnector("carrier.store", {
|
|
244
397
|
listTodos: "listTodos",
|
|
245
398
|
});
|
|
246
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
|
+
|
|
247
419
|
@Workflow("todos-list-decorators", "Todos List (Decorators)")
|
|
248
420
|
@Http("GET", "/todos")
|
|
249
421
|
class TodosListDecoratorsWorkflow {
|
|
@@ -253,13 +425,38 @@ class TodosListDecoratorsWorkflow {
|
|
|
253
425
|
{ params: { limit: 10 } },
|
|
254
426
|
{ order: 0, stage: "post" }
|
|
255
427
|
)
|
|
256
|
-
list(ctx:
|
|
257
|
-
const
|
|
258
|
-
const
|
|
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) => {
|
|
259
438
|
const title = String(t?.title ?? "").trim();
|
|
260
|
-
return {
|
|
439
|
+
return {
|
|
440
|
+
...t,
|
|
441
|
+
title,
|
|
442
|
+
title_upper: title.toUpperCase(),
|
|
443
|
+
completed_label: t?.completed ? "done" as const : "todo" as const,
|
|
444
|
+
};
|
|
261
445
|
});
|
|
262
|
-
|
|
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
|
+
};
|
|
263
460
|
}
|
|
264
461
|
|
|
265
462
|
@Condition(v.stepResult("list", "valid"), { then: "ok", else: "unauth", order: 1 })
|
|
@@ -421,6 +618,123 @@ Example validation failure response body:
|
|
|
421
618
|
|
|
422
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).
|
|
423
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
|
+
|
|
424
738
|
### Custom `execute` connector plus decorators (catalog sample)
|
|
425
739
|
|
|
426
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.
|
|
@@ -503,10 +817,10 @@ class CatalogListDecoratorsWorkflow {
|
|
|
503
817
|
{ params: { limit: v.query("limit") } },
|
|
504
818
|
{ order: 0, stage: "post" }
|
|
505
819
|
)
|
|
506
|
-
list(ctx: any) {
|
|
820
|
+
list(ctx: WorkflowContext<{ products?: CatalogProduct[]; [k: string]: any }>): StepOutput<{ products: any[]; count: number }> {
|
|
507
821
|
const input = ctx?.input ?? {};
|
|
508
822
|
const items = Array.isArray(input.products) ? input.products : [];
|
|
509
|
-
const withLabels = items.map((p
|
|
823
|
+
const withLabels = items.map((p) => ({
|
|
510
824
|
...p,
|
|
511
825
|
price_display: `$${Number(p.price).toFixed(2)}`,
|
|
512
826
|
}));
|
|
@@ -540,7 +854,7 @@ class CatalogCreateDecoratorsWorkflow {
|
|
|
540
854
|
{ body: { name: v.body("name"), price: v.body("price") } },
|
|
541
855
|
{ order: 0, stage: "post" }
|
|
542
856
|
)
|
|
543
|
-
save(ctx: any) {
|
|
857
|
+
save(ctx: WorkflowContext<{ ok?: boolean; product?: CatalogProduct; [k: string]: any }>): StepOutput<any> {
|
|
544
858
|
const input = ctx?.input ?? {};
|
|
545
859
|
if (!input.ok) return { out: input };
|
|
546
860
|
const p = input.product as CatalogProduct;
|
|
@@ -699,6 +1013,192 @@ s.responder('redirect', { location: 'https://example.com', statusCode: 301 });
|
|
|
699
1013
|
s.responder('empty', { statusCode: 202 });
|
|
700
1014
|
```
|
|
701
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
|
+
|
|
702
1202
|
## Encryption
|
|
703
1203
|
|
|
704
1204
|
Use `s.encrypt(connectorId, action, config)` with built-in encryption connectors. All use Node `crypto`; Argon2 and bcrypt require optional packages.
|
|
@@ -785,6 +1285,143 @@ import { defineCustomConnectorFromList } from 'cliodot';
|
|
|
785
1285
|
const MyStore = defineCustomConnectorFromList('my.store', ['login', 'listItems', 'createItem']);
|
|
786
1286
|
```
|
|
787
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
|
+
|
|
788
1425
|
## Connectors by ID
|
|
789
1426
|
|
|
790
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': { ... } } })`.
|