Tài liệu

Luồng tự động

Luồng tự động Flow là workflow tự động thực thi một chuỗi step theo trigger. Flow hỗ trợ chạy theo lịch (cron) và trigger thủ công. Với luồng dựa trên sự kiện, hãy dùng handler/hook cùng @TRIGGER() . Bảng dữ liệu Bảng Mục đích enfyra_flow Cấu hình flow (name, trigger, timeout, ma

Luồng tự động

Flow là workflow tự động thực thi một chuỗi step theo trigger. Flow hỗ trợ chạy theo lịch (cron) và trigger thủ công. Với luồng dựa trên sự kiện, hãy dùng handler/hook cùng @TRIGGER().

Bảng dữ liệu

Bảng Mục đích
enfyra_flow Cấu hình flow (name, trigger, timeout, maxExecutions)
enfyra_flow_step Các step của flow, có thứ tự, kiểu và cấu hình
enfyra_flow_execution Lịch sử và trạng thái thực thi, truy vấn riêng chứ không lồng trong flow

Kiểu trigger

Kiểu Ví dụ cấu hình Mô tả
schedule {"cron": "0 2 * * *", "timezone": "UTC"} Chạy theo lịch cron
manual {} Do người dùng kích hoạt từ UI, API hoặc @TRIGGER() trong handler/hook

Kiểu step

Kiểu Cấu hình Mô tả
script {"code": "..."} Chạy JavaScript tùy chỉnh với đầy đủ context
condition {"code": "return ..."} Đánh giá điều kiện theo JS truthy/falsy; truthy đi nhánh "true", falsy đi nhánh "false"
query {"table": "...", "filter": {...}, "limit": 10} Truy vấn dữ liệu bảng
create {"table": "...", "data": {...}} Tạo bản ghi
update {"table": "...", "id": 1, "data": {...}} Cập nhật bản ghi theo ID
delete {"table": "...", "id": 1} Xóa bản ghi theo ID
http {"url": "...", "method": "POST", "body": {...}, "headers": {...}, "timeout": 30000} Gửi HTTP request (tự thêm Content-Type: application/json khi có body). Xem quy tắc URL của HTTP step.
trigger_flow {"flowId": 2} hoặc {"flowName": "..."} Kích hoạt flow khác
sleep {"ms": 5000} Tạm dừng thực thi N ms
log {"message": "..."} Ghi message vào execution context

Quy tắc URL cho HTTP step (chống SSRF)

Server chỉ cho phép URL http:https: không phải mục tiêu SSRF rõ ràng: localhost, tên loopback phổ biến, IP private/reserved dạng literal và hostname chỉ phân giải về IP private đều bị từ chối. Hãy ưu tiên hostname công khai trên Internet, ví dụ https://api.example.com. Callback nội bộ cần thay đổi kiến trúc có chủ đích, không dùng tùy tiện URL nội bộ trong step này.

Cú pháp mẫu

Flow step hỗ trợ cùng các macro mẫu như handler/hook, đồng thời có macro riêng cho flow. Code được tự transpile khi đưa vào cache.

Macro Mở rộng thành Mô tả
@FLOW_PAYLOAD $ctx.$flow.$payload Dữ liệu payload đầu vào
@TRIGGER $ctx.$trigger Kích hoạt flow từ handler/hook
@FLOW_LAST $ctx.$flow.$last Kết quả step gần nhất
@FLOW $ctx.$flow Toàn bộ chuỗi dữ liệu flow
@FLOW_META $ctx.$flow.$meta Metadata flow (flowId, flowName, executionId, depth)
#table_name $ctx.$repos.table_name Repository của bảng
@HELPERS $ctx.$helpers Helper (jwt, bcrypt, autoSlug)
@USER $ctx.$user Người dùng hiện tại (null với cron)
@THROW400@THROW503, @THROW $ctx.$throw['400'], …, $ctx.$throw Helper lỗi HTTP (dùng key dạng số, không dùng '4xx')
%package $ctx.$pkgs.package Package đã cài

Chuỗi dữ liệu

Mỗi lần chạy flow giữ một context dùng chung ($ctx.$flow / @FLOW) để truyền dữ liệu giữa các step:

@FLOW
  ├── @FLOW_PAYLOAD      // Input data passed to the flow
  ├── @FLOW_LAST         // Result of the most recent step
  ├── @FLOW_META         // { flowId, flowName, executionId, startedAt }
  ├── @FLOW.step_1  // Result of step with key "step_1"
  └── @FLOW.step_2  // Result of step with key "step_2"

Truy cập dữ liệu trong script step (nên dùng cú pháp mẫu)

// Cú pháp mẫu (khuyến nghị)
const email = @FLOW_PAYLOAD.email;
const user = @FLOW.find_user?.data?.[0];
const prev = @FLOW_LAST;
const orders = await #order.find({
  filter: { userId: { _eq: user.id } }
});
return orders;
// Cú pháp đầy đủ tương đương (không khuyến nghị)
// $ctx.$flow.$payload.email, $ctx.$repos.order, etc.

Rẽ nhánh điều kiện

Condition step hỗ trợ rẽ nhánh true/false. Child step tham chiếu condition qua relation parent và trường branch.

[query_users]              ← root step (parent: null)
    ↓
[check_has_users]          ← root step, type: condition
    ├── true:
    │   [process_users]    ← parent: check_has_users, branch: "true"
    │   [send_report]      ← parent: check_has_users, branch: "true"
    └── false:
        [log_empty]        ← parent: check_has_users, branch: "false"
    ↓
[cleanup]                  ← root step (continues after condition)

Tạo branch step qua API:

POST /api/enfyra_flow_step
{
  "flow": { "id": 1 },
  "key": "process_users",
  "stepOrder": 1,
  "type": "script",
  "config": { "code": "return #enfyra_user.find({ limit: 100 })" },
  "parent": { "id": 5 },
  "branch": "true"
}

Quy tắc: - Step có parent: null nằm ở root level và chạy tuần tự - Condition dùng JS truthy/falsy: return user (object = truthy), return null (falsy), return count > 0 (boolean) - Child truthy có branch: "true" được chạy - Child falsy có branch: "false" được chạy - Sau khi condition và child branch hoàn tất, root step kế tiếp tiếp tục - Mỗi branch step có thể đặt onError: skip/stop/retry độc lập

Xử lý lỗi

Chiến lược Hành vi
stop Dừng flow, đánh dấu lần chạy thất bại (mặc định)
skip Ghi nhận lỗi và tiếp tục step kế tiếp
retry Thử lại với exponential backoff tối đa retryAttempts lần

Endpoint quản trị

Endpoint Mô tả
POST /admin/flow/trigger/:id Kích hoạt một lần chạy flow qua BullMQ
POST /admin/test/run Kiểm thử flow step không cần lưu bằng kind: "flow_step"

Kích hoạt flow từ handler

await @TRIGGER('send-welcome-email', { userId: @USER.id });
await @TRIGGER(5, { orderId: @PARAMS.id, total: 100 });

Lịch sử thực thi

Truy vấn riêng bản ghi thực thi, không lồng bên dưới flow:

GET /api/enfyra_flow_execution?filter={"flow":{"_eq":1}}&sort=-id&limit=10

Mỗi bản ghi thực thi gồm: - status: pending, running, completed, failed, cancelled - currentStep: step nơi flow dừng hoặc đang chạy - completedSteps: mảng key của các step đã hoàn tất thành công - error: chi tiết lỗi khi thất bại (message + stack) - duration: tổng thời gian chạy tính bằng ms

Ví dụ: flow xử lý đơn hàng

1. Tạo flow

POST /api/enfyra_flow
{
  "name": "process-order",
  "triggerType": "manual",
  "timeout": 30000,
  "isEnabled": true
}

Sau đó kích hoạt từ post-hook của /order:

await @TRIGGER('process-order', { data: @DATA });

2. Thêm step

POST /api/enfyra_flow_step
{
  "flow": { "id": 1 },
  "key": "validate_stock",
  "stepOrder": 1,
  "type": "script",
  "config": {
    "code": "const order = @FLOW_PAYLOAD.data; const product = await #product.find({ filter: { id: { _eq: order.productId } }, limit: 1 }); return { inStock: product.data[0]?.stock > order.quantity }"
  },
  "timeout": 5000,
  "onError": "stop"
}

3. Kiểm thử step trước khi lưu

POST /api/admin/test/run
{
  "kind": "flow_step",
  "type": "query",
  "config": { "table": "enfyra_user", "filter": { "status": { "_eq": "active" } }, "limit": 5 },
  "timeout": 5000
}

Response: { "success": true, "result": { "data": [...] }, "duration": 42 }

4. Kích hoạt thủ công

POST /api/admin/flow/trigger/1
{ "payload": { "orderId": 123 } }

5. Xem lịch sử thực thi

GET /api/enfyra_flow_execution?filter={"flow":{"_eq":1}}&sort=-id&limit=10

Ví dụ flow chạy theo lịch

POST /api/enfyra_flow
{
  "name": "daily-cleanup",
  "triggerType": "schedule",
  "triggerConfig": { "cron": "0 2 * * *", "timezone": "Asia/Ho_Chi_Minh" },
  "timeout": 60000,
  "isEnabled": true
}

Lịch cron được đăng ký tự động bằng BullMQ Job Scheduler khi cache của flow reload.

An toàn và giới hạn

Hạng mục Chi tiết
Độ sâu lồng tối đa 10 (flow kích hoạt flow qua trigger_flow hoặc $trigger())
Phát hiện vòng lặp Theo dõi flow ID đã đi qua trong chuỗi; ABA bị từ chối ngay
HTTP timeout Mặc định 30 giây, cấu hình từng step bằng config.timeout
Lịch sử thực thi Tự dọn theo maxExecutions của flow, mặc định 100 và xóa bản ghi cũ nhất
Step timeout Theo từng step hoặc kế thừa timeout của flow, mặc định 5000ms
Retry backoff Exponential: 1s, 2s, 4s, 8s... tối đa 30 giây

Kích hoạt flow từ flow step

Flow step có $trigger() giống handler/hook:

// Bên trong một script step
const result = await @TRIGGER('send-notification', { userId: @FLOW_PAYLOAD.userId });

Lời gọi này tuân theo cùng giới hạn độ sâu lồng và phát hiện vòng lặp.