diff --git a/content/docs/build/automation/approvals.mdx b/content/docs/build/automation/approvals.mdx index 7e95d65..38603e4 100644 --- a/content/docs/build/automation/approvals.mdx +++ b/content/docs/build/automation/approvals.mdx @@ -42,6 +42,7 @@ cross-tenant or cross-owner reach — elevation is opt-in and auditable. The node declares **who approves** and how their decisions aggregate: ```ts +// One flow node; the flow around it is omitted (see the complete flow below). { id: 'manager_approval', type: 'approval', @@ -102,6 +103,7 @@ Beyond `first_response` and `unanimous`, 16.0 adds two aggregation modes. **Quorum** finalizes on M-of-N approvals: ```ts +// The `config` of an `approval` node; the node around it is omitted. config: { approvers: [ { type: 'user', value: 'director_a' }, @@ -118,6 +120,7 @@ approvers with `group`, and the node advances only once every group has `minApprovals` approvals (default 1 per group): ```ts +// The `config` of an `approval` node; the node around it is omitted. config: { approvers: [ { type: 'position', value: 'legal_counsel', group: 'legal' }, @@ -151,17 +154,22 @@ including upstream node outputs). The result becomes the approver slate: ```ts +// The `config` of an `approval` node; the node around it is omitted. config: { approvers: [ - { type: 'expression', value: 'vars.legal_review.picked_departments' }, + { + type: 'expression', + value: 'vars.legal_review.picked_departments', + resolveAs: 'department', // each value is a department id, fanned out to its members + }, ], - resolveAs: 'department', // each value is a department id, fanned out to its members } ``` -`resolveAs` (`'user'` by default, or `'department'` / `'position'` / -`'team'`) says what kind of id the expression returned, and expands each -through the same graph lookups the static approver types use. Under +`resolveAs`, a key of the `expression` approver itself (`'user'` by default, +or `'department'` / `'position'` / `'team'`), says what kind of id the +expression returned, and expands each through the same graph lookups the +static approver types use. Under `behavior: 'per_group'`, each returned value forms its own group — one sign-off per returned department. @@ -176,9 +184,11 @@ fill in values. Accepted outputs resume the run as cannot shadow an author's variable: ```ts +// One flow node; the flow around it is omitted. { id: 'legal_review', type: 'approval', + label: 'Legal Review', config: { approvers: [{ type: 'position', value: 'legal_counsel' }], decisionOutputs: [ @@ -224,6 +234,7 @@ export const opportunityApproval = defineFlow({ { id: 'start', type: 'start', + label: 'Proposal over 50,000', config: { triggerType: 'record-after-update', objectName: 'opportunity', @@ -241,9 +252,27 @@ export const opportunityApproval = defineFlow({ lockRecord: true, }, }, - { id: 'mark_approved', type: 'update_record', label: 'Mark Approved' }, - { id: 'mark_rejected', type: 'update_record', label: 'Mark Rejected' }, - { id: 'end', type: 'end' }, + { + id: 'mark_approved', + type: 'update_record', + label: 'Mark Approved', + config: { + objectName: 'opportunity', + filter: { id: '{record.id}' }, + fields: { stage: 'negotiation' }, + }, + }, + { + id: 'mark_rejected', + type: 'update_record', + label: 'Mark Rejected', + config: { + objectName: 'opportunity', + filter: { id: '{record.id}' }, + fields: { stage: 'closed_lost' }, + }, + }, + { id: 'end', type: 'end', label: 'End' }, ], edges: [ { id: 'e1', source: 'start', target: 'manager_approval' }, diff --git a/content/docs/build/automation/index.mdx b/content/docs/build/automation/index.mdx index a252ea4..05fcf4e 100644 --- a/content/docs/build/automation/index.mdx +++ b/content/docs/build/automation/index.mdx @@ -11,7 +11,7 @@ description: Pick the right tool for the job — flows for steps, workflows for | You need | Use | Read | |---|---|---| | "When X happens, do Y" — steps that run on a record change, a schedule, or a button click | **Flow** | [Flows](/docs/build/automation/flows) | -| "This record may only move through these states, by these events" — a controlled lifecycle | **Workflow** (state machine) | [Workflows](/docs/build/automation/workflows) | +| "This record may only move through these states" — a controlled lifecycle | **Workflow** (state machine) | [Workflows](/docs/build/automation/workflows) | | "A person must sign off before this proceeds" — routed human decisions | **Approval** | [Approvals](/docs/build/automation/approvals) | Rule of thumb: model **state** with workflows, model **steps** with flows. Approvals are not a separate engine — an approval is a flow that pauses at an approval node until a human decides. @@ -20,7 +20,7 @@ Rule of thumb: model **state** with workflows, model **steps** with flows. Appro The three compose rather than compete: -- A **workflow** constrains which lifecycle transitions are valid at all — states, transitions, and guards, nothing else. +- A **workflow** constrains which lifecycle transitions are valid at all — states and the moves between them, nothing else. - A **flow** performs the side effects around those transitions: send an email, update records, call an external service, wait, branch. - An **approval node** inside a flow blocks execution until an approver acts, then resumes down the approve or reject branch. @@ -33,6 +33,6 @@ So "a case moves new → assigned → resolved" is a workflow; "notify the manag | Page | Why | |---|---| | [Flows](/docs/build/automation/flows) | The full flow reference — triggers, step types, error handling, CEL | -| [Workflows](/docs/build/automation/workflows) | States, transitions, and guards for strict lifecycles | +| [Workflows](/docs/build/automation/workflows) | States and their allowed transitions, for strict lifecycles | | [Approvals](/docs/build/automation/approvals) | Approval nodes, run-as identity, and the approver experience | -| [CEL expressions](/docs/reference/cel) | The expression language used in conditions and guards | +| [CEL expressions](/docs/reference/cel) | The expression language used in conditions and validation rules | diff --git a/content/docs/build/automation/index.zh-Hans.mdx b/content/docs/build/automation/index.zh-Hans.mdx deleted file mode 100644 index 327b18b..0000000 --- a/content/docs/build/automation/index.zh-Hans.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: 自动化 -description: 为任务选对工具 —— 流程管步骤,工作流管状态,审批管人工签核。 -translation: - source_sha: 1d72c84fc5db49a7703cac9e3227cf8467acb167c1cc65ea30767b9e410a0ba5 - guide_rev: 1 - mode: auto ---- - -**自动化以声明的方式把业务逻辑挂到数据模型上 —— 作为由运行时执行的元数据 —— 而不是散落在应用代码里。**三个工具覆盖全部场景,一开始就选对,能省去日后返工。 - -## 我该用哪一个? - -| 你需要 | 用 | 阅读 | -|---|---|---| -| "X 发生时,做 Y" —— 由记录变更、定时或按钮点击驱动的步骤 | **流程** | [流程](/docs/build/automation/flows) | -| "这条记录只能按这些事件在这些状态间移动" —— 受控的生命周期 | **工作流**(状态机) | [工作流](/docs/build/automation/workflows) | -| "必须有人签核后才能继续" —— 路由给人的决策 | **审批** | [审批流程](/docs/build/automation/approvals) | - -经验法则:**状态**用工作流建模,**步骤**用流程建模。审批不是独立引擎 —— 审批就是在审批节点暂停、等人决策的流程。 - -## 它们如何组合 - -三者是互补而非竞争: - -- **工作流**约束哪些生命周期迁移是合法的 —— 状态、迁移、守卫条件,仅此而已。 -- **流程**执行这些迁移周边的副作用:发邮件、更新记录、调用外部服务、等待、分支。 -- 流程内的**审批节点**阻塞执行直到审批人行动,然后沿批准或拒绝分支继续。 - -所以"工单从 new → assigned → resolved"是工作流;"升级时通知经理"是流程;"超过 5 万美元的折扣需要财务经理签核"是带审批节点的流程。 - -> **提示:**如果需求读起来是"X 发生时,做 Y",那就是流程。如果读起来是"这条记录绝不能跳过某个状态",那就是工作流。从这里出发,你几乎不会选错。 - -## 下一步 - -| 页面 | 原因 | -|---|---| -| [流程](/docs/build/automation/flows) | 完整的流程参考 —— 触发器、步骤类型、错误处理、CEL | -| [工作流](/docs/build/automation/workflows) | 严格生命周期的状态、迁移与守卫条件 | -| [审批流程](/docs/build/automation/approvals) | 审批节点、运行身份与审批人体验 | -| [CEL 表达式](/docs/reference/cel) | 条件与守卫条件所用的表达式语言 | diff --git a/content/docs/build/automation/index.zh-Hant.mdx b/content/docs/build/automation/index.zh-Hant.mdx deleted file mode 100644 index 6eda455..0000000 --- a/content/docs/build/automation/index.zh-Hant.mdx +++ /dev/null @@ -1,42 +0,0 @@ ---- -# @generated zh-Hant from zh-Hans -- do not edit. Edit the English source, then regenerate: pnpm --filter @objectos/docs gen:zh-hant -title: 自動化 -description: 為任務選對工具 —— 流程管步驟,工作流管狀態,審批管人工籤核。 -translation: - source_sha: 1d72c84fc5db49a7703cac9e3227cf8467acb167c1cc65ea30767b9e410a0ba5 - guide_rev: 1 - mode: auto ---- - -**自動化以宣告的方式把業務邏輯掛到資料模型上 —— 作為由執行時執行的後設資料 —— 而不是散落在應用程式碼裡。**三個工具覆蓋全部場景,一開始就選對,能省去日後返工。 - -## 我該用哪一個? - -| 你需要 | 用 | 閱讀 | -|---|---|---| -| "X 發生時,做 Y" —— 由記錄變更、定時或按鈕點選驅動的步驟 | **流程** | [流程](/docs/build/automation/flows) | -| "這條記錄只能按這些事件在這些狀態間移動" —— 受控的生命週期 | **工作流**(狀態機) | [工作流](/docs/build/automation/workflows) | -| "必須有人籤核後才能繼續" —— 路由給人的決策 | **審批** | [審批流程](/docs/build/automation/approvals) | - -經驗法則:**狀態**用工作流建模,**步驟**用流程建模。審批不是獨立引擎 —— 審批就是在審批節點暫停、等人決策的流程。 - -## 它們如何組合 - -三者是互補而非競爭: - -- **工作流**約束哪些生命週期遷移是合法的 —— 狀態、遷移、守衛條件,僅此而已。 -- **流程**執行這些遷移周邊的副作用:發郵件、更新記錄、呼叫外部服務、等待、分支。 -- 流程內的**審批節點**阻塞執行直到審批人行動,然後沿批准或拒絕分支繼續。 - -所以"工單從 new → assigned → resolved"是工作流;"升級時通知經理"是流程;"超過 5 萬美元的折扣需要財務經理籤核"是帶審批節點的流程。 - -> **提示:**如果需求讀起來是"X 發生時,做 Y",那就是流程。如果讀起來是"這條記錄絕不能跳過某個狀態",那就是工作流。從這裡出發,你幾乎不會選錯。 - -## 下一步 - -| 頁面 | 原因 | -|---|---| -| [流程](/docs/build/automation/flows) | 完整的流程參考 —— 觸發器、步驟型別、錯誤處理、CEL | -| [工作流](/docs/build/automation/workflows) | 嚴格生命週期的狀態、遷移與守衛條件 | -| [審批流程](/docs/build/automation/approvals) | 審批節點、執行身份與審批人體驗 | -| [CEL 表示式](/docs/reference/cel) | 條件與守衛條件所用的表示式語言 | diff --git a/content/docs/build/automation/workflows.mdx b/content/docs/build/automation/workflows.mdx index 434032d..b042554 100644 --- a/content/docs/build/automation/workflows.mdx +++ b/content/docs/build/automation/workflows.mdx @@ -1,15 +1,15 @@ --- title: Workflows seoTitle: "Workflows: Record Lifecycle as a State Machine" -description: Model a record's lifecycle as a state machine — valid states, guarded transitions, and nothing a flow can do better. +description: Model a record's lifecycle as a state machine — the states it can be in, the moves allowed between them, and nothing a flow can do better. --- -**A workflow models a record's lifecycle as a finite state machine: the states the record can be in, the events that move it between them, and the guards that must hold for a move to happen.** Use one when the core requirement is "this object can only move through these states by these events." +**A workflow models a record's lifecycle as a finite state machine: the states the record can be in and the moves allowed between them.** You declare it as a `state_machine` validation rule on the object, and every write to the state field is checked against it. Use one when the core requirement is "this object can only move through these states." There is no standalone Salesforce-style Workflow Rule authoring type. The old "workflow" concept splits cleanly in two: -- **State machine metadata** for strict lifecycle transitions — this page. +- **A `state_machine` validation rule** for strict lifecycle transitions — this page. - **[Flows](/docs/build/automation/flows)** for event-triggered or scheduled automation, including [approval](/docs/build/automation/approvals) pauses. @@ -19,7 +19,7 @@ There is no standalone Salesforce-style Workflow Rule authoring type. The old |---|---|---| | Models | *State* — where a record is in its lifecycle | *Steps* — what happens when something occurs | | Answers | "Is this transition allowed right now?" | "What do we do about it?" | -| Shape | States, transitions, guards | Nodes and edges: triggers, actions, branches | +| Shape | A state field and a table of allowed moves | Nodes and edges: triggers, actions, branches | | Side effects | None — it only constrains | All of them — email, updates, HTTP, waits | They compose: the state machine constrains the transitions; flows perform the @@ -29,95 +29,138 @@ external calls. ## Define a state machine A support case that must move `new → assigned → resolved`, with an escalation -path: +path. The state machine is one of the object's `validations`: ```ts -import type { StateMachineConfig } from '@objectstack/spec/automation'; - -export const caseLifecycle: StateMachineConfig = { - id: 'case_lifecycle', - initial: 'new', - states: { - new: { - on: { - ASSIGN: { target: 'assigned' }, - }, - }, - assigned: { - on: { - RESOLVE: { target: 'resolved', cond: 'has_resolution' }, - ESCALATE: { target: 'escalated' }, - }, - }, - escalated: { - on: { - RESOLVE: { target: 'resolved', cond: 'has_resolution' }, +import { P } from '@objectstack/spec'; +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const SupportCase = ObjectSchema.create({ + name: 'support_case', + label: 'Support Case', + fields: { + status: Field.select({ + label: 'Status', + required: true, + options: [ + { label: 'New', value: 'new', default: true }, + { label: 'Assigned', value: 'assigned' }, + { label: 'Escalated', value: 'escalated' }, + { label: 'Resolved', value: 'resolved' }, + ], + }), + resolution: Field.textarea({ + label: 'Resolution', + requiredWhen: P`record.status == 'resolved'`, // no resolved case without one + }), + }, + validations: [ + { + type: 'state_machine', + name: 'case_lifecycle', + field: 'status', + message: 'This status change is not allowed.', + initialStates: ['new'], + transitions: { + new: ['assigned'], + assigned: ['resolved', 'escalated'], + escalated: ['resolved'], + resolved: [], }, }, - resolved: { - type: 'final', - }, - }, -}; + ], +}); ``` -Read it as a contract: a `new` case can only be assigned. An `assigned` case -can be resolved — but only if the `has_resolution` guard passes — or escalated. -A `resolved` case is final; nothing moves it again. +Read it as a contract: a case is created as `new`, and a `new` case can only be +assigned. An `assigned` case can be resolved or escalated, and an escalated one +can be resolved. `resolved` lists nothing, so nothing moves it again. Because +`resolution` is required when the status is `resolved`, the write that resolves +a case without one is refused. ## Anatomy | Key | What it declares | |---|---| -| `id` | The state machine's identifier | -| `initial` | The state every new record starts in | -| `states` | Map of state name → its outgoing transitions | -| `on` | Events this state responds to (`ASSIGN`, `RESOLVE`, …) | -| `target` | The state an event moves the record to | -| `cond` | A guard that must hold for the transition to fire | -| `type: 'final'` | Terminal state — no outgoing transitions | - -### Guards - -A guard (`cond`) makes a transition conditional: in the example above, -`RESOLVE` only reaches `resolved` when `has_resolution` holds. Guards are how -you encode "you can't close a case without a resolution" as a structural rule -instead of a validation scattered through UI code. - -### Events, not field writes - -Transitions fire on named **events** (`ASSIGN`, `ESCALATE`), not on arbitrary -status-field edits. That's the point: the machine defines the complete set of -legal moves, and anything not declared is impossible. - -> **Tip:** Keep the machine minimal — states, transitions, guards. The moment -> you want "and then send an email", you've left workflow territory: put the -> side effect in a [flow](/docs/build/automation/flows) that reacts to the +| `type: 'state_machine'` | This validation rule is a state machine | +| `name` | The rule's identifier (snake_case) | +| `field` | The state field the rule governs | +| `transitions` | Map of each state → the states it may move to; checked when the record is updated | +| `initialStates` | The states a record may be created in; checked when it is inserted | +| `message` | The error a refused write returns | + +The rule also takes the keys every validation rule shares: `severity`, +`events`, `active` and `priority`. + +### Final states + +A state whose list is empty is final: every move out of it is refused. A state +with no key at all is not final. The check has nothing to reason about there +and lets the write through, so declare `[]` when you mean "final". + +### Conditions on a transition + +A `state_machine` rule has no per-transition guard. To make a move conditional, +put the condition where the platform enforces it: + +- **A value a state needs** — `requiredWhen` on that field, as `resolution` + does above. It refuses the write that enters the state without the value, + and leaves rows that predate the rule alone. +- **Anything else** — a sibling `script` or `conditional` validation rule (a + CEL predicate) in the same `validations` array. + +### Writes, not events + +The machine is checked on writes to the state field. An update that changes +`status` to a value not listed under its current value is refused, and so is +an insert outside `initialStates`. There are no named events to declare: the +table of legal moves is the whole machine. + +> **Tip:** Keep the machine minimal — states and the moves between them. The +> moment you want "and then send an email", you've left workflow territory: put +> the side effect in a [flow](/docs/build/automation/flows) that reacts to the > transition. ## Side effects belong in flows -State machines describe valid transitions and guards — nothing else. When a +State machines describe valid transitions — nothing else. When a transition should *do* something (notify sales, stamp a closed date, call an external system), pair the machine with a flow triggered by the record change: ```ts export const dealClosedWon = defineFlow({ name: 'deal_closed_won', + label: 'Deal closed won', type: 'record_change', + status: 'active', nodes: [ { id: 'start', type: 'start', + label: 'Stage moved to closed won', config: { triggerType: 'record-after-update', objectName: 'opportunity', condition: "record.stage == 'closed_won' && previous.stage != 'closed_won'", }, }, - { id: 'set_closed_date', type: 'update_record', label: 'Set Closed Date' }, - { id: 'notify_sales', type: 'notify', label: 'Notify Sales' }, - { id: 'end', type: 'end' }, + { + id: 'set_closed_date', + type: 'update_record', + label: 'Set Closed Date', + config: { + objectName: 'opportunity', + filter: { id: '{record.id}' }, + fields: { closed_date: '{TODAY()}' }, + }, + }, + { + id: 'notify_sales', + type: 'notify', + label: 'Notify Sales', + config: { recipients: '{record.owner}', title: 'Deal won: {record.name}' }, + }, + { id: 'end', type: 'end', label: 'End' }, ], edges: [ { id: 'e1', source: 'start', target: 'set_closed_date' }, @@ -149,8 +192,8 @@ The test: if the old rule was "when X happens, do Y", model it as a small flow. If it was "N days before/after a date field, do Y", declare a time-relative trigger — never a date-equality condition on a record-change flow, which only evaluates when the record happens to change. If it was -"this record must move through controlled states", model the lifecycle as a -state machine and use flows for the side effects. +"this record must move through controlled states", declare a `state_machine` +validation rule and use flows for the side effects. ## Where to go next @@ -158,6 +201,7 @@ state machine and use flows for the side effects. |---|---| | [Flows](/docs/build/automation/flows) | Side effects around transitions — triggers, steps, error handling | | [Approvals](/docs/build/automation/approvals) | Human sign-off as a pause inside a flow | -| [CEL expressions](/docs/reference/cel) | The language behind guards and flow conditions | +| [Validation rules](/docs/build/data/validation-rules) | The rule family a state machine belongs to | +| [CEL expressions](/docs/reference/cel) | The language behind `requiredWhen`, validation rules and flow conditions | | [Data modeling](/docs/build/data) | The objects whose lifecycles you're constraining | | [Automation overview](/docs/build/automation) | Chooser: flow vs workflow vs approval | diff --git a/content/docs/build/automation/workflows.zh-Hans.mdx b/content/docs/build/automation/workflows.zh-Hans.mdx deleted file mode 100644 index 2d4e9b8..0000000 --- a/content/docs/build/automation/workflows.zh-Hans.mdx +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: 工作流 -description: 把记录的生命周期建模成状态机 —— 合法状态、带守卫条件的迁移,其余的交给流程。 -translation: - source_sha: e87f0ff97a1c1f9972cedc7b7eab08e5ecb22406add0a1087b31945e00965c53 - guide_rev: 1 - mode: auto ---- - -**工作流把记录的生命周期建模为有限状态机:记录可以处于的状态、在状态间移动它的事件,以及移动成立所必须满足的守卫条件。**当核心需求是"这个对象只能按这些事件在这些状态间移动"时,就用它。 - -不存在独立的 Salesforce 式 Workflow Rule 编写类型。旧的"工作流"概念被干净地一分为二: - -- **状态机元数据**负责严格的生命周期迁移 —— 即本页。 -- **[流程](/docs/build/automation/flows)**负责事件触发或定时的自动化,包括[审批](/docs/build/automation/approvals)暂停。 - -## 工作流 vs 流程 - -| | 工作流(状态机) | 流程 | -|---|---|---| -| 建模 | *状态* —— 记录处于生命周期的哪里 | *步骤* —— 某事发生时做什么 | -| 回答 | "此刻允许这个迁移吗?" | "我们要对它做什么?" | -| 形态 | 状态、迁移、守卫条件 | 节点与边:触发器、动作、分支 | -| 副作用 | 无 —— 它只做约束 | 全都有 —— 邮件、更新、HTTP、等待 | - -两者组合使用:状态机约束迁移;当你需要通知、更新记录或调用外部系统时,由流程执行迁移周边的副作用。 - -## 定义状态机 - -一个必须按 `new → assigned → resolved` 移动、且带升级路径的支持工单: - -```ts -import type { StateMachineConfig } from '@objectstack/spec/automation'; - -export const caseLifecycle: StateMachineConfig = { - id: 'case_lifecycle', - initial: 'new', - states: { - new: { - on: { - ASSIGN: { target: 'assigned' }, - }, - }, - assigned: { - on: { - RESOLVE: { target: 'resolved', cond: 'has_resolution' }, - ESCALATE: { target: 'escalated' }, - }, - }, - escalated: { - on: { - RESOLVE: { target: 'resolved', cond: 'has_resolution' }, - }, - }, - resolved: { - type: 'final', - }, - }, -}; -``` - -把它当契约来读:`new` 状态的工单只能被分派。`assigned` 状态的工单可以被解决 —— 但只有 `has_resolution` 守卫条件通过时 —— 或者被升级。`resolved` 是终态;再没有什么能移动它。 - -## 解剖 - -| 键 | 声明什么 | -|---|---| -| `id` | 状态机的标识符 | -| `initial` | 每条新记录的初始状态 | -| `states` | 状态名 → 其出向迁移的映射 | -| `on` | 该状态响应的事件(`ASSIGN`、`RESOLVE`……) | -| `target` | 事件把记录移动到的状态 | -| `cond` | 迁移触发前必须满足的守卫条件 | -| `type: 'final'` | 终态 —— 没有出向迁移 | - -### 守卫条件 - -守卫条件(`cond`)让迁移带上前提:上例中,只有当 `has_resolution` 成立时 `RESOLVE` 才能到达 `resolved`。守卫条件把"没有解决方案就不能关单"编码成结构性规则,而不是散落在 UI 代码里的验证。 - -### 事件,而非字段写入 - -迁移由具名**事件**(`ASSIGN`、`ESCALATE`)触发,而不是对状态字段的任意编辑。这正是重点:状态机定义了合法移动的完整集合,未声明的一律不可能发生。 - -> **提示:**保持状态机最小 —— 状态、迁移、守卫条件。一旦你想"然后再发封邮件",你就已经离开了工作流的领地:把副作用放进一个响应该迁移的[流程](/docs/build/automation/flows)。 - -## 副作用属于流程 - -状态机只描述合法迁移与守卫条件 —— 别无其他。当某次迁移应该*做*点什么(通知销售、写入关闭日期、调用外部系统)时,给状态机配上一个由记录变更触发的流程: - -```ts -export const dealClosedWon = defineFlow({ - name: 'deal_closed_won', - type: 'record_change', - nodes: [ - { - id: 'start', - type: 'start', - config: { - triggerType: 'record-after-update', - objectName: 'opportunity', - condition: "record.stage == 'closed_won' && previous.stage != 'closed_won'", - }, - }, - { id: 'set_closed_date', type: 'update_record', label: 'Set Closed Date' }, - { id: 'notify_sales', type: 'notify', label: 'Notify Sales' }, - { id: 'end', type: 'end' }, - ], - edges: [ - { id: 'e1', source: 'start', target: 'set_closed_date' }, - { id: 'e2', source: 'set_closed_date', target: 'notify_sales' }, - { id: 'e3', source: 'notify_sales', target: 'end' }, - ], -}); -``` - -状态机保证这笔交易是合法地*到达* `closed_won` 的;流程处理接下来发生的事。上面这样的条件是 CEL 表达式 —— 见 [CEL](/docs/reference/cel)。 - -## 从 Workflow Rules 迁移 - -如果你来自有 Workflow Rules 的平台,对照表如下: - -| 旧概念 | 当前对应 | -|---|---| -| Workflow Rule | 流程 | -| 时间触发器("某日期前/后 N 天") | 时间相对触发器 —— 流程开始节点上的 `config.timeRelative`(16.0,见[流程](/docs/build/automation/flows)) | -| 时间触发器(固定时钟) | 定时流程 | -| 字段更新动作 | `update_record` 节点 | -| 邮件提醒 | `notify` 节点 | -| HTTP 调用 | `http` 节点 | -| Approval Process | 带一个或多个 `approval` 节点的流程 | - -判断标准:如果旧规则是"X 发生时,做 Y",就建成一个小流程。如果它是"某日期字段前/后 N 天,做 Y",就声明时间相对触发器 —— 千万别在记录变更流程上写日期相等条件,那种条件只有在记录恰好被修改时才会求值。如果它是"这条记录必须在受控状态间移动",就把生命周期建成状态机,副作用交给流程。 - -## 下一步 - -| 页面 | 原因 | -|---|---| -| [流程](/docs/build/automation/flows) | 迁移周边的副作用 —— 触发器、步骤、错误处理 | -| [审批流程](/docs/build/automation/approvals) | 作为流程内暂停的人工签核 | -| [CEL 表达式](/docs/reference/cel) | 守卫条件与流程条件背后的语言 | -| [数据建模](/docs/build/data) | 你正在约束其生命周期的对象 | -| [自动化总览](/docs/build/automation) | 选择器:流程 vs 工作流 vs 审批 | diff --git a/content/docs/build/automation/workflows.zh-Hant.mdx b/content/docs/build/automation/workflows.zh-Hant.mdx deleted file mode 100644 index ce2bddc..0000000 --- a/content/docs/build/automation/workflows.zh-Hant.mdx +++ /dev/null @@ -1,143 +0,0 @@ ---- -# @generated zh-Hant from zh-Hans -- do not edit. Edit the English source, then regenerate: pnpm --filter @objectos/docs gen:zh-hant -title: 工作流 -description: 把記錄的生命週期建模成狀態機 —— 合法狀態、帶守衛條件的遷移,其餘的交給流程。 -translation: - source_sha: e87f0ff97a1c1f9972cedc7b7eab08e5ecb22406add0a1087b31945e00965c53 - guide_rev: 1 - mode: auto ---- - -**工作流把記錄的生命週期建模為有限狀態機:記錄可以處於的狀態、在狀態間移動它的事件,以及移動成立所必須滿足的守衛條件。**當核心需求是"這個物件只能按這些事件在這些狀態間移動"時,就用它。 - -不存在獨立的 Salesforce 式 Workflow Rule 編寫型別。舊的"工作流"概念被幹淨地一分為二: - -- **狀態機後設資料**負責嚴格的生命週期遷移 —— 即本頁。 -- **[流程](/docs/build/automation/flows)**負責事件觸發或定時的自動化,包括[審批](/docs/build/automation/approvals)暫停。 - -## 工作流 vs 流程 - -| | 工作流(狀態機) | 流程 | -|---|---|---| -| 建模 | *狀態* —— 記錄處於生命週期的哪裡 | *步驟* —— 某事發生時做什麼 | -| 回答 | "此刻允許這個遷移嗎?" | "我們要對它做什麼?" | -| 形態 | 狀態、遷移、守衛條件 | 節點與邊:觸發器、動作、分支 | -| 副作用 | 無 —— 它只做約束 | 全都有 —— 郵件、更新、HTTP、等待 | - -兩者組合使用:狀態機約束遷移;當你需要通知、更新記錄或呼叫外部系統時,由流程執行遷移周邊的副作用。 - -## 定義狀態機 - -一個必須按 `new → assigned → resolved` 移動、且帶升級路徑的支援工單: - -```ts -import type { StateMachineConfig } from '@objectstack/spec/automation'; - -export const caseLifecycle: StateMachineConfig = { - id: 'case_lifecycle', - initial: 'new', - states: { - new: { - on: { - ASSIGN: { target: 'assigned' }, - }, - }, - assigned: { - on: { - RESOLVE: { target: 'resolved', cond: 'has_resolution' }, - ESCALATE: { target: 'escalated' }, - }, - }, - escalated: { - on: { - RESOLVE: { target: 'resolved', cond: 'has_resolution' }, - }, - }, - resolved: { - type: 'final', - }, - }, -}; -``` - -把它當契約來讀:`new` 狀態的工單隻能被分派。`assigned` 狀態的工單可以被解決 —— 但只有 `has_resolution` 守衛條件通過時 —— 或者被升級。`resolved` 是終態;再沒有什麼能移動它。 - -## 解剖 - -| 鍵 | 宣告什麼 | -|---|---| -| `id` | 狀態機的識別符號 | -| `initial` | 每條新記錄的初始狀態 | -| `states` | 狀態名 → 其出向遷移的對映 | -| `on` | 該狀態響應的事件(`ASSIGN`、`RESOLVE`……) | -| `target` | 事件把記錄移動到的狀態 | -| `cond` | 遷移觸發前必須滿足的守衛條件 | -| `type: 'final'` | 終態 —— 沒有出向遷移 | - -### 守衛條件 - -守衛條件(`cond`)讓遷移帶上前提:上例中,只有當 `has_resolution` 成立時 `RESOLVE` 才能到達 `resolved`。守衛條件把"沒有解決方案就不能關單"編碼成結構性規則,而不是散落在 UI 程式碼裡的驗證。 - -### 事件,而非欄位寫入 - -遷移由具名**事件**(`ASSIGN`、`ESCALATE`)觸發,而不是對狀態欄位的任意編輯。這正是重點:狀態機定義了合法移動的完整集合,未宣告的一律不可能發生。 - -> **提示:**保持狀態機最小 —— 狀態、遷移、守衛條件。一旦你想"然後再發封郵件",你就已經離開了工作流的領地:把副作用放進一個響應該遷移的[流程](/docs/build/automation/flows)。 - -## 副作用屬於流程 - -狀態機只描述合法遷移與守衛條件 —— 別無其他。當某次遷移應該*做*點什麼(通知銷售、寫入關閉日期、呼叫外部系統)時,給狀態機配上一個由記錄變更觸發的流程: - -```ts -export const dealClosedWon = defineFlow({ - name: 'deal_closed_won', - type: 'record_change', - nodes: [ - { - id: 'start', - type: 'start', - config: { - triggerType: 'record-after-update', - objectName: 'opportunity', - condition: "record.stage == 'closed_won' && previous.stage != 'closed_won'", - }, - }, - { id: 'set_closed_date', type: 'update_record', label: 'Set Closed Date' }, - { id: 'notify_sales', type: 'notify', label: 'Notify Sales' }, - { id: 'end', type: 'end' }, - ], - edges: [ - { id: 'e1', source: 'start', target: 'set_closed_date' }, - { id: 'e2', source: 'set_closed_date', target: 'notify_sales' }, - { id: 'e3', source: 'notify_sales', target: 'end' }, - ], -}); -``` - -狀態機保證這筆交易是合法地*到達* `closed_won` 的;流程處理接下來發生的事。上面這樣的條件是 CEL 表示式 —— 見 [CEL](/docs/reference/cel)。 - -## 從 Workflow Rules 遷移 - -如果你來自有 Workflow Rules 的平臺,對照表如下: - -| 舊概念 | 當前對應 | -|---|---| -| Workflow Rule | 流程 | -| 時間觸發器("某日期前/後 N 天") | 時間相對觸發器 —— 流程開始節點上的 `config.timeRelative`(16.0,見[流程](/docs/build/automation/flows)) | -| 時間觸發器(固定時鐘) | 定時流程 | -| 欄位更新動作 | `update_record` 節點 | -| 郵件提醒 | `notify` 節點 | -| HTTP 呼叫 | `http` 節點 | -| Approval Process | 帶一個或多個 `approval` 節點的流程 | - -判斷標準:如果舊規則是"X 發生時,做 Y",就建成一個小流程。如果它是"某日期欄位前/後 N 天,做 Y",就宣告時間相對觸發器 —— 千萬別在記錄變更流程上寫日期相等條件,那種條件只有在記錄恰好被修改時才會求值。如果它是"這條記錄必須在受控狀態間移動",就把生命週期建成狀態機,副作用交給流程。 - -## 下一步 - -| 頁面 | 原因 | -|---|---| -| [流程](/docs/build/automation/flows) | 遷移周邊的副作用 —— 觸發器、步驟、錯誤處理 | -| [審批流程](/docs/build/automation/approvals) | 作為流程內暫停的人工籤核 | -| [CEL 表示式](/docs/reference/cel) | 守衛條件與流程條件背後的語言 | -| [資料建模](/docs/build/data) | 你正在約束其生命週期的物件 | -| [自動化總覽](/docs/build/automation) | 選擇器:流程 vs 工作流 vs 審批 | diff --git a/content/docs/build/data/formulas.mdx b/content/docs/build/data/formulas.mdx index dc32425..5020a72 100644 --- a/content/docs/build/data/formulas.mdx +++ b/content/docs/build/data/formulas.mdx @@ -35,8 +35,8 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; export const Invoice = ObjectSchema.create({ name: 'invoice', fields: { - subtotal: Field.decimal({ label: 'Subtotal' }), - tax_rate: Field.decimal({ label: 'Tax Rate' }), + subtotal: Field.currency({ label: 'Subtotal' }), + tax_rate: Field.number({ label: 'Tax Rate', scale: 4 }), total: Field.formula({ label: 'Total', @@ -168,7 +168,10 @@ po_number: Field.text({ }), rating: Field.select({ label: 'Rating', - options: [ /* ... */ ], + options: [ + { label: 'Hot', value: 'hot' }, + { label: 'Cold', value: 'cold' }, + ], visibleWhen: P`record.status == 'qualified'`, }), notes: Field.textarea({ diff --git a/content/docs/build/data/index.de.mdx b/content/docs/build/data/index.de.mdx deleted file mode 100644 index 9793252..0000000 --- a/content/docs/build/data/index.de.mdx +++ /dev/null @@ -1,264 +0,0 @@ ---- -title: Datenmodell -description: Objekte, Felder, Beziehungen, Validierung, Indizes — der KI beschrieben oder in TypeScript geschrieben. -translation: - source_sha: 212c8e723195aa898b7942afad5cc47073f4661161aecd0da62b89223a34e462 - guide_rev: 1 - mode: auto ---- - -Das Datenmodell ist die zentrale Quelle der Wahrheit für Ihre App. Sobald -ein Objekt existiert, stellt Ihnen ObjectOS REST-APIs, eine Console-Ansicht, -RBAC-Checkpoints, Audit-Log-Einträge und KI-Tool-Bereitstellung zur -Verfügung — kostenlos. - -**Die meisten Kunden schreiben das Schema nie von Hand.** Sie beschreiben, -was sie benötigen, im [AI Builder](/docs/build/ai-builder), und die -Plattform erstellt die Objekte, Felder, Indizes und Übersetzungen. Diese -Seite beschreibt die zugrunde liegende Struktur — damit Sie verstehen, was -die KI generiert, und sie bei Bedarf direkt bearbeiten können. - -## Autorenpfade - -| Pfad | Sieht aus wie | -|---|---| -| **AI Builder** (primär) | *„Erstelle ein `support_ticket`-Objekt mit subject, description, priority, status, assignee."* | -| **Console-Klick-Erstellung** | Console → Objects → New Object → Formulare | -| **TypeScript (`*.object.ts`)** | Das unten gezeigte TS — typischerweise innerhalb einer geforkten [Vorlage](/docs/build/templates) | - -Alle drei erzeugen dasselbe Schema. Das Schema ist kanonisch; alles -andere wird daraus abgeleitet. - -## Aufbau eines Objekts - -```ts -// src/objects/task.ts -import { ObjectSchema, Field } from '@objectstack/spec/data'; - -export const Task = ObjectSchema.create({ - name: 'todo_task', - label: 'Task', - pluralLabel: 'Tasks', - icon: 'check-square', - description: 'A single unit of work.', - - fields: { - subject: Field.text({ label: 'Subject', required: true, maxLength: 200 }), - description: Field.markdown({ label: 'Description' }), - status: Field.select({ - label: 'Status', - options: [ - { label: 'To Do', value: 'todo', default: true }, - { label: 'In Progress', value: 'in_progress' }, - { label: 'Done', value: 'done' }, - ], - }), - due: Field.date({ label: 'Due' }), - assignee: Field.lookup('sys_user', { label: 'Assignee' }), - }, - - enable: { - trackHistory: true, // record field changes in audit log - apiEnabled: true, // expose REST endpoints (default true) - feeds: true, // chatter / comments / @mentions - }, -}); -``` - -Registrieren Sie es in Ihrem Stack: - -```ts -// objectstack.config.ts -import { defineStack } from '@objectstack/spec'; -import * as objects from './src/objects'; - -export default defineStack({ - manifest: { id: 'my.app', namespace: 'myapp', version: '0.1.0', type: 'app', name: 'My App' }, - objects: Object.values(objects), -}); -``` - -Das ist alles, was Sie brauchen. `os dev` kompiliert neu, und -`/api/v1/data/todo_task`, die Console-Task-Ansicht und die -Console-Berechtigungszeile erscheinen allesamt. - -## Feldtypen - -ObjectStack liefert ~25 Feldtypen. Die am häufigsten verwendeten: - -### Skalare - -| Typ | Was es speichert | Helfer | -|---|---|---| -| `text` | Kurze Zeichenkette | `Field.text({ maxLength, required })` | -| `textarea` | Lange Zeichenkette | `Field.textarea(...)` | -| `markdown` | Rich-Text mit Markdown | `Field.markdown(...)` | -| `number` | Ganzzahl | `Field.number({ min, max })` | -| `decimal` | Exakte Dezimalzahl (Geld usw.) | `Field.decimal({ precision, scale })` | -| `boolean` | Wahr/Falsch | `Field.boolean({ defaultValue })` | -| `date` | Kalenderdatum | `Field.date(...)` | -| `datetime` | Zeitstempel | `Field.datetime(...)` | -| `email` | Validierte E-Mail | `Field.email(...)` | -| `url` | Validierte URL | `Field.url(...)` | -| `phone` | Validierte Telefonnummer | `Field.phone(...)` | -| `json` | Beliebiges JSON | `Field.json(...)` | - -### Auswahlmöglichkeiten - -| Typ | Verwendung für | -|---|---| -| `select` | Einzelauswahl (Enum) | -| `multiselect` | Mehrfachauswahl | - -### Beziehungen - -| Typ | Kardinalität | Helfer | -|---|---|---| -| `lookup` | Eins-zu-viele (FK) | `Field.lookup({ reference: 'sys_user' })` | -| `masterDetail` | Eins-zu-viele mit kaskadierendem Löschen | `Field.masterDetail({ reference: 'order' })` | - -### Dateien & Medien - -| Typ | Was es speichert | -|---|---| -| `file` | Eine Datei über den Speicherdienst | -| `image` | Bilddatei mit Vorschau | - -### Berechnet / abgeleitet - -| Typ | Verhalten | -|---|---| -| `formula` | Beim Lesen aus einem CEL-Ausdruck berechnet | -| `summary` | Aggregat verknüpfter Datensätze (Summe/Anzahl/Durchschnitt) | -| `autonumber` | Sequenz (`INV-{000001}`) | -| `created`, `lastModified` | Systemgepflegte Zeitstempel | -| `createdBy`, `lastModifiedBy` | Systemgepflegte Benutzerreferenzen | - -## Erforderlich / eindeutig / Standard - -Gängige Modifikatoren für jedes skalare Feld: - -```ts -Field.text({ - label: 'Code', - required: true, - unique: true, // unique constraint enforced at DB level - defaultValue: '', - helpText: 'Internal short code', -}) -``` - -## Validierung - -Inline: - -```ts -Field.number({ label: 'Quantity', min: 1, max: 9999 }) -Field.text({ label: 'SKU', pattern: '^[A-Z]{3}-[0-9]{4}$' }) -``` - -Objektebene-Regeln (feldübergreifend): - -```ts -ObjectSchema.create({ - name: 'order', - fields: { /* ... */ }, - validations: [ - { - name: 'discount_lt_total', - message: 'Discount cannot exceed total', - condition: 'discount < total', - }, - ], -}); -``` - -Die Validierung läuft bei jedem Schreibvorgang — REST, Console, ObjectQL — -sodass es keine „Hintertür" gibt. - -## Indizes & Performance - -```ts -ObjectSchema.create({ - name: 'order', - fields: { /* ... */ }, - indexes: [ - { fields: ['status', 'created_at'] }, - { fields: ['account', 'created_at'], unique: false }, - ], -}); -``` - -Der Treiber erstellt beim Schema-Sync echte DB-Indizes. - -## Feldgruppen - -Für lange Formulare gruppieren Sie Felder in der Console: - -```ts -ObjectSchema.create({ - name: 'task', - fieldGroups: [ - { key: 'core', label: 'Task', icon: 'check-square' }, - { key: 'planning', label: 'Planning', icon: 'calendar' }, - { key: 'meta', label: 'Metadata', icon: 'info', defaultExpanded: false }, - ], - fields: { - subject: Field.text({ label: 'Subject', group: 'core' }), - due: Field.date({ label: 'Due', group: 'planning' }), - }, -}); -``` - -## Lebenszyklus & Eigentümerschaft - -```ts -ObjectSchema.create({ - name: 'task', - ownership: 'own', // 'own' | 'shared' | 'system' - enable: { - apiEnabled: true, // generated REST endpoints - trackHistory: true, // audit log of field changes - feeds: true, // sys_comment / sys_activity / @mentions - trash: true, // soft-delete with restore (default true) - }, -}); -``` - -## Systemobjekte (kostenlos bei jedem Projekt) - -Sie müssen diese nicht deklarieren — sie sind immer vorhanden: - -| Objekt | Was | -|---|---| -| `sys_user` | Benutzerkonten | -| `sys_org` | Organisationen / Mandanten | -| `sys_member` | Org-Mitgliedschaft | -| `sys_role`, `sys_permission_set` | RBAC-Primitive | -| `sys_audit_log` | Audit-Trail (wenn Audit-Fähigkeit geladen) | -| `sys_file`, `sys_attachment` | Datei-Metadaten (wenn Speicher geladen) | -| `sys_comment`, `sys_activity` | Feed / Chatter (wenn Feed geladen) | -| `sys_session`, `sys_api_key` | Auth-Artefakte | -| `sys_webhook`, `sys_webhook_delivery` | Webhook-Abonnements (wenn aktiviert) | - -Referenzieren Sie sie in `lookup`-Feldern über den Namen — z. B. `Field.lookup({ reference: 'sys_user' })`. - -## Polymorphe Plattformfunktionen - -Wenn Sie `feeds: true` und `trackHistory: true` aktivieren, nimmt Ihr Objekt -automatisch teil an: - -- `sys_comment` (thread_id = `:`) -- `sys_attachment` (parent_object = ``, parent_id = ``) -- `sys_activity` (Timeline) -- `sys_audit_log` (Diffs auf Feldebene) - -Sie verdrahten diese nicht pro Objekt — sie sind auf der Plattform polymorph. - -## Wie es weitergeht - -- [Berechtigungen](/docs/configure/permissions) — Zugriff auf Ihre Objekte steuern -- [Flows / Automatisierung](/docs/build/automation/flows) — auf Datensatzänderungen reagieren -- [API-Zugriff](/docs/configure/api-access) — Ihre generierte REST aufrufen -- [`os explain`](/docs/reference/cli) — das gerenderte Schema ausgeben -- [`@objectstack/spec`-Quelle](https://github.com/objectstack-ai/objectstack/tree/main/packages/spec) — das Schema ist der Vertrag; alles hier wird daraus abgeleitet diff --git a/content/docs/build/data/index.es.mdx b/content/docs/build/data/index.es.mdx deleted file mode 100644 index 8510d78..0000000 --- a/content/docs/build/data/index.es.mdx +++ /dev/null @@ -1,263 +0,0 @@ ---- -title: Modelo de datos -description: Objetos, campos, relaciones, validación, índices — descritos a la IA o escritos en TypeScript. -translation: - source_sha: 212c8e723195aa898b7942afad5cc47073f4661161aecd0da62b89223a34e462 - guide_rev: 1 - mode: auto ---- - -El modelo de datos es la única fuente de verdad de tu aplicación. Una vez -que existe un objeto, ObjectOS te ofrece APIs REST, una vista en Console, puntos -de control de RBAC, entradas en el registro de auditoría y exposición de -herramientas de IA — de forma gratuita. - -**La mayoría de los clientes nunca escriben el esquema a mano.** Describen lo -que necesitan en el [AI Builder](/docs/build/ai-builder) y la -plataforma crea los objetos, campos, índices y traducciones. Esta -página describe la forma subyacente — para que entiendas lo que la IA -está generando y puedas editarlo directamente cuando quieras. - -## Vías de creación - -| Vía | Cómo se ve | -|---|---| -| **AI Builder** (principal) | *"Crea un objeto `support_ticket` con asunto, descripción, prioridad, estado y responsable."* | -| **Construcción con clics en Console** | Console → Objects → New Object → formularios | -| **TypeScript (`*.object.ts`)** | El TS que se muestra a continuación — normalmente dentro de una [plantilla](/docs/build/templates) bifurcada | - -Las tres producen el mismo esquema. El esquema es canónico; todo lo -demás se deriva de él. - -## Anatomía de un objeto - -```ts -// src/objects/task.ts -import { ObjectSchema, Field } from '@objectstack/spec/data'; - -export const Task = ObjectSchema.create({ - name: 'todo_task', - label: 'Task', - pluralLabel: 'Tasks', - icon: 'check-square', - description: 'A single unit of work.', - - fields: { - subject: Field.text({ label: 'Subject', required: true, maxLength: 200 }), - description: Field.markdown({ label: 'Description' }), - status: Field.select({ - label: 'Status', - options: [ - { label: 'To Do', value: 'todo', default: true }, - { label: 'In Progress', value: 'in_progress' }, - { label: 'Done', value: 'done' }, - ], - }), - due: Field.date({ label: 'Due' }), - assignee: Field.lookup('sys_user', { label: 'Assignee' }), - }, - - enable: { - trackHistory: true, // record field changes in audit log - apiEnabled: true, // expose REST endpoints (default true) - feeds: true, // chatter / comments / @mentions - }, -}); -``` - -Regístralo en tu stack: - -```ts -// objectstack.config.ts -import { defineStack } from '@objectstack/spec'; -import * as objects from './src/objects'; - -export default defineStack({ - manifest: { id: 'my.app', namespace: 'myapp', version: '0.1.0', type: 'app', name: 'My App' }, - objects: Object.values(objects), -}); -``` - -Eso es todo lo que necesitas. `os dev` recompila, y `/api/v1/data/todo_task`, -la vista Task de Console y la fila de permisos de Console aparecen todas. - -## Tipos de campo - -ObjectStack incluye unos 25 tipos de campo. Los más usados: - -### Escalares - -| Tipo | Qué almacena | Helper | -|---|---|---| -| `text` | Cadena corta | `Field.text({ maxLength, required })` | -| `textarea` | Cadena larga | `Field.textarea(...)` | -| `markdown` | Texto enriquecido con markdown | `Field.markdown(...)` | -| `number` | Entero | `Field.number({ min, max })` | -| `decimal` | Decimal exacto (dinero, etc.) | `Field.decimal({ precision, scale })` | -| `boolean` | Verdadero/falso | `Field.boolean({ defaultValue })` | -| `date` | Fecha de calendario | `Field.date(...)` | -| `datetime` | Marca de tiempo | `Field.datetime(...)` | -| `email` | Correo electrónico validado | `Field.email(...)` | -| `url` | URL validada | `Field.url(...)` | -| `phone` | Teléfono validado | `Field.phone(...)` | -| `json` | JSON arbitrario | `Field.json(...)` | - -### Opciones - -| Tipo | Se usa para | -|---|---| -| `select` | Opción única (enum) | -| `multiselect` | Varias opciones | - -### Relaciones - -| Tipo | Cardinalidad | Helper | -|---|---|---| -| `lookup` | Uno a muchos (FK) | `Field.lookup({ reference: 'sys_user' })` | -| `masterDetail` | Uno a muchos con eliminación en cascada | `Field.masterDetail({ reference: 'order' })` | - -### Archivos y multimedia - -| Tipo | Qué almacena | -|---|---| -| `file` | Un archivo mediante el servicio de almacenamiento | -| `image` | Archivo de imagen con vista previa | - -### Calculados / derivados - -| Tipo | Comportamiento | -|---|---| -| `formula` | Calculado en el momento de la lectura a partir de una expresión CEL | -| `summary` | Agregado de registros relacionados (suma/recuento/promedio) | -| `autonumber` | Secuencia (`INV-{000001}`) | -| `created`, `lastModified` | Marcas de tiempo mantenidas por el sistema | -| `createdBy`, `lastModifiedBy` | Referencias de usuario mantenidas por el sistema | - -## Required / unique / default - -Modificadores comunes en cada campo escalar: - -```ts -Field.text({ - label: 'Code', - required: true, - unique: true, // unique constraint enforced at DB level - defaultValue: '', - helpText: 'Internal short code', -}) -``` - -## Validación - -En línea: - -```ts -Field.number({ label: 'Quantity', min: 1, max: 9999 }) -Field.text({ label: 'SKU', pattern: '^[A-Z]{3}-[0-9]{4}$' }) -``` - -Reglas a nivel de objeto (entre campos): - -```ts -ObjectSchema.create({ - name: 'order', - fields: { /* ... */ }, - validations: [ - { - name: 'discount_lt_total', - message: 'Discount cannot exceed total', - condition: 'discount < total', - }, - ], -}); -``` - -La validación se ejecuta en cada escritura — REST, Console, ObjectQL — por lo que -no hay "puerta trasera". - -## Índices y rendimiento - -```ts -ObjectSchema.create({ - name: 'order', - fields: { /* ... */ }, - indexes: [ - { fields: ['status', 'created_at'] }, - { fields: ['account', 'created_at'], unique: false }, - ], -}); -``` - -El controlador crea índices reales en la base de datos al sincronizar el esquema. - -## Grupos de campos - -Para formularios largos, agrupa los campos en Console: - -```ts -ObjectSchema.create({ - name: 'task', - fieldGroups: [ - { key: 'core', label: 'Task', icon: 'check-square' }, - { key: 'planning', label: 'Planning', icon: 'calendar' }, - { key: 'meta', label: 'Metadata', icon: 'info', defaultExpanded: false }, - ], - fields: { - subject: Field.text({ label: 'Subject', group: 'core' }), - due: Field.date({ label: 'Due', group: 'planning' }), - }, -}); -``` - -## Ciclo de vida y propiedad - -```ts -ObjectSchema.create({ - name: 'task', - ownership: 'own', // 'own' | 'shared' | 'system' - enable: { - apiEnabled: true, // generated REST endpoints - trackHistory: true, // audit log of field changes - feeds: true, // sys_comment / sys_activity / @mentions - trash: true, // soft-delete with restore (default true) - }, -}); -``` - -## Objetos del sistema (gratuitos con cada proyecto) - -No tienes que declararlos — siempre están ahí: - -| Objeto | Qué | -|---|---| -| `sys_user` | Cuentas de usuario | -| `sys_org` | Organizaciones / tenants | -| `sys_member` | Pertenencia a una organización | -| `sys_role`, `sys_permission_set` | Primitivas de RBAC | -| `sys_audit_log` | Rastro de auditoría (cuando se carga la capacidad de auditoría) | -| `sys_file`, `sys_attachment` | Metadatos de archivos (cuando se carga el almacenamiento) | -| `sys_comment`, `sys_activity` | Feed / chatter (cuando se carga el feed) | -| `sys_session`, `sys_api_key` | Artefactos de autenticación | -| `sys_webhook`, `sys_webhook_delivery` | Suscripciones de webhook (cuando están habilitadas) | - -Haz referencia a ellos en los campos `lookup` por su nombre — p. ej. `Field.lookup({ reference: 'sys_user' })`. - -## Funciones polimórficas de la plataforma - -Cuando habilitas `feeds: true` y `trackHistory: true`, tu objeto -participa automáticamente en: - -- `sys_comment` (thread_id = `:`) -- `sys_attachment` (parent_object = ``, parent_id = ``) -- `sys_activity` (cronología) -- `sys_audit_log` (diferencias a nivel de campo) - -No conectas esto por cada objeto — es polimórfico en la plataforma. - -## Adónde ir a continuación - -- [Permisos](/docs/configure/permissions) — controla el acceso a tus objetos -- [Flujos / Automatización](/docs/build/automation/flows) — reacciona a los cambios en los registros -- [Acceso a la API](/docs/configure/api-access) — llama a tu REST generado -- [`os explain`](/docs/reference/cli) — imprime el esquema renderizado -- [Código fuente de `@objectstack/spec`](https://github.com/objectstack-ai/objectstack/tree/main/packages/spec) — el esquema es el contrato; todo lo aquí descrito se deriva de él diff --git a/content/docs/build/data/index.fr.mdx b/content/docs/build/data/index.fr.mdx deleted file mode 100644 index 4fb38e6..0000000 --- a/content/docs/build/data/index.fr.mdx +++ /dev/null @@ -1,265 +0,0 @@ ---- -title: Modèle de données -description: Objets, champs, relations, validation, index — décrits à l'IA ou écrits en TypeScript. -translation: - source_sha: 212c8e723195aa898b7942afad5cc47073f4661161aecd0da62b89223a34e462 - guide_rev: 1 - mode: auto ---- - -Le modèle de données est la source de vérité unique de votre application. Dès -qu'un objet existe, ObjectOS vous fournit des API REST, une vue Console, des -points de contrôle RBAC, des entrées de journal d'audit et une exposition aux -outils d'IA — gratuitement. - -**La plupart des clients n'écrivent jamais le schéma à la main.** Ils décrivent -ce dont ils ont besoin dans l'[AI Builder](/docs/build/ai-builder) et la -plateforme crée les objets, les champs, les index et les traductions. Cette -page décrit la structure sous-jacente — afin que vous compreniez ce que l'IA -génère et que vous puissiez la modifier directement quand vous le souhaitez. - -## Méthodes de création - -| Méthode | À quoi cela ressemble | -|---|---| -| **AI Builder** (principale) | *« Créer un objet `support_ticket` avec sujet, description, priorité, statut, responsable. »* | -| **Construction par clic dans la Console** | Console → Objects → New Object → formulaires | -| **TypeScript (`*.object.ts`)** | Le TS présenté ci-dessous — généralement à l'intérieur d'un [template](/docs/build/templates) forké | - -Les trois produisent le même schéma. Le schéma est canonique ; tout le reste en -est dérivé. - -## Anatomie d'un objet - -```ts -// src/objects/task.ts -import { ObjectSchema, Field } from '@objectstack/spec/data'; - -export const Task = ObjectSchema.create({ - name: 'todo_task', - label: 'Task', - pluralLabel: 'Tasks', - icon: 'check-square', - description: 'A single unit of work.', - - fields: { - subject: Field.text({ label: 'Subject', required: true, maxLength: 200 }), - description: Field.markdown({ label: 'Description' }), - status: Field.select({ - label: 'Status', - options: [ - { label: 'To Do', value: 'todo', default: true }, - { label: 'In Progress', value: 'in_progress' }, - { label: 'Done', value: 'done' }, - ], - }), - due: Field.date({ label: 'Due' }), - assignee: Field.lookup('sys_user', { label: 'Assignee' }), - }, - - enable: { - trackHistory: true, // record field changes in audit log - apiEnabled: true, // expose REST endpoints (default true) - feeds: true, // chatter / comments / @mentions - }, -}); -``` - -Enregistrez-le dans votre stack : - -```ts -// objectstack.config.ts -import { defineStack } from '@objectstack/spec'; -import * as objects from './src/objects'; - -export default defineStack({ - manifest: { id: 'my.app', namespace: 'myapp', version: '0.1.0', type: 'app', name: 'My App' }, - objects: Object.values(objects), -}); -``` - -C'est tout ce dont vous avez besoin. `os dev` recompile, et -`/api/v1/data/todo_task`, la vue Task de la Console et la ligne d'autorisation -de la Console apparaissent tous. - -## Types de champs - -ObjectStack fournit environ 25 types de champs. Les plus utilisés : - -### Scalaires - -| Type | Ce qu'il stocke | Helper | -|---|---|---| -| `text` | Chaîne courte | `Field.text({ maxLength, required })` | -| `textarea` | Chaîne longue | `Field.textarea(...)` | -| `markdown` | Texte enrichi avec markdown | `Field.markdown(...)` | -| `number` | Entier | `Field.number({ min, max })` | -| `decimal` | Décimal exact (monnaie, etc.) | `Field.decimal({ precision, scale })` | -| `boolean` | Vrai/faux | `Field.boolean({ defaultValue })` | -| `date` | Date du calendrier | `Field.date(...)` | -| `datetime` | Horodatage | `Field.datetime(...)` | -| `email` | E-mail validé | `Field.email(...)` | -| `url` | URL validée | `Field.url(...)` | -| `phone` | Téléphone validé | `Field.phone(...)` | -| `json` | JSON arbitraire | `Field.json(...)` | - -### Choix - -| Type | À utiliser pour | -|---|---| -| `select` | Choix unique (enum) | -| `multiselect` | Choix multiples | - -### Relations - -| Type | Cardinalité | Helper | -|---|---|---| -| `lookup` | Un-à-plusieurs (FK) | `Field.lookup({ reference: 'sys_user' })` | -| `masterDetail` | Un-à-plusieurs avec suppression en cascade | `Field.masterDetail({ reference: 'order' })` | - -### Fichiers et médias - -| Type | Ce qu'il stocke | -|---|---| -| `file` | Un fichier via le service de stockage | -| `image` | Fichier image avec aperçu | - -### Calculé / dérivé - -| Type | Comportement | -|---|---| -| `formula` | Calculé à la lecture à partir d'une expression CEL | -| `summary` | Agrégat d'enregistrements liés (somme/nombre/moyenne) | -| `autonumber` | Séquence (`INV-{000001}`) | -| `created`, `lastModified` | Horodatages gérés par le système | -| `createdBy`, `lastModifiedBy` | Références utilisateur gérées par le système | - -## Requis / unique / par défaut - -Modificateurs courants sur chaque champ scalaire : - -```ts -Field.text({ - label: 'Code', - required: true, - unique: true, // unique constraint enforced at DB level - defaultValue: '', - helpText: 'Internal short code', -}) -``` - -## Validation - -En ligne : - -```ts -Field.number({ label: 'Quantity', min: 1, max: 9999 }) -Field.text({ label: 'SKU', pattern: '^[A-Z]{3}-[0-9]{4}$' }) -``` - -Règles au niveau de l'objet (entre champs) : - -```ts -ObjectSchema.create({ - name: 'order', - fields: { /* ... */ }, - validations: [ - { - name: 'discount_lt_total', - message: 'Discount cannot exceed total', - condition: 'discount < total', - }, - ], -}); -``` - -La validation s'exécute à chaque écriture — REST, Console, ObjectQL — il n'y a -donc aucune « porte dérobée ». - -## Index et performance - -```ts -ObjectSchema.create({ - name: 'order', - fields: { /* ... */ }, - indexes: [ - { fields: ['status', 'created_at'] }, - { fields: ['account', 'created_at'], unique: false }, - ], -}); -``` - -Le driver crée de vrais index de base de données lors de la synchronisation du -schéma. - -## Groupes de champs - -Pour les formulaires longs, regroupez les champs dans la Console : - -```ts -ObjectSchema.create({ - name: 'task', - fieldGroups: [ - { key: 'core', label: 'Task', icon: 'check-square' }, - { key: 'planning', label: 'Planning', icon: 'calendar' }, - { key: 'meta', label: 'Metadata', icon: 'info', defaultExpanded: false }, - ], - fields: { - subject: Field.text({ label: 'Subject', group: 'core' }), - due: Field.date({ label: 'Due', group: 'planning' }), - }, -}); -``` - -## Cycle de vie et propriété - -```ts -ObjectSchema.create({ - name: 'task', - ownership: 'own', // 'own' | 'shared' | 'system' - enable: { - apiEnabled: true, // generated REST endpoints - trackHistory: true, // audit log of field changes - feeds: true, // sys_comment / sys_activity / @mentions - trash: true, // soft-delete with restore (default true) - }, -}); -``` - -## Objets système (gratuits avec chaque projet) - -Vous n'avez pas à les déclarer — ils sont toujours présents : - -| Objet | Quoi | -|---|---| -| `sys_user` | Comptes utilisateurs | -| `sys_org` | Organisations / locataires | -| `sys_member` | Appartenance à une organisation | -| `sys_role`, `sys_permission_set` | Primitives RBAC | -| `sys_audit_log` | Piste d'audit (lorsque la capacité d'audit est chargée) | -| `sys_file`, `sys_attachment` | Métadonnées de fichier (lorsque le stockage est chargé) | -| `sys_comment`, `sys_activity` | Fil / chatter (lorsque le fil est chargé) | -| `sys_session`, `sys_api_key` | Artefacts d'authentification | -| `sys_webhook`, `sys_webhook_delivery` | Abonnements webhook (lorsqu'ils sont activés) | - -Référencez-les dans les champs `lookup` par leur nom — par exemple `Field.lookup({ reference: 'sys_user' })`. - -## Fonctionnalités polymorphes de la plateforme - -Lorsque vous activez `feeds: true` et `trackHistory: true`, votre objet -participe automatiquement à : - -- `sys_comment` (thread_id = `:`) -- `sys_attachment` (parent_object = ``, parent_id = ``) -- `sys_activity` (chronologie) -- `sys_audit_log` (différences au niveau des champs) - -Vous ne câblez pas cela par objet — c'est polymorphe sur la plateforme. - -## Pour aller plus loin - -- [Permissions](/docs/configure/permissions) — contrôlez l'accès à vos objets -- [Flows / Automation](/docs/build/automation/flows) — réagissez aux modifications d'enregistrements -- [API Access](/docs/configure/api-access) — appelez vos API REST générées -- [`os explain`](/docs/reference/cli) — affichez le schéma rendu -- [Source de `@objectstack/spec`](https://github.com/objectstack-ai/objectstack/tree/main/packages/spec) — le schéma est le contrat ; tout ce qui est ici en est dérivé diff --git a/content/docs/build/data/index.ja.mdx b/content/docs/build/data/index.ja.mdx deleted file mode 100644 index cd6a239..0000000 --- a/content/docs/build/data/index.ja.mdx +++ /dev/null @@ -1,265 +0,0 @@ ---- -title: データモデル -description: オブジェクト、フィールド、リレーションシップ、バリデーション、インデックス — AI に説明するか、TypeScript で記述します。 -translation: - source_sha: 212c8e723195aa898b7942afad5cc47073f4661161aecd0da62b89223a34e462 - guide_rev: 1 - mode: auto ---- - -データモデルは、アプリにとって唯一の信頼できる情報源です。オブジェクトが -存在するようになると、ObjectOS は REST API、Console ビュー、RBAC の -チェックポイント、監査ログのエントリ、そして AI ツールへの公開を — 無償で -提供します。 - -**ほとんどのお客様はスキーマを手書きすることはありません。**必要なものを -[AI Builder](/docs/build/ai-builder) で説明すると、プラットフォームが -オブジェクト、フィールド、インデックス、翻訳を作成します。このページでは -その基盤となる構造を説明します — AI が生成しているものを理解し、必要に -応じて直接編集できるようにするためです。 - -## オーサリングのパス - -| パス | 具体例 | -|---|---| -| **AI Builder**(推奨) | *「subject、description、priority、status、assignee を持つ `support_ticket` オブジェクトを作成して。」* | -| **Console のクリックビルド** | Console → Objects → New Object → フォーム | -| **TypeScript(`*.object.ts`)** | 下に示す TS — 通常はフォークした[テンプレート](/docs/build/templates)の中で記述 | - -この 3 つはいずれも同じスキーマを生成します。スキーマが正規のものであり、 -それ以外はすべて派生物です。 - -## オブジェクトの構造 - -```ts -// src/objects/task.ts -import { ObjectSchema, Field } from '@objectstack/spec/data'; - -export const Task = ObjectSchema.create({ - name: 'todo_task', - label: 'Task', - pluralLabel: 'Tasks', - icon: 'check-square', - description: 'A single unit of work.', - - fields: { - subject: Field.text({ label: 'Subject', required: true, maxLength: 200 }), - description: Field.markdown({ label: 'Description' }), - status: Field.select({ - label: 'Status', - options: [ - { label: 'To Do', value: 'todo', default: true }, - { label: 'In Progress', value: 'in_progress' }, - { label: 'Done', value: 'done' }, - ], - }), - due: Field.date({ label: 'Due' }), - assignee: Field.lookup('sys_user', { label: 'Assignee' }), - }, - - enable: { - trackHistory: true, // record field changes in audit log - apiEnabled: true, // expose REST endpoints (default true) - feeds: true, // chatter / comments / @mentions - }, -}); -``` - -スタックに登録します: - -```ts -// objectstack.config.ts -import { defineStack } from '@objectstack/spec'; -import * as objects from './src/objects'; - -export default defineStack({ - manifest: { id: 'my.app', namespace: 'myapp', version: '0.1.0', type: 'app', name: 'My App' }, - objects: Object.values(objects), -}); -``` - -必要なのはこれだけです。`os dev` が再コンパイルを行い、`/api/v1/data/todo_task`、 -Console の Task ビュー、そして Console のパーミッション行がすべて表示されます。 - -## フィールドタイプ - -ObjectStack には約 25 種類のフィールドタイプが付属します。よく使われるものは -次のとおりです: - -### スカラー - -| タイプ | 格納する内容 | ヘルパー | -|---|---|---| -| `text` | 短い文字列 | `Field.text({ maxLength, required })` | -| `textarea` | 長い文字列 | `Field.textarea(...)` | -| `markdown` | markdown 形式のリッチテキスト | `Field.markdown(...)` | -| `number` | 整数 | `Field.number({ min, max })` | -| `decimal` | 正確な小数(金額など) | `Field.decimal({ precision, scale })` | -| `boolean` | 真偽値 | `Field.boolean({ defaultValue })` | -| `date` | カレンダー日付 | `Field.date(...)` | -| `datetime` | タイムスタンプ | `Field.datetime(...)` | -| `email` | バリデーション済みメールアドレス | `Field.email(...)` | -| `url` | バリデーション済み URL | `Field.url(...)` | -| `phone` | バリデーション済み電話番号 | `Field.phone(...)` | -| `json` | 任意の JSON | `Field.json(...)` | - -### 選択肢 - -| タイプ | 用途 | -|---|---| -| `select` | 単一選択(列挙) | -| `multiselect` | 複数選択 | - -### リレーションシップ - -| タイプ | カーディナリティ | ヘルパー | -|---|---|---| -| `lookup` | 一対多(FK) | `Field.lookup({ reference: 'sys_user' })` | -| `masterDetail` | カスケード削除付きの一対多 | `Field.masterDetail({ reference: 'order' })` | - -### ファイルとメディア - -| タイプ | 格納する内容 | -|---|---| -| `file` | ストレージサービス経由の単一ファイル | -| `image` | プレビュー付きの画像ファイル | - -### 計算 / 派生 - -| タイプ | 動作 | -|---|---| -| `formula` | CEL 式から読み取り時に計算 | -| `summary` | 関連レコードの集計(sum/count/avg) | -| `autonumber` | シーケンス(`INV-{000001}`) | -| `created`, `lastModified` | システムが保守するタイムスタンプ | -| `createdBy`, `lastModifiedBy` | システムが保守するユーザー参照 | - -## 必須 / 一意 / デフォルト - -すべてのスカラーフィールドで共通の修飾子: - -```ts -Field.text({ - label: 'Code', - required: true, - unique: true, // unique constraint enforced at DB level - defaultValue: '', - helpText: 'Internal short code', -}) -``` - -## バリデーション - -インライン: - -```ts -Field.number({ label: 'Quantity', min: 1, max: 9999 }) -Field.text({ label: 'SKU', pattern: '^[A-Z]{3}-[0-9]{4}$' }) -``` - -オブジェクトレベルのルール(フィールド間): - -```ts -ObjectSchema.create({ - name: 'order', - fields: { /* ... */ }, - validations: [ - { - name: 'discount_lt_total', - message: 'Discount cannot exceed total', - condition: 'discount < total', - }, - ], -}); -``` - -バリデーションはあらゆる書き込み時に実行されます — REST、Console、ObjectQL のいずれでも — -そのため「裏口」は存在しません。 - -## インデックスとパフォーマンス - -```ts -ObjectSchema.create({ - name: 'order', - fields: { /* ... */ }, - indexes: [ - { fields: ['status', 'created_at'] }, - { fields: ['account', 'created_at'], unique: false }, - ], -}); -``` - -ドライバーはスキーマ同期時に実際の DB インデックスを作成します。 - -## フィールドグループ - -長いフォームでは、Console 内でフィールドをグループ化します: - -```ts -ObjectSchema.create({ - name: 'task', - fieldGroups: [ - { key: 'core', label: 'Task', icon: 'check-square' }, - { key: 'planning', label: 'Planning', icon: 'calendar' }, - { key: 'meta', label: 'Metadata', icon: 'info', defaultExpanded: false }, - ], - fields: { - subject: Field.text({ label: 'Subject', group: 'core' }), - due: Field.date({ label: 'Due', group: 'planning' }), - }, -}); -``` - -## ライフサイクルと所有権 - -```ts -ObjectSchema.create({ - name: 'task', - ownership: 'own', // 'own' | 'shared' | 'system' - enable: { - apiEnabled: true, // generated REST endpoints - trackHistory: true, // audit log of field changes - feeds: true, // sys_comment / sys_activity / @mentions - trash: true, // soft-delete with restore (default true) - }, -}); -``` - -## システムオブジェクト(すべてのプロジェクトで無償) - -これらを宣言する必要はありません — 常に存在します: - -| オブジェクト | 内容 | -|---|---| -| `sys_user` | ユーザーアカウント | -| `sys_org` | 組織 / テナント | -| `sys_member` | 組織のメンバーシップ | -| `sys_role`, `sys_permission_set` | RBAC のプリミティブ | -| `sys_audit_log` | 監査証跡(監査機能がロードされている場合) | -| `sys_file`, `sys_attachment` | ファイルのメタデータ(ストレージがロードされている場合) | -| `sys_comment`, `sys_activity` | フィード / chatter(フィードがロードされている場合) | -| `sys_session`, `sys_api_key` | 認証アーティファクト | -| `sys_webhook`, `sys_webhook_delivery` | Webhook サブスクリプション(有効化されている場合) | - -`lookup` フィールドでは名前で参照します — 例: `Field.lookup({ reference: 'sys_user' })`。 - -## ポリモーフィックなプラットフォーム機能 - -`feeds: true` と `trackHistory: true` を有効にすると、オブジェクトは -自動的に次の機能に参加します: - -- `sys_comment`(thread_id = `:`) -- `sys_attachment`(parent_object = ``、parent_id = ``) -- `sys_activity`(タイムライン) -- `sys_audit_log`(フィールドレベルの差分) - -これらをオブジェクトごとに配線する必要はありません — プラットフォーム上で -ポリモーフィックに動作します。 - -## 次に読むべきもの - -- [パーミッション](/docs/configure/permissions) — オブジェクトへのアクセスを制御 -- [フロー / 自動化](/docs/build/automation/flows) — レコードの変更に反応 -- [API アクセス](/docs/configure/api-access) — 生成された REST を呼び出す -- [`os explain`](/docs/reference/cli) — レンダリングされたスキーマを出力 -- [`@objectstack/spec` ソース](https://github.com/objectstack-ai/objectstack/tree/main/packages/spec) — スキーマが契約であり、ここに記載されているものはすべてそこから派生します diff --git a/content/docs/build/data/index.ko.mdx b/content/docs/build/data/index.ko.mdx deleted file mode 100644 index 727d4f6..0000000 --- a/content/docs/build/data/index.ko.mdx +++ /dev/null @@ -1,262 +0,0 @@ ---- -title: 데이터 모델 -description: 객체, 필드, 관계, 유효성 검사, 인덱스 — AI에게 설명하거나 TypeScript로 작성합니다. -translation: - source_sha: 212c8e723195aa898b7942afad5cc47073f4661161aecd0da62b89223a34e462 - guide_rev: 1 - mode: auto ---- - -데이터 모델은 앱의 단일 진실 공급원(single source of truth)입니다. 객체가 -한번 존재하게 되면, ObjectOS는 REST API, Console 뷰, RBAC -체크포인트, 감사 로그 항목, 그리고 AI 도구 노출을 — 무료로 제공합니다. - -**대부분의 고객은 스키마를 직접 손으로 작성하지 않습니다.** 그들은 필요한 -것을 [AI Builder](/docs/build/ai-builder)에서 설명하고, 플랫폼이 -객체, 필드, 인덱스, 번역을 생성합니다. 이 -페이지에서는 그 기저의 형태를 설명합니다 — 그래서 AI가 무엇을 -생성하는지 이해하고, 원할 때 직접 편집할 수 있습니다. - -## 작성 경로 - -| 경로 | 형태 | -|---|---| -| **AI Builder** (기본) | *"subject, description, priority, status, assignee를 가진 `support_ticket` 객체를 생성해줘."* | -| **Console 클릭 빌드** | Console → Objects → New Object → 폼 | -| **TypeScript (`*.object.ts`)** | 아래에 표시된 TS — 일반적으로 포크된 [템플릿](/docs/build/templates) 내부 | - -세 가지 모두 동일한 스키마를 생성합니다. 스키마가 표준(canonical)이며, 그 밖의 -모든 것은 파생됩니다. - -## 객체의 구조 - -```ts -// src/objects/task.ts -import { ObjectSchema, Field } from '@objectstack/spec/data'; - -export const Task = ObjectSchema.create({ - name: 'todo_task', - label: 'Task', - pluralLabel: 'Tasks', - icon: 'check-square', - description: 'A single unit of work.', - - fields: { - subject: Field.text({ label: 'Subject', required: true, maxLength: 200 }), - description: Field.markdown({ label: 'Description' }), - status: Field.select({ - label: 'Status', - options: [ - { label: 'To Do', value: 'todo', default: true }, - { label: 'In Progress', value: 'in_progress' }, - { label: 'Done', value: 'done' }, - ], - }), - due: Field.date({ label: 'Due' }), - assignee: Field.lookup('sys_user', { label: 'Assignee' }), - }, - - enable: { - trackHistory: true, // record field changes in audit log - apiEnabled: true, // expose REST endpoints (default true) - feeds: true, // chatter / comments / @mentions - }, -}); -``` - -스택에 등록합니다: - -```ts -// objectstack.config.ts -import { defineStack } from '@objectstack/spec'; -import * as objects from './src/objects'; - -export default defineStack({ - manifest: { id: 'my.app', namespace: 'myapp', version: '0.1.0', type: 'app', name: 'My App' }, - objects: Object.values(objects), -}); -``` - -필요한 것은 그게 전부입니다. `os dev`가 재컴파일하면, `/api/v1/data/todo_task`, -Console Task 뷰, 그리고 Console 권한 행이 모두 나타납니다. - -## 필드 타입 - -ObjectStack은 약 25가지 필드 타입을 제공합니다. 가장 많이 사용되는 것들: - -### 스칼라 - -| 타입 | 저장하는 것 | 헬퍼 | -|---|---|---| -| `text` | 짧은 문자열 | `Field.text({ maxLength, required })` | -| `textarea` | 긴 문자열 | `Field.textarea(...)` | -| `markdown` | 마크다운이 포함된 리치 텍스트 | `Field.markdown(...)` | -| `number` | 정수 | `Field.number({ min, max })` | -| `decimal` | 정확한 소수 (금액 등) | `Field.decimal({ precision, scale })` | -| `boolean` | 참/거짓 | `Field.boolean({ defaultValue })` | -| `date` | 달력 날짜 | `Field.date(...)` | -| `datetime` | 타임스탬프 | `Field.datetime(...)` | -| `email` | 유효성 검증된 이메일 | `Field.email(...)` | -| `url` | 유효성 검증된 URL | `Field.url(...)` | -| `phone` | 유효성 검증된 전화번호 | `Field.phone(...)` | -| `json` | 임의의 JSON | `Field.json(...)` | - -### 선택 - -| 타입 | 용도 | -|---|---| -| `select` | 단일 선택 (열거형) | -| `multiselect` | 다중 선택 | - -### 관계 - -| 타입 | 카디널리티 | 헬퍼 | -|---|---|---| -| `lookup` | 일대다 (FK) | `Field.lookup({ reference: 'sys_user' })` | -| `masterDetail` | 연쇄 삭제가 있는 일대다 | `Field.masterDetail({ reference: 'order' })` | - -### 파일 및 미디어 - -| 타입 | 저장하는 것 | -|---|---| -| `file` | 스토리지 서비스를 통한 단일 파일 | -| `image` | 미리보기가 있는 이미지 파일 | - -### 계산 / 파생 - -| 타입 | 동작 | -|---|---| -| `formula` | CEL 표현식으로부터 읽기 시점에 계산됨 | -| `summary` | 연관 레코드의 집계 (합계/개수/평균) | -| `autonumber` | 시퀀스 (`INV-{000001}`) | -| `created`, `lastModified` | 시스템이 관리하는 타임스탬프 | -| `createdBy`, `lastModifiedBy` | 시스템이 관리하는 사용자 참조 | - -## 필수 / 고유 / 기본값 - -모든 스칼라 필드에서 사용되는 공통 수정자: - -```ts -Field.text({ - label: 'Code', - required: true, - unique: true, // unique constraint enforced at DB level - defaultValue: '', - helpText: 'Internal short code', -}) -``` - -## 유효성 검사 - -인라인: - -```ts -Field.number({ label: 'Quantity', min: 1, max: 9999 }) -Field.text({ label: 'SKU', pattern: '^[A-Z]{3}-[0-9]{4}$' }) -``` - -객체 수준 규칙 (필드 간): - -```ts -ObjectSchema.create({ - name: 'order', - fields: { /* ... */ }, - validations: [ - { - name: 'discount_lt_total', - message: 'Discount cannot exceed total', - condition: 'discount < total', - }, - ], -}); -``` - -유효성 검사는 모든 쓰기 작업 — REST, Console, ObjectQL — 에서 실행되므로, -"뒷문(back door)"은 존재하지 않습니다. - -## 인덱스 및 성능 - -```ts -ObjectSchema.create({ - name: 'order', - fields: { /* ... */ }, - indexes: [ - { fields: ['status', 'created_at'] }, - { fields: ['account', 'created_at'], unique: false }, - ], -}); -``` - -드라이버는 스키마 동기화 시 실제 DB 인덱스를 생성합니다. - -## 필드 그룹 - -긴 폼의 경우, Console에서 필드를 그룹화합니다: - -```ts -ObjectSchema.create({ - name: 'task', - fieldGroups: [ - { key: 'core', label: 'Task', icon: 'check-square' }, - { key: 'planning', label: 'Planning', icon: 'calendar' }, - { key: 'meta', label: 'Metadata', icon: 'info', defaultExpanded: false }, - ], - fields: { - subject: Field.text({ label: 'Subject', group: 'core' }), - due: Field.date({ label: 'Due', group: 'planning' }), - }, -}); -``` - -## 라이프사이클 및 소유권 - -```ts -ObjectSchema.create({ - name: 'task', - ownership: 'own', // 'own' | 'shared' | 'system' - enable: { - apiEnabled: true, // generated REST endpoints - trackHistory: true, // audit log of field changes - feeds: true, // sys_comment / sys_activity / @mentions - trash: true, // soft-delete with restore (default true) - }, -}); -``` - -## 시스템 객체 (모든 프로젝트에 무료 제공) - -이것들은 선언할 필요가 없습니다 — 항상 존재합니다: - -| 객체 | 내용 | -|---|---| -| `sys_user` | 사용자 계정 | -| `sys_org` | 조직 / 테넌트 | -| `sys_member` | 조직 멤버십 | -| `sys_role`, `sys_permission_set` | RBAC 기본 요소 | -| `sys_audit_log` | 감사 추적 (감사 기능이 로드되었을 때) | -| `sys_file`, `sys_attachment` | 파일 메타데이터 (스토리지가 로드되었을 때) | -| `sys_comment`, `sys_activity` | 피드 / chatter (피드가 로드되었을 때) | -| `sys_session`, `sys_api_key` | 인증 아티팩트 | -| `sys_webhook`, `sys_webhook_delivery` | 웹훅 구독 (활성화되었을 때) | - -`lookup` 필드에서 이름으로 참조합니다 — 예: `Field.lookup({ reference: 'sys_user' })`. - -## 다형성 플랫폼 기능 - -`feeds: true`와 `trackHistory: true`를 활성화하면, 객체가 -자동으로 다음에 참여합니다: - -- `sys_comment` (thread_id = `:`) -- `sys_attachment` (parent_object = ``, parent_id = ``) -- `sys_activity` (타임라인) -- `sys_audit_log` (필드 수준 차이) - -이것들을 객체별로 연결할 필요가 없습니다 — 플랫폼에서 다형성으로 처리됩니다. - -## 다음 단계 - -- [권한](/docs/configure/permissions) — 객체에 대한 접근을 통제합니다 -- [플로우 / 자동화](/docs/build/automation/flows) — 레코드 변경에 반응합니다 -- [API 접근](/docs/configure/api-access) — 생성된 REST를 호출합니다 -- [`os explain`](/docs/reference/cli) — 렌더링된 스키마를 출력합니다 -- [`@objectstack/spec` 소스](https://github.com/objectstack-ai/objectstack/tree/main/packages/spec) — 스키마가 계약이며, 여기의 모든 것은 그로부터 파생됩니다 diff --git a/content/docs/build/data/index.mdx b/content/docs/build/data/index.mdx index d49b519..3a13156 100644 --- a/content/docs/build/data/index.mdx +++ b/content/docs/build/data/index.mdx @@ -69,12 +69,14 @@ import { defineStack } from '@objectstack/spec'; import * as objects from './src/objects'; export default defineStack({ - manifest: { id: 'my.app', namespace: 'myapp', version: '0.1.0', type: 'app', name: 'My App' }, + manifest: { id: 'my.app', namespace: 'todo', version: '0.1.0', type: 'app', name: 'My App' }, objects: Object.values(objects), }); ``` -That's all you need. `os dev` recompiles, and `/api/v1/data/todo_task`, +Every object name starts with the stack's `namespace` and an underscore: +`todo_task` in the `todo` namespace. That's all you need. `os dev` +recompiles, and `/api/v1/data/todo_task`, the generated Task view, and its permission row all appear. ## Field types @@ -88,8 +90,8 @@ ObjectStack ships ~25 field types. The most-used ones: | `text` | Short string | `Field.text({ maxLength, required })` | | `textarea` | Long string | `Field.textarea(...)` | | `markdown` | Rich text with markdown | `Field.markdown(...)` | -| `number` | Integer | `Field.number({ min, max })` | -| `decimal` | Exact decimal (money, etc.) | `Field.decimal({ precision, scale })` | +| `number` | Number; `scale` sets the decimal places | `Field.number({ min, max, scale })` | +| `currency` | Money amount | `Field.currency(...)` | | `boolean` | True/false | `Field.boolean({ defaultValue })` | | `date` | Calendar date | `Field.date(...)` | | `datetime` | Timestamp | `Field.datetime(...)` | @@ -109,8 +111,8 @@ ObjectStack ships ~25 field types. The most-used ones: | Type | Cardinality | Helper | |---|---|---| -| `lookup` | One-to-many (FK) | `Field.lookup({ reference: 'sys_user' })` | -| `masterDetail` | One-to-many with cascade delete | `Field.masterDetail({ reference: 'order' })` | +| `lookup` | One-to-many (FK) | `Field.lookup('sys_user', { label })` | +| `master_detail` | One-to-many with cascade delete | `Field.masterDetail('order', { label })` | ### Files & media @@ -139,7 +141,7 @@ Field.text({ required: true, unique: true, // unique constraint enforced at DB level defaultValue: '', - helpText: 'Internal short code', + inlineHelpText: 'Internal short code', }) ``` @@ -149,20 +151,35 @@ Inline: ```ts Field.number({ label: 'Quantity', min: 1, max: 9999 }) -Field.text({ label: 'SKU', pattern: '^[A-Z]{3}-[0-9]{4}$' }) +Field.text({ label: 'SKU', minLength: 8, maxLength: 8 }) ``` -Object-level rules (cross-field): +Object-level rules — a cross-field rule, and a `format` rule for a pattern. +A rule's `condition` describes the invalid record: when it is true, the write +is refused. ```ts ObjectSchema.create({ name: 'order', - fields: { /* ... */ }, + fields: { + sku: Field.text({ label: 'SKU' }), + discount: Field.currency({ label: 'Discount' }), + total: Field.currency({ label: 'Total' }), + }, validations: [ { + type: 'cross_field', name: 'discount_lt_total', message: 'Discount cannot exceed total', - condition: 'discount < total', + fields: ['discount', 'total'], + condition: 'record.discount > record.total', + }, + { + type: 'format', + name: 'sku_format', + message: 'SKU looks like ABC-1234', + field: 'sku', + regex: '^[A-Z]{3}-[0-9]{4}$', }, ], }); @@ -210,7 +227,8 @@ ObjectSchema.create({ ```ts ObjectSchema.create({ name: 'task', - ownership: 'own', // 'own' | 'shared' | 'system' + fields: { subject: Field.text({ label: 'Subject' }) }, + ownership: 'user', // 'user' (default) | 'business_unit' | 'org' | 'none' sharingModel: 'private', // OWD — custom objects default to private (v13) enable: { apiEnabled: true, // generated REST endpoints (default true) @@ -230,6 +248,7 @@ exist) is a parse error naming the key, not a silently dropped setting. The seven flags above are the whole of it, plus one more key: ```ts +// Keys of an object; its name and fields are omitted. enable: { apiMethods: ['get', 'list', 'create', 'update'], // no delete over the API } @@ -284,10 +303,12 @@ bounds their growth (ObjectStack 14.4, ADR-0057): ```ts ObjectSchema.create({ name: 'my_event', + fields: { payload: Field.json({ label: 'Payload' }) }, lifecycle: { - class: 'event', // 'record' | 'audit' | 'telemetry' | 'transient' | 'event' - retention: '14d', // reaper deletes rows past the window - storage: { rotation: 'weekly' }, // time-sharded tables, O(1) expiry + class: 'event', // 'record' | 'audit' | 'telemetry' | 'transient' | 'event' + retention: { maxAge: '14d' }, // reaper deletes rows past the window + // time-sharded table: 2 shards of 1 week + storage: { strategy: 'rotation', shards: 2, unit: 'week' }, }, }); ``` @@ -315,7 +336,7 @@ You don't have to declare these — they're always there: | `sys_session`, `sys_api_key` | Auth artifacts | | `sys_webhook`, `sys_webhook_delivery` | Webhook subs (when enabled) | -Reference them in `lookup` fields by name — e.g. `Field.lookup({ reference: 'sys_user' })`. +Reference them in `lookup` fields by name — e.g. `Field.lookup('sys_user', { label: 'Owner' })`. ## Polymorphic platform features diff --git a/content/docs/build/data/index.zh-Hans.mdx b/content/docs/build/data/index.zh-Hans.mdx deleted file mode 100644 index 2bf4f10..0000000 --- a/content/docs/build/data/index.zh-Hans.mdx +++ /dev/null @@ -1,288 +0,0 @@ ---- -title: 数据模型 -description: 对象、字段、关系、校验、索引 —— 描述给 AI,或者用 TypeScript 写。 -translation: - source_sha: 212c8e723195aa898b7942afad5cc47073f4661161aecd0da62b89223a34e462 - guide_rev: 1 - mode: auto ---- - -数据模型是你应用的唯一事实来源。一旦对象存在,ObjectOS 就免费给你 REST API、Console 视图、RBAC 检查点、审计日志条目和 AI 工具暴露。 - -**多数客户从不手写 Schema。**他们在 [AI Builder](/docs/build/ai-builder) 里描述他们想要的,平台就会创建对象、字段、索引与翻译。本页描述底层结构 —— 让你理解 AI 生成的内容,并在需要时直接编辑。 - -## 编写路径 - -| 路径 | 形态 | -|---|---| -| **AI Builder**(主推) | *"创建一个 `support_ticket` 对象,含 subject、description、priority、status、assignee。"* | -| **Console 点选构建** | Console → Objects → New Object → 表单 | -| **TypeScript(`*.object.ts`)** | 下面展示的 TS —— 通常在某个 fork 的[模板](/docs/build/templates)里 | - -三种方式产生同一份 Schema。Schema 是规范的;其他一切都从它派生。 - -## 对象的解剖 - -```ts -// src/objects/task.ts -import { ObjectSchema, Field } from '@objectstack/spec/data'; - -export const Task = ObjectSchema.create({ - name: 'todo_task', - label: 'Task', - pluralLabel: 'Tasks', - icon: 'check-square', - description: 'A single unit of work.', - - fields: { - subject: Field.text({ label: 'Subject', required: true, maxLength: 200 }), - description: Field.markdown({ label: 'Description' }), - status: Field.select({ - label: 'Status', - options: [ - { label: 'To Do', value: 'todo', default: true }, - { label: 'In Progress', value: 'in_progress' }, - { label: 'Done', value: 'done' }, - ], - }), - due: Field.date({ label: 'Due' }), - assignee: Field.lookup('sys_user', { label: 'Assignee' }), - }, - - enable: { - trackHistory: true, // 在审计日志里记录字段变更 - apiEnabled: true, // 暴露 REST 端点(默认 true) - feeds: true, // chatter / 评论 / @提及 - }, -}); -``` - -在你的 stack 中注册它: - -```ts -// objectstack.config.ts -import { defineStack } from '@objectstack/spec'; -import * as objects from './src/objects'; - -export default defineStack({ - manifest: { id: 'my.app', namespace: 'myapp', version: '0.1.0', type: 'app', name: 'My App' }, - objects: Object.values(objects), -}); -``` - -需要的就这些。`os dev` 重编译之后,`/api/v1/data/todo_task`、Console 的 Task 视图和 Console 的权限行就都出现了。 - -## 字段类型 - -ObjectStack 自带约 25 种字段类型。最常用的: - -### 标量 - -| 类型 | 存什么 | Helper | -|---|---|---| -| `text` | 短字符串 | `Field.text({ maxLength, required })` | -| `textarea` | 长字符串 | `Field.textarea(...)` | -| `markdown` | Markdown 富文本 | `Field.markdown(...)` | -| `number` | 整数 | `Field.number({ min, max })` | -| `decimal` | 精确小数(金额等) | `Field.decimal({ precision, scale })` | -| `boolean` | True/false | `Field.boolean({ defaultValue })` | -| `date` | 日历日期 | `Field.date(...)` | -| `datetime` | 时间戳 | `Field.datetime(...)` | -| `email` | 经校验的邮箱 | `Field.email(...)` | -| `url` | 经校验的 URL | `Field.url(...)` | -| `phone` | 经校验的电话 | `Field.phone(...)` | -| `json` | 任意 JSON | `Field.json(...)` | - -### 选项 - -| 类型 | 用于 | -|---|---| -| `select` | 单选(枚举) | -| `multiselect` | 多选 | - -### 关系 - -| 类型 | 基数 | Helper | -|---|---|---| -| `lookup` | 一对多(外键) | `Field.lookup({ reference: 'sys_user' })` | -| `masterDetail` | 一对多并级联删除 | `Field.masterDetail({ reference: 'order' })` | - -### 文件与媒体 - -| 类型 | 存什么 | -|---|---| -| `file` | 通过存储服务保存的一个文件 | -| `image` | 带预览的图片文件 | - -### 计算 / 派生 - -| 类型 | 行为 | -|---|---| -| `formula` | 读时按 CEL 表达式计算 | -| `summary` | 关联记录的聚合(sum/count/avg) | -| `autonumber` | 序列号(`INV-{000001}`) | -| `created`、`lastModified` | 系统维护的时间戳 | -| `createdBy`、`lastModifiedBy` | 系统维护的用户引用 | - -## required / unique / 默认值 - -每个标量字段通用的修饰符: - -```ts -Field.text({ - label: 'Code', - required: true, - unique: true, // 数据库层强制唯一约束 - defaultValue: '', - helpText: 'Internal short code', -}) -``` - -## 校验 - -内联: - -```ts -Field.number({ label: 'Quantity', min: 1, max: 9999 }) -Field.text({ label: 'SKU', pattern: '^[A-Z]{3}-[0-9]{4}$' }) -``` - -对象级规则(跨字段): - -```ts -ObjectSchema.create({ - name: 'order', - fields: { /* ... */ }, - validations: [ - { - name: 'discount_lt_total', - message: 'Discount cannot exceed total', - condition: 'discount < total', - }, - ], -}); -``` - -校验对每次写入都执行 —— REST、Console、ObjectQL —— 没有"后门"。 - -## 索引与性能 - -```ts -ObjectSchema.create({ - name: 'order', - fields: { /* ... */ }, - indexes: [ - { fields: ['status', 'created_at'] }, - { fields: ['account', 'created_at'], unique: false }, - ], -}); -``` - -驱动在 Schema 同步时创建真实的数据库索引。 - -## 字段分组 - -针对长表单,在 Console 里把字段分组: - -```ts -ObjectSchema.create({ - name: 'task', - fieldGroups: [ - { key: 'core', label: 'Task', icon: 'check-square' }, - { key: 'planning', label: 'Planning', icon: 'calendar' }, - { key: 'meta', label: 'Metadata', icon: 'info', defaultExpanded: false }, - ], - fields: { - subject: Field.text({ label: 'Subject', group: 'core' }), - due: Field.date({ label: 'Due', group: 'planning' }), - }, -}); -``` - -## 能力开关与所有权 - -```ts -ObjectSchema.create({ - name: 'task', - ownership: 'own', // 'own' | 'shared' | 'system' - sharingModel: 'private', // OWD——自定义对象默认私有(v13) - enable: { - apiEnabled: true, // 生成 REST 端点 - trackHistory: true, // History 选项卡 + 字段级差异(默认 true) - feeds: true, // sys_comment / @提及(默认 true) - activities: true, // CRUD 镜像到 sys_activity 时间线(默认 true) - files: true, // 附件面板(需显式开启,默认 false) - trash: true, // 软删除并可恢复(默认 true) - }, -}); -``` - -> **16.0 变更:**对象级 `softDelete` 属性已被移除 —— 再传入它, -> `ObjectSchema.create` 会抛出带定位信息的错误。对应的现行能力是 -> `enable: { trash: true }`(软删除并可恢复),默认开启。同一轮清理还 -> 移除了 `versioning`、`search`(改用顶层 `searchableFields`)、 -> `recordName`(改用作为 `nameField` 的自动编号字段)、`keyPrefix`、 -> `tags`、`active` 和 `abstract`。 - -自 ObjectStack 14 起,`enable.*` 开关是**强制执行**的,而不再只是声明: -`feeds: false` 会以 403 `FEEDS_DISABLED` 拒绝评论创建;`files` 必须显式开启 -后才能创建 `sys_attachment` 行(否则返回 403 `FILES_DISABLED`); -`activities` / `trackHistory` 控制时间线和 History 选项卡。无论开关如何, -合规用途的 `sys_audit_log` 行始终会写入。 - -## 数据生命周期(保留策略) - -大数据量对象可以声明 `lifecycle` 块,让平台约束其增长(ObjectStack 14.4,ADR-0057): - -```ts -ObjectSchema.create({ - name: 'my_event', - lifecycle: { - class: 'event', // 'record' | 'audit' | 'telemetry' | 'transient' | 'event' - retention: '14d', // 回收器删除超出窗口的行 - storage: { rotation: 'weekly' }, // 按时间分片表,O(1) 过期 - }, -}); -``` - -内置的 LifecycleService(可用 `OS_LIFECYCLE_DISABLED=1` 关闭)负责回收过期行、 -轮换分片表,并归档 audit 类对象。`sys_activity`(14 天)、`sys_audit_log` -(热存 90 天后归档)等平台对象自带生命周期声明,可通过 -`lifecycle.retention_overrides` 设置按环境调整。 - -## 系统对象(每个项目免费自带) - -这些你不必声明 —— 它们一直都在: - -| 对象 | 是什么 | -|---|---| -| `sys_user` | 用户账户 | -| `sys_org` | 组织 / 租户 | -| `sys_member` | 组织成员关系 | -| `sys_position`、`sys_permission_set` | RBAC 原语 | -| `sys_audit_log` | 审计轨迹(加载审计能力时) | -| `sys_file`、`sys_attachment` | 文件元数据(加载存储能力时) | -| `sys_comment`、`sys_activity` | Feed / chatter(加载 feed 能力时) | -| `sys_session`、`sys_api_key` | 认证产物 | -| `sys_webhook`、`sys_webhook_delivery` | Webhook 订阅(启用时) | - -在 `lookup` 字段中按名称引用它们 —— 如 `Field.lookup({ reference: 'sys_user' })`。 - -## 多态平台特性 - -当你启用 `feeds: true` 与 `trackHistory: true`,你的对象就自动加入: - -- `sys_comment`(thread_id = `:`) -- `sys_attachment`(parent_object = ``,parent_id = ``) -- `sys_activity`(时间线) -- `sys_audit_log`(字段级 diff) - -你不必为每个对象单独接线 —— 它们在平台上是多态的。 - -## 下一步 - -- [Permissions](/docs/configure/permissions) —— 给对象访问开闸 -- [Flows / Automation](/docs/build/automation/flows) —— 对记录变更做出反应 -- [API Access](/docs/configure/api-access) —— 调用生成的 REST -- [`os explain`](/docs/reference/cli) —— 打印渲染后的 Schema -- [`@objectstack/spec` 源码](https://github.com/objectstack-ai/objectstack/tree/main/packages/spec) —— Schema 即契约;此处的一切都从它派生 diff --git a/content/docs/build/data/index.zh-Hant.mdx b/content/docs/build/data/index.zh-Hant.mdx deleted file mode 100644 index e1d0eb2..0000000 --- a/content/docs/build/data/index.zh-Hant.mdx +++ /dev/null @@ -1,289 +0,0 @@ ---- -# @generated zh-Hant from zh-Hans -- do not edit. Edit the English source, then regenerate: pnpm --filter @objectos/docs gen:zh-hant -title: 資料模型 -description: 物件、欄位、關係、校驗、索引 —— 描述給 AI,或者用 TypeScript 寫。 -translation: - source_sha: 212c8e723195aa898b7942afad5cc47073f4661161aecd0da62b89223a34e462 - guide_rev: 1 - mode: auto ---- - -資料模型是你應用的唯一事實來源。一旦物件存在,ObjectOS 就免費給你 REST API、Console 檢視、RBAC 檢查點、審計日誌條目和 AI 工具暴露。 - -**多數客戶從不手寫 Schema。**他們在 [AI Builder](/docs/build/ai-builder) 裡描述他們想要的,平臺就會建立物件、欄位、索引與翻譯。本頁描述底層結構 —— 讓你理解 AI 生成的內容,並在需要時直接編輯。 - -## 編寫路徑 - -| 路徑 | 形態 | -|---|---| -| **AI Builder**(主推) | *"建立一個 `support_ticket` 物件,含 subject、description、priority、status、assignee。"* | -| **Console 點選構建** | Console → Objects → New Object → 表單 | -| **TypeScript(`*.object.ts`)** | 下面展示的 TS —— 通常在某個 fork 的[模板](/docs/build/templates)裡 | - -三種方式產生同一份 Schema。Schema 是規範的;其他一切都從它派生。 - -## 物件的解剖 - -```ts -// src/objects/task.ts -import { ObjectSchema, Field } from '@objectstack/spec/data'; - -export const Task = ObjectSchema.create({ - name: 'todo_task', - label: 'Task', - pluralLabel: 'Tasks', - icon: 'check-square', - description: 'A single unit of work.', - - fields: { - subject: Field.text({ label: 'Subject', required: true, maxLength: 200 }), - description: Field.markdown({ label: 'Description' }), - status: Field.select({ - label: 'Status', - options: [ - { label: 'To Do', value: 'todo', default: true }, - { label: 'In Progress', value: 'in_progress' }, - { label: 'Done', value: 'done' }, - ], - }), - due: Field.date({ label: 'Due' }), - assignee: Field.lookup('sys_user', { label: 'Assignee' }), - }, - - enable: { - trackHistory: true, // 在审计日志里记录字段变更 - apiEnabled: true, // 暴露 REST 端点(默认 true) - feeds: true, // chatter / 评论 / @提及 - }, -}); -``` - -在你的 stack 中註冊它: - -```ts -// objectstack.config.ts -import { defineStack } from '@objectstack/spec'; -import * as objects from './src/objects'; - -export default defineStack({ - manifest: { id: 'my.app', namespace: 'myapp', version: '0.1.0', type: 'app', name: 'My App' }, - objects: Object.values(objects), -}); -``` - -需要的就這些。`os dev` 重編譯之後,`/api/v1/data/todo_task`、Console 的 Task 檢視和 Console 的許可權行就都出現了。 - -## 欄位型別 - -ObjectStack 自帶約 25 種欄位型別。最常用的: - -### 標量 - -| 型別 | 存什麼 | Helper | -|---|---|---| -| `text` | 短字串 | `Field.text({ maxLength, required })` | -| `textarea` | 長字串 | `Field.textarea(...)` | -| `markdown` | Markdown 富文本 | `Field.markdown(...)` | -| `number` | 整數 | `Field.number({ min, max })` | -| `decimal` | 精確小數(金額等) | `Field.decimal({ precision, scale })` | -| `boolean` | True/false | `Field.boolean({ defaultValue })` | -| `date` | 日曆日期 | `Field.date(...)` | -| `datetime` | 時間戳 | `Field.datetime(...)` | -| `email` | 經校驗的郵箱 | `Field.email(...)` | -| `url` | 經校驗的 URL | `Field.url(...)` | -| `phone` | 經校驗的電話 | `Field.phone(...)` | -| `json` | 任意 JSON | `Field.json(...)` | - -### 選項 - -| 型別 | 用於 | -|---|---| -| `select` | 單選(列舉) | -| `multiselect` | 多選 | - -### 關係 - -| 型別 | 基數 | Helper | -|---|---|---| -| `lookup` | 一對多(外部索引鍵) | `Field.lookup({ reference: 'sys_user' })` | -| `masterDetail` | 一對多並級聯刪除 | `Field.masterDetail({ reference: 'order' })` | - -### 檔案與媒體 - -| 型別 | 存什麼 | -|---|---| -| `file` | 通過儲存服務儲存的一個檔案 | -| `image` | 帶預覽的圖片檔案 | - -### 計算 / 派生 - -| 型別 | 行為 | -|---|---| -| `formula` | 讀時按 CEL 表示式計算 | -| `summary` | 關聯記錄的聚合(sum/count/avg) | -| `autonumber` | 序列號(`INV-{000001}`) | -| `created`、`lastModified` | 系統維護的時間戳 | -| `createdBy`、`lastModifiedBy` | 系統維護的使用者引用 | - -## required / unique / 預設值 - -每個標量欄位通用的修飾符: - -```ts -Field.text({ - label: 'Code', - required: true, - unique: true, // 数据库层强制唯一约束 - defaultValue: '', - helpText: 'Internal short code', -}) -``` - -## 校驗 - -內聯: - -```ts -Field.number({ label: 'Quantity', min: 1, max: 9999 }) -Field.text({ label: 'SKU', pattern: '^[A-Z]{3}-[0-9]{4}$' }) -``` - -物件級規則(跨欄位): - -```ts -ObjectSchema.create({ - name: 'order', - fields: { /* ... */ }, - validations: [ - { - name: 'discount_lt_total', - message: 'Discount cannot exceed total', - condition: 'discount < total', - }, - ], -}); -``` - -校驗對每次寫入都執行 —— REST、Console、ObjectQL —— 沒有"後門"。 - -## 索引與效能 - -```ts -ObjectSchema.create({ - name: 'order', - fields: { /* ... */ }, - indexes: [ - { fields: ['status', 'created_at'] }, - { fields: ['account', 'created_at'], unique: false }, - ], -}); -``` - -驅動在 Schema 同步時建立真實的資料庫索引。 - -## 欄位分組 - -針對長表單,在 Console 裡把欄位分組: - -```ts -ObjectSchema.create({ - name: 'task', - fieldGroups: [ - { key: 'core', label: 'Task', icon: 'check-square' }, - { key: 'planning', label: 'Planning', icon: 'calendar' }, - { key: 'meta', label: 'Metadata', icon: 'info', defaultExpanded: false }, - ], - fields: { - subject: Field.text({ label: 'Subject', group: 'core' }), - due: Field.date({ label: 'Due', group: 'planning' }), - }, -}); -``` - -## 能力開關與所有權 - -```ts -ObjectSchema.create({ - name: 'task', - ownership: 'own', // 'own' | 'shared' | 'system' - sharingModel: 'private', // OWD——自定义对象默认私有(v13) - enable: { - apiEnabled: true, // 生成 REST 端点 - trackHistory: true, // History 选项卡 + 字段级差异(默认 true) - feeds: true, // sys_comment / @提及(默认 true) - activities: true, // CRUD 镜像到 sys_activity 时间线(默认 true) - files: true, // 附件面板(需显式开启,默认 false) - trash: true, // 软删除并可恢复(默认 true) - }, -}); -``` - -> **16.0 變更:**物件級 `softDelete` 屬性已被移除 —— 再傳入它, -> `ObjectSchema.create` 會丟擲帶定位資訊的錯誤。對應的現行能力是 -> `enable: { trash: true }`(軟刪除並可恢復),預設開啟。同一輪清理還 -> 移除了 `versioning`、`search`(改用頂層 `searchableFields`)、 -> `recordName`(改用作為 `nameField` 的自動編號欄位)、`keyPrefix`、 -> `tags`、`active` 和 `abstract`。 - -自 ObjectStack 14 起,`enable.*` 開關是**強制執行**的,而不再只是宣告: -`feeds: false` 會以 403 `FEEDS_DISABLED` 拒絕評論建立;`files` 必須顯式開啟 -後才能建立 `sys_attachment` 行(否則返回 403 `FILES_DISABLED`); -`activities` / `trackHistory` 控制時間線和 History 選項卡。無論開關如何, -合規用途的 `sys_audit_log` 行始終會寫入。 - -## 資料生命週期(保留策略) - -大數據量物件可以宣告 `lifecycle` 塊,讓平臺約束其增長(ObjectStack 14.4,ADR-0057): - -```ts -ObjectSchema.create({ - name: 'my_event', - lifecycle: { - class: 'event', // 'record' | 'audit' | 'telemetry' | 'transient' | 'event' - retention: '14d', // 回收器删除超出窗口的行 - storage: { rotation: 'weekly' }, // 按时间分片表,O(1) 过期 - }, -}); -``` - -內建的 LifecycleService(可用 `OS_LIFECYCLE_DISABLED=1` 關閉)負責回收過期行、 -輪換分片表,並歸檔 audit 類物件。`sys_activity`(14 天)、`sys_audit_log` -(熱存 90 天后歸檔)等平臺物件自帶生命週期宣告,可通過 -`lifecycle.retention_overrides` 設定按環境調整。 - -## 系統物件(每個專案免費自帶) - -這些你不必宣告 —— 它們一直都在: - -| 物件 | 是什麼 | -|---|---| -| `sys_user` | 使用者賬戶 | -| `sys_org` | 組織 / 租戶 | -| `sys_member` | 組織成員關係 | -| `sys_position`、`sys_permission_set` | RBAC 原語 | -| `sys_audit_log` | 審計軌跡(載入審計能力時) | -| `sys_file`、`sys_attachment` | 檔案後設資料(載入儲存能力時) | -| `sys_comment`、`sys_activity` | Feed / chatter(載入 feed 能力時) | -| `sys_session`、`sys_api_key` | 認證產物 | -| `sys_webhook`、`sys_webhook_delivery` | Webhook 訂閱(啟用時) | - -在 `lookup` 欄位中按名稱引用它們 —— 如 `Field.lookup({ reference: 'sys_user' })`。 - -## 多型平臺特性 - -當你啟用 `feeds: true` 與 `trackHistory: true`,你的物件就自動加入: - -- `sys_comment`(thread_id = `:`) -- `sys_attachment`(parent_object = ``,parent_id = ``) -- `sys_activity`(時間線) -- `sys_audit_log`(欄位級 diff) - -你不必為每個物件單獨接線 —— 它們在平臺上是多型的。 - -## 下一步 - -- [Permissions](/docs/configure/permissions) —— 給物件訪問開閘 -- [Flows / Automation](/docs/build/automation/flows) —— 對記錄變更做出反應 -- [API Access](/docs/configure/api-access) —— 呼叫生成的 REST -- [`os explain`](/docs/reference/cli) —— 列印渲染後的 Schema -- [`@objectstack/spec` 原始碼](https://github.com/objectstack-ai/objectstack/tree/main/packages/spec) —— Schema 即契約;此處的一切都從它派生 diff --git a/content/docs/build/data/relationships.mdx b/content/docs/build/data/relationships.mdx index bd332b0..c39ca04 100644 --- a/content/docs/build/data/relationships.mdx +++ b/content/docs/build/data/relationships.mdx @@ -12,7 +12,7 @@ the parent page, and `expand` in queries. | Type | Cardinality | Delete default | Use for | |---|---|---|---| | `lookup` | Many-to-one | `set_null` | Loose references (ticket → assignee) | -| `masterDetail` | Many-to-one, owned | `cascade` | Parent-child ownership (order → line items) | +| `master_detail` | Many-to-one, owned | `cascade` | Parent-child ownership (order → line items) | | `tree` | Self-referencing | — | Hierarchies (categories, org charts) | | `user` | Many-to-one | `set_null` | Person picker — a lookup specialized to `sys_user` | | `summary` | Roll-up onto parent | — | Aggregate children (sum, count, avg) | @@ -84,7 +84,7 @@ inline as line items on the parent form. export const OrderLine = ObjectSchema.create({ name: 'order_line', fields: { - order: Field.masterDetail({ reference: 'order', label: 'Order' }), + order: Field.masterDetail('order', { label: 'Order' }), product: Field.lookup('product', { label: 'Product' }), quantity: Field.number({ label: 'Quantity', min: 1 }), }, @@ -141,9 +141,15 @@ object**: an object with two relationship fields, one to each side. export const Enrollment = ObjectSchema.create({ name: 'enrollment', fields: { - student: Field.masterDetail({ reference: 'student', label: 'Student' }), - course: Field.masterDetail({ reference: 'course', label: 'Course' }), - grade: Field.select({ label: 'Grade', options: [ /* ... */ ] }), + student: Field.masterDetail('student', { label: 'Student' }), + course: Field.masterDetail('course', { label: 'Course' }), + grade: Field.select({ + label: 'Grade', + options: [ + { label: 'Pass', value: 'pass' }, + { label: 'Fail', value: 'fail' }, + ], + }), }, indexes: [ { fields: ['student', 'course'], unique: true }, // one enrollment per pair @@ -161,7 +167,7 @@ Aggregate child records onto the parent with a `summary` field. Only valid on master objects — the parent side of a master-detail: ```ts -// On the Order object +// Fields of an object — this one belongs on the Order object. total_lines: { type: 'summary', label: 'Line Count', diff --git a/content/docs/build/data/validation-rules.mdx b/content/docs/build/data/validation-rules.mdx index 3714098..7da4e90 100644 --- a/content/docs/build/data/validation-rules.mdx +++ b/content/docs/build/data/validation-rules.mdx @@ -27,6 +27,7 @@ Validation is layered — use the lowest layer that can express the rule: The universal modifiers work on every field type: ```ts +// Fields of an object; the object around them is omitted. import { ObjectSchema, Field } from '@objectstack/spec/data'; fields: { @@ -76,7 +77,13 @@ export const Order = ObjectSchema.create({ name: 'order', fields: { amount: Field.currency({ label: 'Amount', required: true }), - status: Field.select({ label: 'Status', options: [ /* ... */ ] }), + status: Field.select({ + label: 'Status', + options: [ + { label: 'Draft', value: 'draft' }, + { label: 'Submitted', value: 'submitted' }, + ], + }), }, validations: [ @@ -126,6 +133,8 @@ Every rule type shares this base shape: Two rules you'll reach for constantly: ```ts +// Two validation rules, entries of the object's `validations` array. + // State machine — enforce the status flow { name: 'order_status_transitions', @@ -176,14 +185,17 @@ SELECT-then-INSERT check is inherently racy (TOCTOU); a database unique constraint is not. Enforce uniqueness at the data layer: ```ts -// Field-level -email: Field.email({ label: 'Contact Email', unique: true }), +// Keys of an object; its name and its other fields are omitted. +fields: { + // Field-level + email: Field.email({ label: 'Contact Email', unique: true }), +}, // Composite / scoped via indexes indexes: [ { fields: ['code', 'organization'], unique: true }, // add `partial` for a scoped/conditional constraint -] +], ``` The same logic excludes async/remote validation (a client-form concern diff --git a/content/docs/build/interface/actions.de.mdx b/content/docs/build/interface/actions.de.mdx deleted file mode 100644 index 0f91136..0000000 --- a/content/docs/build/interface/actions.de.mdx +++ /dev/null @@ -1,206 +0,0 @@ ---- -title: Actions -description: Benannte Operationen, die die Plattform als REST-Endpunkte, Console-Schaltflächen, Flow-Schritte und AI-Tools bereitstellt — aus einer einzigen Deklaration. -translation: - source_sha: d4f9d1d0443d08d33b2ea0aa548c7b8db1541a8b693a65d481af6cafba577018 - guide_rev: 1 - mode: auto ---- - -Eine **Action** ist eine benannte Operation auf einem Objekt. Deklarieren Sie sie einmal und -sie erscheint als: - -- ein **REST-Endpunkt** unter `/api/v1/actions//` -- eine **Schaltfläche** in der Datensatzdetailansicht der Console -- ein **Flow-Schritt** (`type: 'action'`) für die Automatisierung -- ein **AI-Tool** (`action_`) für Agents und den AI Builder - -Sie wiederholen sich nicht über vier Oberflächen hinweg. Eine Deklaration; vier -Möglichkeiten zum Aufruf. - -## Eine Action deklarieren - -```ts -// src/actions/approve_invoice.action.ts -import { Action } from '@objectstack/spec'; - -export const approveInvoice = Action.create({ - name: 'approve_invoice', // lowercase snake_case (machine id) - label: 'Approve Invoice', - objectName: 'invoice', // attaches to the invoice object - icon: 'check', - variant: 'primary', - locations: ['record_header'], // where the button shows - confirmText: 'Approve this invoice?', - successMessage: 'Invoice approved', - refreshAfter: true, - - // collect input before running - params: [ - { name: 'note', label: 'Approval note', type: 'textarea' }, - ], - - // only show the button when the record is still pending - visible: 'record.status == "pending"', - - // what it does — a sandboxed script body - type: 'script', - body: { - language: 'js', - source: ` - await ctx.data.update('invoice', input.id, { - status: 'approved', - approved_by: ctx.user.id, - approved_at: now(), - approval_note: input.note, - }); - `, - }, -}); -``` - -Nach der erneuten Kompilierung durch `os dev`: - -- `POST /api/v1/actions/invoice/approve_invoice` funktioniert -- Die Invoice-Datensatzseite in der Console zeigt eine **Approve Invoice**-Schaltfläche -- Ein Flow kann `{ type: 'action', action: 'approve_invoice', inputs: { note: '…' } }` enthalten -- Der AI-Assistent kann `action_approve_invoice` aufrufen, wenn seine Skills es erlauben - -## Action-Typen - -Das Feld `type` entscheidet, was die Action tut: - -| `type` | Was ausgeführt wird | Verwendung für | -|---|---|---| -| `script` | Ein `body` — ein L1-Formelausdruck oder Sandbox-L2-JavaScript | Die meisten Fälle — serverseitige Logik, auditierbar + AI-aufrufbar | -| `api` | Ein HTTP-Aufruf an einen `target`-Endpunkt (`method`, `bodyExtra`) | Wiederverwendung von Daten-API- oder Plattform-Endpunkten | -| `flow` | Führt den in `target` benannten Flow aus | Mehrstufige Geschäftsprozesse | -| `url` | Navigiert zur `target`-URL | Deep Links, Weiterleitungs-Actions | -| `modal` | Öffnet die in `target` benannte Seite/das Modal | Benutzerdefinierte Dialoge | -| `form` | Öffnet die in `target` benannte FormView | Geführte Dateneingabe | - -```ts -// api type — reuse a data-API endpoint -Action.create({ - name: 'archive_order', - objectName: 'order', - label: 'Archive', - locations: ['list_item'], - type: 'api', - method: 'PATCH', - target: '/api/v1/data/order/{id}', - bodyExtra: { archived: true }, -}); -``` - -Andere Typen als `script` erfordern ein `target`. Welchen Typ auch immer, die Action -ist auf jeder Oberfläche derselbe vollwertige Bürger. - -## Eine Action aufrufen - -### REST - -```bash -# the record id can go in the body, or in the path -curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice/inv_123 \ - -H 'Authorization: Bearer ' \ - -H 'Content-Type: application/json' \ - -d '{"note": "LGTM"}' -``` - -Params werden flach im Anfragetext gesendet. Die Datensatz-ID kann -entweder als abschließendes Pfadsegment (`.../approve_invoice/:recordId`) oder im -Anfragetext angegeben werden. Die Antwort ist der Rückgabewert Ihres Skript-Bodys (oder das Aufrufergebnis -bei Actions vom Typ `api`). - -### Console - -Standardmäßig zeigt die Console Actions als Schaltflächen auf der Datensatzdetailseite an, -gefiltert durch das `visible`-Prädikat der Action. Überschreiben Sie die Platzierung -in Ihrer View-Konfiguration: - -```ts -defineView({ - name: 'invoice_detail', - object: 'invoice', - actions: ['approve_invoice', 'reject_invoice', 'send_to_customer'], -}); -``` - -### Aus einem Flow - -```ts -{ - type: 'action', - action: 'approve_invoice', - inputs: { note: 'Auto-approved by SLA flow' }, - record: '{!trigger.record.id}', -} -``` - -### Von einem AI-Agent - -Wenn `approve_invoice` in einem Skill enthalten ist, das der Agent besitzt, kann das LLM es -aufrufen. Die Eingaben stammen aus der Konversation; die Berechtigungen werden so durchgesetzt, als ob -der Benutzer sie direkt aufgerufen hätte. - -> *"Approve invoice INV-2042 with note 'verified by phone.'"* - -## Berechtigungen - -Actions laufen mit den Berechtigungen des aufrufenden Benutzers. Die Plattform prüft: - -1. **Objektberechtigungen** — die [Berechtigungssätze](/docs/configure/permissions/permission-sets) des Benutzers - müssen den objektbezogenen Zugriff gewähren, den die Action benötigt (z. B. Aktualisieren). -2. **Feldberechtigungen** — für jedes Feld, das die Action schreibt, muss der Benutzer - über Schreibzugriff verfügen (FLS). -3. **UI-Gating** — die Prädikate `visible` und `disabled` (CEL, - ausgewertet gegen `record`, `os.user` und Params) steuern, ob die - Schaltfläche in der Console gerendert oder ausgegraut wird. - -Fehlgeschlagene Berechtigungsprüfungen geben `403` mit einem `PERMISSION_DENIED`-Fehler zurück. - -## Integrierte Actions - -Jedes Objekt erhält diese kostenlos: - -| Action | Was sie tut | -|---|---| -| `create` | Einen Datensatz einfügen | -| `update` | Einen Datensatz aktualisieren | -| `delete` | Einen Datensatz löschen (oder soft-löschen) | -| `restore` | Ein Soft-Delete rückgängig machen | -| `clone` | Einen Datensatz tief kopieren | -| `share` | Direkt mit einem Benutzer / einer Rolle teilen | - -Deklarieren Sie diese nicht erneut — sie folgen den [Lebenszyklus- & Capability-Flags](/docs/build/data) des Objekts. - -## Auditing - -Plattformereignisse landen in `sys_audit_log`, einem unveränderlichen Verlauf mit Feldern -wie unter anderem: - -- `user_id` — der auslösende Benutzer -- `action` — der Name der Action -- `object_name` und `record_id` — was berührt wurde -- `old_value` / `new_value` — die Änderung -- `ip_address` / `user_agent` — Ursprung der Anfrage -- `created_at` — wann es geschah - -Dies ist Ihre erste Anlaufstelle für Fragen wie *"Wer hat die Schaltfläche gedrückt?"*. - -## Actions mit dem AI Builder generieren - -> *"Create an action `escalate_ticket` on `support_ticket` that sets -> priority to urgent and assigns it to the on-call engineer."* - -Der [AI Builder](/docs/build/ai-builder) generiert die Action-Metadaten und -stellt die Änderung zur Genehmigung in die Warteschlange. Nach der Genehmigung ist die Action aufrufbar über -REST, Console, Flows und — rekursiv — die AI selbst. - -## Wie es weitergeht - -- [Flows](/docs/build/automation/flows) — mehrere Actions zu Geschäftslogik zusammensetzen -- [Agents](/docs/build/agents) — Actions als AI-Tools bereitstellen -- [API Access](/docs/configure/api-access) — Actions aus externen Systemen aufrufen -- [Permissions](/docs/configure/permissions) — steuern, wer was aufrufen darf diff --git a/content/docs/build/interface/actions.es.mdx b/content/docs/build/interface/actions.es.mdx deleted file mode 100644 index 636a1d4..0000000 --- a/content/docs/build/interface/actions.es.mdx +++ /dev/null @@ -1,206 +0,0 @@ ---- -title: Acciones -description: Operaciones con nombre que la plataforma expone como endpoints REST, botones de Console, pasos de flujo y herramientas de IA, a partir de una sola declaración. -translation: - source_sha: d4f9d1d0443d08d33b2ea0aa548c7b8db1541a8b693a65d481af6cafba577018 - guide_rev: 1 - mode: auto ---- - -Una **Acción** es una operación con nombre sobre un objeto. Decláralo una vez y -aparece como: - -- un **endpoint REST** en `/api/v1/actions//` -- un **botón** en el detalle de registro de Console -- un **paso de flujo** (`type: 'action'`) para automatización -- una **herramienta de IA** (`action_`) para Agents y el AI Builder - -No te repites en cuatro superficies. Una declaración; cuatro -formas de invocarla. - -## Declarar una acción - -```ts -// src/actions/approve_invoice.action.ts -import { Action } from '@objectstack/spec'; - -export const approveInvoice = Action.create({ - name: 'approve_invoice', // lowercase snake_case (machine id) - label: 'Approve Invoice', - objectName: 'invoice', // attaches to the invoice object - icon: 'check', - variant: 'primary', - locations: ['record_header'], // where the button shows - confirmText: 'Approve this invoice?', - successMessage: 'Invoice approved', - refreshAfter: true, - - // collect input before running - params: [ - { name: 'note', label: 'Approval note', type: 'textarea' }, - ], - - // only show the button when the record is still pending - visible: 'record.status == "pending"', - - // what it does — a sandboxed script body - type: 'script', - body: { - language: 'js', - source: ` - await ctx.data.update('invoice', input.id, { - status: 'approved', - approved_by: ctx.user.id, - approved_at: now(), - approval_note: input.note, - }); - `, - }, -}); -``` - -Después de que `os dev` recompile: - -- `POST /api/v1/actions/invoice/approve_invoice` funciona -- La página de registro de Invoice en Console muestra un botón **Approve Invoice** -- Un flujo puede incluir `{ type: 'action', action: 'approve_invoice', inputs: { note: '…' } }` -- El asistente de IA puede llamar a `action_approve_invoice` si sus skills lo permiten - -## Tipos de acción - -El campo `type` decide qué hace la acción: - -| `type` | Qué se ejecuta | Úsalo para | -|---|---|---| -| `script` | Un `body` — una expresión de fórmula L1 o JavaScript L2 en sandbox | La mayoría de los casos — lógica del lado del servidor, auditable y llamable por IA | -| `api` | Una llamada HTTP a un endpoint `target` (`method`, `bodyExtra`) | Reutilizar endpoints de la data-API o de la plataforma | -| `flow` | Ejecuta el flujo indicado en `target` | Procesos de negocio de varios pasos | -| `url` | Navega a la URL de `target` | Enlaces directos, acciones de tipo redirección | -| `modal` | Abre la página/modal indicada en `target` | Diálogos personalizados | -| `form` | Abre la FormView indicada en `target` | Entrada de datos guiada | - -```ts -// api type — reuse a data-API endpoint -Action.create({ - name: 'archive_order', - objectName: 'order', - label: 'Archive', - locations: ['list_item'], - type: 'api', - method: 'PATCH', - target: '/api/v1/data/order/{id}', - bodyExtra: { archived: true }, -}); -``` - -Los tipos distintos de `script` requieren un `target`. Sea cual sea el tipo, la acción -es el mismo ciudadano de primera clase en todas las superficies. - -## Llamar a una acción - -### REST - -```bash -# the record id can go in the body, or in the path -curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice/inv_123 \ - -H 'Authorization: Bearer ' \ - -H 'Content-Type: application/json' \ - -d '{"note": "LGTM"}' -``` - -Los parámetros se envían planos en el cuerpo de la solicitud. El id del registro puede suministrarse -como un segmento final de la ruta (`.../approve_invoice/:recordId`) o en -el cuerpo. La respuesta es el valor de retorno del cuerpo de tu script (o el resultado -de la llamada en el caso de las acciones de tipo `api`). - -### Console - -Por defecto, Console muestra las acciones como botones en la página de detalle del registro, -filtradas por el predicado `visible` de la acción. Anula la ubicación -en la configuración de tu vista: - -```ts -defineView({ - name: 'invoice_detail', - object: 'invoice', - actions: ['approve_invoice', 'reject_invoice', 'send_to_customer'], -}); -``` - -### Desde un flujo - -```ts -{ - type: 'action', - action: 'approve_invoice', - inputs: { note: 'Auto-approved by SLA flow' }, - record: '{!trigger.record.id}', -} -``` - -### Desde un AI Agent - -Si `approve_invoice` está en alguna skill que tenga el agente, el LLM puede llamarla. -Las entradas provienen de la conversación; los permisos se aplican como si -el usuario la hubiera invocado directamente. - -> *"Aprueba la factura INV-2042 con la nota 'verificado por teléfono'."* - -## Permisos - -Las acciones se ejecutan con los permisos del usuario que las invoca. La plataforma comprueba: - -1. **Permisos de objeto** — los [conjuntos de permisos](/docs/configure/permissions/permission-sets) - del usuario deben otorgar el acceso a nivel de objeto que necesita la acción (p. ej. actualizar). -2. **Permisos de campo** — para cualquier campo que la acción escriba, el usuario - debe tener acceso de escritura (FLS). -3. **Control de UI** — los predicados `visible` y `disabled` (CEL, - evaluados contra `record`, `os.user` y los parámetros) controlan si el - botón se renderiza o aparece atenuado en Console. - -Las comprobaciones de permisos fallidas devuelven `403` con un error `PERMISSION_DENIED`. - -## Acciones integradas - -Cada objeto obtiene estas de forma gratuita: - -| Acción | Qué hace | -|---|---| -| `create` | Inserta un registro | -| `update` | Actualiza un registro | -| `delete` | Elimina (o elimina de forma reversible) un registro | -| `restore` | Deshace una eliminación reversible | -| `clone` | Copia en profundidad un registro | -| `share` | Comparte directamente con un usuario / rol | - -No vuelvas a declarar estas — siguen los [indicadores de ciclo de vida y capacidad](/docs/build/data) del objeto. - -## Auditoría - -Los eventos de la plataforma se registran en `sys_audit_log`, un rastro inmutable con campos -que incluyen: - -- `user_id` — el usuario originario -- `action` — el nombre de la acción -- `object_name` y `record_id` — qué se modificó -- `old_value` / `new_value` — el cambio -- `ip_address` / `user_agent` — origen de la solicitud -- `created_at` — cuándo ocurrió - -Este es tu primer recurso para preguntas del tipo *"¿quién pulsó el botón?"*. - -## Generar acciones con el AI Builder - -> *"Crea una acción `escalate_ticket` en `support_ticket` que establezca -> la prioridad en urgente y la asigne al ingeniero de guardia."* - -El [AI Builder](/docs/build/ai-builder) genera los metadatos de la acción y -pone en cola el cambio para su aprobación. Tras la aprobación, la acción puede invocarse desde -REST, Console, flujos y — recursivamente — la propia IA. - -## Adónde ir después - -- [Flows](/docs/build/automation/flows) — compón varias acciones en lógica de negocio -- [Agents](/docs/build/agents) — expón acciones como herramientas de IA -- [API Access](/docs/configure/api-access) — llama a acciones desde sistemas externos -- [Permissions](/docs/configure/permissions) — controla quién puede llamar a qué diff --git a/content/docs/build/interface/actions.fr.mdx b/content/docs/build/interface/actions.fr.mdx deleted file mode 100644 index e6ac9db..0000000 --- a/content/docs/build/interface/actions.fr.mdx +++ /dev/null @@ -1,206 +0,0 @@ ---- -title: Actions -description: Opérations nommées que la plateforme expose en tant qu'endpoints REST, boutons Console, étapes de flow et outils IA — à partir d'une seule déclaration. -translation: - source_sha: d4f9d1d0443d08d33b2ea0aa548c7b8db1541a8b693a65d481af6cafba577018 - guide_rev: 1 - mode: auto ---- - -Une **Action** est une opération nommée sur un objet. Déclarez-la une seule fois et -elle apparaît comme : - -- un **endpoint REST** à `/api/v1/actions//` -- un **bouton** dans le détail d'enregistrement de Console -- une **étape de flow** (`type: 'action'`) pour l'automatisation -- un **outil IA** (`action_`) pour les Agents et l'AI Builder - -Vous ne vous répétez pas sur quatre surfaces. Une seule déclaration ; quatre -façons de l'appeler. - -## Déclarer une action - -```ts -// src/actions/approve_invoice.action.ts -import { Action } from '@objectstack/spec'; - -export const approveInvoice = Action.create({ - name: 'approve_invoice', // lowercase snake_case (machine id) - label: 'Approve Invoice', - objectName: 'invoice', // attaches to the invoice object - icon: 'check', - variant: 'primary', - locations: ['record_header'], // where the button shows - confirmText: 'Approve this invoice?', - successMessage: 'Invoice approved', - refreshAfter: true, - - // collect input before running - params: [ - { name: 'note', label: 'Approval note', type: 'textarea' }, - ], - - // only show the button when the record is still pending - visible: 'record.status == "pending"', - - // what it does — a sandboxed script body - type: 'script', - body: { - language: 'js', - source: ` - await ctx.data.update('invoice', input.id, { - status: 'approved', - approved_by: ctx.user.id, - approved_at: now(), - approval_note: input.note, - }); - `, - }, -}); -``` - -Après la recompilation par `os dev` : - -- `POST /api/v1/actions/invoice/approve_invoice` fonctionne -- La page d'enregistrement Invoice dans Console affiche un bouton **Approve Invoice** -- Un flow peut inclure `{ type: 'action', action: 'approve_invoice', inputs: { note: '…' } }` -- L'assistant IA peut appeler `action_approve_invoice` si ses skills le permettent - -## Types d'action - -Le champ `type` décide de ce que fait l'action : - -| `type` | Ce qui s'exécute | À utiliser pour | -|---|---|---| -| `script` | Un `body` — une expression de formule L1 ou du JavaScript L2 en sandbox | La plupart des cas — logique côté serveur, auditable + appelable par l'IA | -| `api` | Un appel HTTP vers un endpoint `target` (`method`, `bodyExtra`) | Réutiliser des endpoints data-API ou de la plateforme | -| `flow` | Exécute le flow nommé dans `target` | Processus métier multi-étapes | -| `url` | Navigue vers l'URL `target` | Liens profonds, actions de type redirection | -| `modal` | Ouvre la page/modale nommée dans `target` | Boîtes de dialogue personnalisées | -| `form` | Ouvre la FormView nommée dans `target` | Saisie de données guidée | - -```ts -// api type — reuse a data-API endpoint -Action.create({ - name: 'archive_order', - objectName: 'order', - label: 'Archive', - locations: ['list_item'], - type: 'api', - method: 'PATCH', - target: '/api/v1/data/order/{id}', - bodyExtra: { archived: true }, -}); -``` - -Les types autres que `script` nécessitent un `target`. Quel que soit le type, l'action -est le même citoyen de première classe sur chaque surface. - -## Appeler une action - -### REST - -```bash -# the record id can go in the body, or in the path -curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice/inv_123 \ - -H 'Authorization: Bearer ' \ - -H 'Content-Type: application/json' \ - -d '{"note": "LGTM"}' -``` - -Les params sont envoyés à plat dans le corps de la requête. L'identifiant de l'enregistrement peut être fourni -soit comme segment de chemin final (`.../approve_invoice/:recordId`), soit dans -le corps. La réponse est la valeur de retour du corps de votre script (ou le résultat de l'appel -pour les actions de type `api`). - -### Console - -Par défaut, Console affiche les actions sous forme de boutons sur la page de détail de l'enregistrement, -filtrés par le prédicat `visible` de l'action. Remplacez le placement -dans la configuration de votre vue : - -```ts -defineView({ - name: 'invoice_detail', - object: 'invoice', - actions: ['approve_invoice', 'reject_invoice', 'send_to_customer'], -}); -``` - -### Depuis un flow - -```ts -{ - type: 'action', - action: 'approve_invoice', - inputs: { note: 'Auto-approved by SLA flow' }, - record: '{!trigger.record.id}', -} -``` - -### Depuis un Agent IA - -Si `approve_invoice` se trouve dans l'un des skills dont dispose l'agent, le LLM peut l'appeler. -Les entrées proviennent de la conversation ; les permissions sont appliquées comme si -l'utilisateur l'avait invoquée directement. - -> *"Approve invoice INV-2042 with note 'verified by phone.'"* - -## Permissions - -Les actions s'exécutent avec les permissions de l'utilisateur appelant. La plateforme vérifie : - -1. **Permissions d'objet** — les [permission sets](/docs/configure/permissions/permission-sets) de l'utilisateur - doivent accorder l'accès au niveau de l'objet dont l'action a besoin (par exemple update). -2. **Permissions de champ** — pour chaque champ que l'action écrit, l'utilisateur - doit avoir un accès en écriture (FLS). -3. **Contrôle UI** — les prédicats `visible` et `disabled` (CEL, - évalués par rapport à `record`, `os.user` et aux params) déterminent si le - bouton s'affiche ou est grisé dans Console. - -Les vérifications de permission échouées renvoient un `403` avec une erreur `PERMISSION_DENIED`. - -## Actions intégrées - -Chaque objet dispose gratuitement de celles-ci : - -| Action | Ce qu'elle fait | -|---|---| -| `create` | Insère un enregistrement | -| `update` | Met à jour un enregistrement | -| `delete` | Supprime (ou supprime en douceur) un enregistrement | -| `restore` | Annule une suppression douce | -| `clone` | Copie en profondeur un enregistrement | -| `share` | Partage direct avec un utilisateur / rôle | - -Ne redéclarez pas celles-ci — elles suivent les [drapeaux de cycle de vie et de capacité](/docs/build/data) de l'objet. - -## Audit - -Les événements de la plateforme aboutissent dans `sys_audit_log`, une piste immuable avec des champs -notamment : - -- `user_id` — l'utilisateur à l'origine de l'action -- `action` — le nom de l'action -- `object_name` et `record_id` — ce qui a été touché -- `old_value` / `new_value` — le changement -- `ip_address` / `user_agent` — l'origine de la requête -- `created_at` — quand cela s'est produit - -C'est votre premier point d'arrêt pour les questions du type *"qui a appuyé sur le bouton ?"*. - -## Générer des actions avec l'AI Builder - -> *"Create an action `escalate_ticket` on `support_ticket` that sets -> priority to urgent and assigns it to the on-call engineer."* - -L'[AI Builder](/docs/build/ai-builder) génère les métadonnées de l'action et -met le changement en file d'attente pour approbation. Après approbation, l'action est appelable depuis -REST, Console, les flows et — de manière récursive — l'IA elle-même. - -## Où aller ensuite - -- [Flows](/docs/build/automation/flows) — composez plusieurs actions en logique métier -- [Agents](/docs/build/agents) — exposez les actions en tant qu'outils IA -- [API Access](/docs/configure/api-access) — appelez les actions depuis des systèmes externes -- [Permissions](/docs/configure/permissions) — contrôlez qui peut appeler quoi diff --git a/content/docs/build/interface/actions.ja.mdx b/content/docs/build/interface/actions.ja.mdx deleted file mode 100644 index 9f35fef..0000000 --- a/content/docs/build/interface/actions.ja.mdx +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: アクション -description: プラットフォームが REST エンドポイント、Console のボタン、フローのステップ、AI ツールとして公開する名前付きの操作 — 1 つの宣言から。 -translation: - source_sha: d4f9d1d0443d08d33b2ea0aa548c7b8db1541a8b693a65d481af6cafba577018 - guide_rev: 1 - mode: auto ---- - -**アクション**とは、オブジェクトに対する名前付きの操作です。一度宣言すれば、 -次のように表れます。 - -- `/api/v1/actions//` の **REST エンドポイント** -- Console のレコード詳細にある**ボタン** -- 自動化のための**フローステップ**(`type: 'action'`) -- Agents と AI Builder のための **AI ツール**(`action_`) - -4 つのサーフェスにわたって同じことを繰り返す必要はありません。宣言は 1 つ、 -呼び出し方は 4 通りです。 - -## アクションを宣言する - -```ts -// src/actions/approve_invoice.action.ts -import { Action } from '@objectstack/spec'; - -export const approveInvoice = Action.create({ - name: 'approve_invoice', // lowercase snake_case (machine id) - label: 'Approve Invoice', - objectName: 'invoice', // attaches to the invoice object - icon: 'check', - variant: 'primary', - locations: ['record_header'], // where the button shows - confirmText: 'Approve this invoice?', - successMessage: 'Invoice approved', - refreshAfter: true, - - // collect input before running - params: [ - { name: 'note', label: 'Approval note', type: 'textarea' }, - ], - - // only show the button when the record is still pending - visible: 'record.status == "pending"', - - // what it does — a sandboxed script body - type: 'script', - body: { - language: 'js', - source: ` - await ctx.data.update('invoice', input.id, { - status: 'approved', - approved_by: ctx.user.id, - approved_at: now(), - approval_note: input.note, - }); - `, - }, -}); -``` - -`os dev` による再コンパイル後: - -- `POST /api/v1/actions/invoice/approve_invoice` が機能する -- Console の Invoice レコードページに **Approve Invoice** ボタンが表示される -- フローに `{ type: 'action', action: 'approve_invoice', inputs: { note: '…' } }` を含められる -- AI アシスタントは、スキルが許可していれば `action_approve_invoice` を呼び出せる - -## アクションの種類 - -`type` フィールドがアクションの動作を決定します。 - -| `type` | 実行される内容 | 用途 | -|---|---|---| -| `script` | `body` — L1 の formula 式、またはサンドボックス化された L2 の JavaScript | ほとんどのケース — サーバーサイドのロジック、監査可能かつ AI から呼び出し可能 | -| `api` | `target` エンドポイントへの HTTP 呼び出し(`method`、`bodyExtra`) | data-API またはプラットフォームのエンドポイントの再利用 | -| `flow` | `target` で指定した名前のフローを実行 | 複数ステップのビジネスプロセス | -| `url` | `target` の URL へ遷移 | ディープリンク、リダイレクト形式のアクション | -| `modal` | `target` で指定した名前のページ/モーダルを開く | カスタムダイアログ | -| `form` | `target` で指定した名前の FormView を開く | ガイド付きのデータ入力 | - -```ts -// api type — reuse a data-API endpoint -Action.create({ - name: 'archive_order', - objectName: 'order', - label: 'Archive', - locations: ['list_item'], - type: 'api', - method: 'PATCH', - target: '/api/v1/data/order/{id}', - bodyExtra: { archived: true }, -}); -``` - -`script` 以外の種類には `target` が必要です。どの種類であっても、アクションは -すべてのサーフェスで同じファーストクラスの存在です。 - -## アクションを呼び出す - -### REST - -```bash -# the record id can go in the body, or in the path -curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice/inv_123 \ - -H 'Authorization: Bearer ' \ - -H 'Content-Type: application/json' \ - -d '{"note": "LGTM"}' -``` - -パラメータはリクエストボディにフラットな形で送信されます。レコード id は、 -末尾のパスセグメント(`.../approve_invoice/:recordId`)として、またはボディで -渡せます。レスポンスはスクリプトボディの戻り値(または `api` 種類のアクションでは -呼び出し結果)です。 - -### Console - -デフォルトでは、Console はアクションをレコード詳細ページのボタンとして表示し、 -アクションの `visible` 述語でフィルタリングします。配置を上書きするには、 -ビュー設定で指定します。 - -```ts -defineView({ - name: 'invoice_detail', - object: 'invoice', - actions: ['approve_invoice', 'reject_invoice', 'send_to_customer'], -}); -``` - -### フローから - -```ts -{ - type: 'action', - action: 'approve_invoice', - inputs: { note: 'Auto-approved by SLA flow' }, - record: '{!trigger.record.id}', -} -``` - -### AI Agent から - -`approve_invoice` がエージェントの持ついずれかのスキルに含まれていれば、LLM は -それを呼び出せます。入力は会話から取得され、権限はユーザーが直接呼び出した場合と -同じように適用されます。 - -> *「INV-2042 の請求書を、メモ『電話で確認済み』を付けて承認して。」* - -## 権限 - -アクションは呼び出したユーザーの権限で実行されます。プラットフォームは次を -チェックします。 - -1. **オブジェクト権限** — ユーザーの[権限セット](/docs/configure/permissions/permission-sets)が、 - アクションに必要なオブジェクトレベルのアクセス(例: update)を付与している必要があります。 -2. **フィールド権限** — アクションが書き込む任意のフィールドについて、ユーザーは - 書き込みアクセス(FLS)を持っている必要があります。 -3. **UI ゲーティング** — `visible` と `disabled` の述語(CEL、`record`、`os.user`、 - およびパラメータに対して評価される)が、Console でボタンを描画するか、 - グレーアウトするかを制御します。 - -権限チェックに失敗すると、`PERMISSION_DENIED` エラーとともに `403` を返します。 - -## 組み込みアクション - -すべてのオブジェクトには、これらが標準で備わっています。 - -| アクション | 動作 | -|---|---| -| `create` | レコードを挿入 | -| `update` | レコードを更新 | -| `delete` | レコードを削除(またはソフト削除) | -| `restore` | ソフト削除を取り消し | -| `clone` | レコードをディープコピー | -| `share` | ユーザー / ロールと直接共有 | - -これらを再宣言しないでください — これらはオブジェクトの[ライフサイクルと機能フラグ](/docs/build/data)に従います。 - -## 監査 - -プラットフォームのイベントは、次のフィールドを含む不変の証跡である -`sys_audit_log` に記録されます。 - -- `user_id` — 発生元のユーザー -- `action` — アクション名 -- `object_name` と `record_id` — 操作対象 -- `old_value` / `new_value` — 変更内容 -- `ip_address` / `user_agent` — リクエストの発信元 -- `created_at` — 発生日時 - -これは*「誰がボタンを押したのか?」*という疑問の最初の手がかりです。 - -## AI Builder でアクションを生成する - -> *「`support_ticket` に `escalate_ticket` というアクションを作成し、優先度を -> urgent に設定して、オンコールのエンジニアに割り当てて。」* - -[AI Builder](/docs/build/ai-builder) はアクションのメタデータを生成し、その変更を -承認待ちのキューに入れます。承認後、アクションは REST、Console、フロー、そして -再帰的に AI 自身からも呼び出せるようになります。 - -## 次に読むもの - -- [フロー](/docs/build/automation/flows) — 複数のアクションを組み合わせてビジネスロジックを構成する -- [Agents](/docs/build/agents) — アクションを AI ツールとして公開する -- [API アクセス](/docs/configure/api-access) — 外部システムからアクションを呼び出す -- [権限](/docs/configure/permissions) — 誰が何を呼び出せるかを制御する diff --git a/content/docs/build/interface/actions.ko.mdx b/content/docs/build/interface/actions.ko.mdx deleted file mode 100644 index b271b5d..0000000 --- a/content/docs/build/interface/actions.ko.mdx +++ /dev/null @@ -1,203 +0,0 @@ ---- -title: 액션(Actions) -description: 플랫폼이 REST 엔드포인트, Console 버튼, 플로우 단계, AI 도구로 노출하는 명명된 작업 — 단 하나의 선언으로 제공됩니다. -translation: - source_sha: d4f9d1d0443d08d33b2ea0aa548c7b8db1541a8b693a65d481af6cafba577018 - guide_rev: 1 - mode: auto ---- - -**액션(Action)**은 객체에 대한 명명된 작업입니다. 한 번만 선언하면 -다음과 같이 나타납니다: - -- `/api/v1/actions//` 위치의 **REST 엔드포인트** -- Console 레코드 상세 화면의 **버튼** -- 자동화를 위한 **플로우 단계**(`type: 'action'`) -- Agents와 AI Builder를 위한 **AI 도구**(`action_`) - -네 가지 표면에 걸쳐 같은 작업을 반복하지 않습니다. 하나의 선언으로 네 가지 -호출 방법을 제공합니다. - -## 액션 선언하기 - -```ts -// src/actions/approve_invoice.action.ts -import { Action } from '@objectstack/spec'; - -export const approveInvoice = Action.create({ - name: 'approve_invoice', // lowercase snake_case (machine id) - label: 'Approve Invoice', - objectName: 'invoice', // attaches to the invoice object - icon: 'check', - variant: 'primary', - locations: ['record_header'], // where the button shows - confirmText: 'Approve this invoice?', - successMessage: 'Invoice approved', - refreshAfter: true, - - // collect input before running - params: [ - { name: 'note', label: 'Approval note', type: 'textarea' }, - ], - - // only show the button when the record is still pending - visible: 'record.status == "pending"', - - // what it does — a sandboxed script body - type: 'script', - body: { - language: 'js', - source: ` - await ctx.data.update('invoice', input.id, { - status: 'approved', - approved_by: ctx.user.id, - approved_at: now(), - approval_note: input.note, - }); - `, - }, -}); -``` - -`os dev`가 재컴파일한 후: - -- `POST /api/v1/actions/invoice/approve_invoice`가 동작합니다 -- Console의 Invoice 레코드 페이지에 **Approve Invoice** 버튼이 표시됩니다 -- 플로우에 `{ type: 'action', action: 'approve_invoice', inputs: { note: '…' } }`를 포함할 수 있습니다 -- 스킬이 허용하는 경우 AI 어시스턴트가 `action_approve_invoice`를 호출할 수 있습니다 - -## 액션 유형 - -`type` 필드가 액션이 수행할 작업을 결정합니다: - -| `type` | 실행되는 것 | 사용 목적 | -|---|---|---| -| `script` | `body` — L1 formula 표현식 또는 샌드박스화된 L2 JavaScript | 대부분의 경우 — 서버 측 로직, 감사 가능 + AI 호출 가능 | -| `api` | `target` 엔드포인트로의 HTTP 호출(`method`, `bodyExtra`) | 데이터 API 또는 플랫폼 엔드포인트 재사용 | -| `flow` | `target`에 명명된 플로우를 실행 | 다단계 비즈니스 프로세스 | -| `url` | `target` URL로 이동 | 딥 링크, 리디렉션 방식의 액션 | -| `modal` | `target`에 명명된 페이지/모달을 열기 | 사용자 정의 대화 상자 | -| `form` | `target`에 명명된 FormView를 열기 | 가이드 방식의 데이터 입력 | - -```ts -// api type — reuse a data-API endpoint -Action.create({ - name: 'archive_order', - objectName: 'order', - label: 'Archive', - locations: ['list_item'], - type: 'api', - method: 'PATCH', - target: '/api/v1/data/order/{id}', - bodyExtra: { archived: true }, -}); -``` - -`script`이 아닌 유형은 `target`이 필요합니다. 어떤 유형이든 액션은 모든 -표면에서 동일한 일급 시민입니다. - -## 액션 호출하기 - -### REST - -```bash -# the record id can go in the body, or in the path -curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice/inv_123 \ - -H 'Authorization: Bearer ' \ - -H 'Content-Type: application/json' \ - -d '{"note": "LGTM"}' -``` - -파라미터는 요청 본문에 평면 형태로 전송됩니다. 레코드 id는 후행 경로 세그먼트 -(`.../approve_invoice/:recordId`)로 제공하거나 본문에 포함할 수 있습니다. 응답은 -스크립트 본문의 반환 값(또는 `api` 유형 액션의 경우 호출 결과)입니다. - -### Console - -기본적으로 Console은 액션의 `visible` 조건으로 필터링하여 레코드 상세 페이지에 -버튼으로 액션을 표시합니다. 뷰 설정에서 배치를 재정의할 수 있습니다: - -```ts -defineView({ - name: 'invoice_detail', - object: 'invoice', - actions: ['approve_invoice', 'reject_invoice', 'send_to_customer'], -}); -``` - -### 플로우에서 - -```ts -{ - type: 'action', - action: 'approve_invoice', - inputs: { note: 'Auto-approved by SLA flow' }, - record: '{!trigger.record.id}', -} -``` - -### AI Agent에서 - -`approve_invoice`가 에이전트가 보유한 스킬에 포함되어 있으면 LLM이 이를 -호출할 수 있습니다. 입력은 대화에서 가져오며, 권한은 사용자가 직접 호출한 -것처럼 적용됩니다. - -> *"인보이스 INV-2042를 '전화로 확인됨'이라는 메모와 함께 승인해줘."* - -## 권한 - -액션은 호출하는 사용자의 권한으로 실행됩니다. 플랫폼은 다음을 확인합니다: - -1. **객체 권한** — 사용자의 [권한 세트](/docs/configure/permissions/permission-sets)가 - 액션에 필요한 객체 수준 접근 권한(예: 업데이트)을 부여해야 합니다. -2. **필드 권한** — 액션이 쓰는 모든 필드에 대해 사용자가 쓰기 접근 권한(FLS)을 - 가지고 있어야 합니다. -3. **UI 게이팅** — `visible` 및 `disabled` 조건(CEL, `record`, `os.user`, - 파라미터에 대해 평가됨)이 Console에서 버튼이 렌더링될지 또는 회색 처리될지를 - 제어합니다. - -권한 확인에 실패하면 `PERMISSION_DENIED` 오류와 함께 `403`을 반환합니다. - -## 내장 액션 - -모든 객체는 다음을 기본으로 제공받습니다: - -| 액션 | 수행하는 작업 | -|---|---| -| `create` | 레코드 삽입 | -| `update` | 레코드 업데이트 | -| `delete` | 레코드 삭제(또는 소프트 삭제) | -| `restore` | 소프트 삭제 취소 | -| `clone` | 레코드 깊은 복사 | -| `share` | 사용자 / 역할과 직접 공유 | - -이러한 액션은 다시 선언하지 마세요 — 객체의 [라이프사이클 및 기능 플래그](/docs/build/data)를 따릅니다. - -## 감사(Auditing) - -플랫폼 이벤트는 다음 필드를 포함하는 불변 기록인 `sys_audit_log`에 기록됩니다: - -- `user_id` — 작업을 시작한 사용자 -- `action` — 액션 이름 -- `object_name` 및 `record_id` — 변경된 대상 -- `old_value` / `new_value` — 변경 내용 -- `ip_address` / `user_agent` — 요청 출처 -- `created_at` — 발생 시점 - -이는 *"누가 버튼을 눌렀는가?"*라는 질문에 가장 먼저 확인할 곳입니다. - -## AI Builder로 액션 생성하기 - -> *"`support_ticket`에 우선순위를 긴급으로 설정하고 온콜 엔지니어에게 -> 할당하는 액션 `escalate_ticket`을 만들어줘."* - -[AI Builder](/docs/build/ai-builder)는 액션 메타데이터를 생성하고 변경 사항을 -승인 대기열에 넣습니다. 승인 후에는 REST, Console, 플로우, 그리고 — 재귀적으로 — -AI 자체에서 액션을 호출할 수 있습니다. - -## 다음으로 갈 곳 - -- [플로우](/docs/build/automation/flows) — 여러 액션을 비즈니스 로직으로 구성하기 -- [Agents](/docs/build/agents) — 액션을 AI 도구로 노출하기 -- [API 접근](/docs/configure/api-access) — 외부 시스템에서 액션 호출하기 -- [권한](/docs/configure/permissions) — 누가 무엇을 호출할 수 있는지 제어하기 diff --git a/content/docs/build/interface/actions.mdx b/content/docs/build/interface/actions.mdx index a1dbd61..f4bffc2 100644 --- a/content/docs/build/interface/actions.mdx +++ b/content/docs/build/interface/actions.mdx @@ -1,7 +1,7 @@ --- title: Actions seoTitle: "Actions: REST Endpoints, Buttons and AI Tools" -description: Named operations the platform exposes as REST endpoints, buttons, flow steps, and AI tools — from one declaration. +description: Named operations the platform exposes as REST endpoints, buttons and AI tools — from one declaration. --- An **Action** is a named operation on an object. Declare it once and @@ -9,17 +9,17 @@ it appears as: - a **REST endpoint** at `/api/v1/actions//` - a **button** on the record detail page -- a **flow step** (`type: 'action'`) for automation - an **AI tool** (`action_`) for Agents and the AI Builder -You don't repeat yourself across four surfaces. One declaration; four -ways to call it. +You don't repeat yourself across three surfaces. One declaration; three +ways to call it. An action can also start a [flow](/docs/build/automation/flows) +(`type: 'flow'`) when the work takes more than one step. ## Declare an action ```ts // src/actions/approve_invoice.action.ts -import { Action } from '@objectstack/spec'; +import { Action } from '@objectstack/spec/ui'; export const approveInvoice = Action.create({ name: 'approve_invoice', // lowercase snake_case (machine id) @@ -28,7 +28,7 @@ export const approveInvoice = Action.create({ icon: 'check', variant: 'primary', locations: ['record_header'], // where the button shows - confirmText: 'Approve this invoice?', + description: 'Approve this invoice?', // shown in the param dialog successMessage: 'Invoice approved', refreshAfter: true, @@ -60,7 +60,6 @@ After `os dev` recompiles: - `POST /api/v1/actions/invoice/approve_invoice` works - The Invoice record page shows an **Approve Invoice** button -- A flow can include `{ type: 'action', action: 'approve_invoice', inputs: { note: '…' } }` - The AI assistant can call `action_approve_invoice` if its skills allow ## Action types @@ -112,27 +111,38 @@ result for `api`-type actions). ### From the UI -By default, actions appear as buttons on the record detail page, -filtered by the action's `visible` predicate. Override placement -in your view config: +Where a button shows is declared on the action itself: `locations` +(`record_header`, `record_more`, `list_item`, `list_toolbar`, …) and +`order`. The `visible` predicate then decides, record by record, whether it +renders. A list view can also name actions for each row and for a selection +of rows: ```ts -defineView({ - name: 'invoice_detail', - object: 'invoice', - actions: ['approve_invoice', 'reject_invoice', 'send_to_customer'], -}); +// One list view; the container and `data` are omitted (see Views). +{ + type: 'grid', + columns: ['number', 'customer', 'amount', 'status'], + rowActions: ['approve_invoice', 'reject_invoice'], + bulkActions: ['send_to_customer'], +} ``` -### From a flow +### With a flow + +There is no flow node that calls an action. Go the other way: put the +multi-step logic in a flow, and start it from an action with `type: 'flow'` +and the flow's name in `target`: ```ts -{ - type: 'action', - action: 'approve_invoice', - inputs: { note: 'Auto-approved by SLA flow' }, - record: '{!trigger.record.id}', -} +// flow type — start a flow by name +Action.create({ + name: 'run_invoice_approval', + objectName: 'invoice', + label: 'Run approval', + locations: ['record_more'], + type: 'flow', + target: 'approve_invoice', +}); ``` ### From an AI Agent @@ -193,11 +203,11 @@ This is your first stop for *"who pushed the button?"* questions. The [AI Builder](/docs/build/ai-builder) generates the action metadata and queues the change for approval. After approval, the action is callable from -REST, the UI, flows, and — recursively — the AI itself. +REST, the UI, and — recursively — the AI itself. ## Where to go next -- [Flows](/docs/build/automation/flows) — compose multiple actions into business logic +- [Flows](/docs/build/automation/flows) — the multi-step logic a `type: 'flow'` action starts - [Agents](/docs/build/agents) — expose actions as AI tools - [API Access](/docs/configure/api-access) — call actions from external systems - [Permissions](/docs/configure/permissions) — gate who can call what diff --git a/content/docs/build/interface/actions.zh-Hans.mdx b/content/docs/build/interface/actions.zh-Hans.mdx deleted file mode 100644 index 3c98d10..0000000 --- a/content/docs/build/interface/actions.zh-Hans.mdx +++ /dev/null @@ -1,189 +0,0 @@ ---- -title: 操作 -description: 平台从单一声明出发,将命名操作同时暴露为 REST 端点、Console 按钮、流程步骤和 AI 工具。 -translation: - source_sha: d4f9d1d0443d08d33b2ea0aa548c7b8db1541a8b693a65d481af6cafba577018 - guide_rev: 1 - mode: auto ---- - -**Action** 是对象上的一个命名操作。只需声明一次,它就会以以下形式出现: - -- 一个位于 `/api/v1/actions//` 的 **REST 端点** -- Console 记录详情中的一个**按钮** -- 用于自动化的一个**流程步骤**(`type: 'action'`) -- 供 Agents 和 AI Builder 使用的一个 **AI 工具**(`action_`) - -你无需在四个界面上重复定义。一次声明,四种调用方式。 - -## 声明一个 action - -```ts -// src/actions/approve_invoice.action.ts -import { Action } from '@objectstack/spec'; - -export const approveInvoice = Action.create({ - name: 'approve_invoice', // lowercase snake_case (machine id) - label: 'Approve Invoice', - objectName: 'invoice', // attaches to the invoice object - icon: 'check', - variant: 'primary', - locations: ['record_header'], // where the button shows - confirmText: 'Approve this invoice?', - successMessage: 'Invoice approved', - refreshAfter: true, - - // collect input before running - params: [ - { name: 'note', label: 'Approval note', type: 'textarea' }, - ], - - // only show the button when the record is still pending - visible: 'record.status == "pending"', - - // what it does — a sandboxed script body - type: 'script', - body: { - language: 'js', - source: ` - await ctx.data.update('invoice', input.id, { - status: 'approved', - approved_by: ctx.user.id, - approved_at: now(), - approval_note: input.note, - }); - `, - }, -}); -``` - -在 `os dev` 重新编译之后: - -- `POST /api/v1/actions/invoice/approve_invoice` 可用 -- Console 中的 Invoice 记录页面会显示一个 **Approve Invoice** 按钮 -- 流程可以包含 `{ type: 'action', action: 'approve_invoice', inputs: { note: '…' } }` -- 如果 AI 助手的技能允许,它可以调用 `action_approve_invoice` - -## Action 类型 - -`type` 字段决定 action 执行什么: - -| `type` | 执行内容 | 适用场景 | -|---|---|---| -| `script` | 一个 `body` —— 一个 L1 formula 表达式或沙箱化的 L2 JavaScript | 大多数场景 —— 服务端逻辑,可审计且可被 AI 调用 | -| `api` | 对某个 `target` 端点发起的 HTTP 调用(`method`、`bodyExtra`) | 复用数据 API 或平台端点 | -| `flow` | 运行 `target` 中指定名称的流程 | 多步骤业务流程 | -| `url` | 跳转到 `target` URL | 深度链接、重定向式操作 | -| `modal` | 打开 `target` 中指定名称的页面/模态框 | 自定义对话框 | -| `form` | 打开 `target` 中指定名称的 FormView | 引导式数据录入 | - -```ts -// api type — reuse a data-API endpoint -Action.create({ - name: 'archive_order', - objectName: 'order', - label: 'Archive', - locations: ['list_item'], - type: 'api', - method: 'PATCH', - target: '/api/v1/data/order/{id}', - bodyExtra: { archived: true }, -}); -``` - -除 `script` 之外的类型都需要一个 `target`。无论哪种类型,该 action 在每个界面上都是同等的一级公民。 - -## 调用一个 action - -### REST - -```bash -# the record id can go in the body, or in the path -curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice/inv_123 \ - -H 'Authorization: Bearer ' \ - -H 'Content-Type: application/json' \ - -d '{"note": "LGTM"}' -``` - -参数以扁平形式放在请求体中发送。记录 id 既可以作为路径末尾的片段提供(`.../approve_invoice/:recordId`),也可以放在请求体中。响应是你的脚本 body 的返回值(对于 `api` 类型的 action 则是调用结果)。 - -### Console - -默认情况下,Console 会将 action 显示为记录详情页面上的按钮,并按 action 的 `visible` 谓词进行过滤。你可以在视图配置中覆盖其位置: - -```ts -defineView({ - name: 'invoice_detail', - object: 'invoice', - actions: ['approve_invoice', 'reject_invoice', 'send_to_customer'], -}); -``` - -### 来自流程 - -```ts -{ - type: 'action', - action: 'approve_invoice', - inputs: { note: 'Auto-approved by SLA flow' }, - record: '{!trigger.record.id}', -} -``` - -### 来自 AI Agent - -如果 `approve_invoice` 包含在该 agent 拥有的任意技能中,LLM 就可以调用它。输入来自对话内容;权限的执行方式与用户直接调用时完全一致。 - -> *"Approve invoice INV-2042 with note 'verified by phone.'"* - -## 权限 - -Action 以发起调用的用户的权限运行。平台会检查: - -1. **对象权限** —— 用户的[权限集](/docs/configure/permissions/permission-sets)必须授予该 action 所需的对象级访问权限(例如 update)。 -2. **字段权限** —— 对于该 action 写入的任何字段,用户必须拥有写入访问权限(FLS)。 -3. **UI 控制** —— `visible` 和 `disabled` 谓词(CEL,针对 `record`、`os.user` 和参数求值)控制按钮在 Console 中是渲染还是置灰。 - -未通过的权限检查会返回 `403`,并附带一个 `PERMISSION_DENIED` 错误。 - -## 内置 action - -每个对象都会免费获得以下 action: - -| Action | 作用 | -|---|---| -| `create` | 插入一条记录 | -| `update` | 更新一条记录 | -| `delete` | 删除(或软删除)一条记录 | -| `restore` | 撤销软删除 | -| `clone` | 深拷贝一条记录 | -| `share` | 直接与某个用户/角色共享 | - -不要重新声明这些 —— 它们遵循对象的[生命周期与能力标志](/docs/build/data)。 - -## 审计 - -平台事件会落入 `sys_audit_log`,这是一条不可变的轨迹,其字段包括: - -- `user_id` —— 发起操作的用户 -- `action` —— action 名称 -- `object_name` 和 `record_id` —— 被操作的对象 -- `old_value` / `new_value` —— 变更内容 -- `ip_address` / `user_agent` —— 请求来源 -- `created_at` —— 发生时间 - -当你需要回答*"是谁按下了这个按钮?"*之类的问题时,这里是首选的查证之处。 - -## 使用 AI Builder 生成 action - -> *"Create an action `escalate_ticket` on `support_ticket` that sets -> priority to urgent and assigns it to the on-call engineer."* - -[AI Builder](/docs/build/ai-builder) 会生成该 action 的元数据,并将变更排队等待审批。审批通过后,该 action 即可从 REST、Console、流程,以及 —— 递归地 —— AI 自身进行调用。 - -## 后续去向 - -- [Flows](/docs/build/automation/flows) —— 将多个 action 组合成业务逻辑 -- [Agents](/docs/build/agents) —— 将 action 暴露为 AI 工具 -- [API Access](/docs/configure/api-access) —— 从外部系统调用 action -- [Permissions](/docs/configure/permissions) —— 控制谁可以调用什么 diff --git a/content/docs/build/interface/actions.zh-Hant.mdx b/content/docs/build/interface/actions.zh-Hant.mdx deleted file mode 100644 index 9a69dee..0000000 --- a/content/docs/build/interface/actions.zh-Hant.mdx +++ /dev/null @@ -1,190 +0,0 @@ ---- -# @generated zh-Hant from zh-Hans -- do not edit. Edit the English source, then regenerate: pnpm --filter @objectos/docs gen:zh-hant -title: 操作 -description: 平臺從單一宣告出發,將命名操作同時暴露為 REST 端點、Console 按鈕、流程步驟和 AI 工具。 -translation: - source_sha: d4f9d1d0443d08d33b2ea0aa548c7b8db1541a8b693a65d481af6cafba577018 - guide_rev: 1 - mode: auto ---- - -**Action** 是物件上的一個命名操作。只需宣告一次,它就會以以下形式出現: - -- 一個位於 `/api/v1/actions//` 的 **REST 端點** -- Console 記錄詳情中的一個**按鈕** -- 用於自動化的一個**流程步驟**(`type: 'action'`) -- 供 Agents 和 AI Builder 使用的一個 **AI 工具**(`action_`) - -你無需在四個介面上重複定義。一次宣告,四種呼叫方式。 - -## 宣告一個 action - -```ts -// src/actions/approve_invoice.action.ts -import { Action } from '@objectstack/spec'; - -export const approveInvoice = Action.create({ - name: 'approve_invoice', // lowercase snake_case (machine id) - label: 'Approve Invoice', - objectName: 'invoice', // attaches to the invoice object - icon: 'check', - variant: 'primary', - locations: ['record_header'], // where the button shows - confirmText: 'Approve this invoice?', - successMessage: 'Invoice approved', - refreshAfter: true, - - // collect input before running - params: [ - { name: 'note', label: 'Approval note', type: 'textarea' }, - ], - - // only show the button when the record is still pending - visible: 'record.status == "pending"', - - // what it does — a sandboxed script body - type: 'script', - body: { - language: 'js', - source: ` - await ctx.data.update('invoice', input.id, { - status: 'approved', - approved_by: ctx.user.id, - approved_at: now(), - approval_note: input.note, - }); - `, - }, -}); -``` - -在 `os dev` 重新編譯之後: - -- `POST /api/v1/actions/invoice/approve_invoice` 可用 -- Console 中的 Invoice 記錄頁面會顯示一個 **Approve Invoice** 按鈕 -- 流程可以包含 `{ type: 'action', action: 'approve_invoice', inputs: { note: '…' } }` -- 如果 AI 助手的技能允許,它可以呼叫 `action_approve_invoice` - -## Action 型別 - -`type` 欄位決定 action 執行什麼: - -| `type` | 執行內容 | 適用場景 | -|---|---|---| -| `script` | 一個 `body` —— 一個 L1 formula 表示式或沙箱化的 L2 JavaScript | 大多數場景 —— 服務端邏輯,可審計且可被 AI 呼叫 | -| `api` | 對某個 `target` 端點發起的 HTTP 呼叫(`method`、`bodyExtra`) | 複用資料 API 或平臺端點 | -| `flow` | 執行 `target` 中指定名稱的流程 | 多步驟業務流程 | -| `url` | 跳轉到 `target` URL | 深度連結、重定向式操作 | -| `modal` | 開啟 `target` 中指定名稱的頁面/模態框 | 自定義對話方塊 | -| `form` | 開啟 `target` 中指定名稱的 FormView | 引導式資料錄入 | - -```ts -// api type — reuse a data-API endpoint -Action.create({ - name: 'archive_order', - objectName: 'order', - label: 'Archive', - locations: ['list_item'], - type: 'api', - method: 'PATCH', - target: '/api/v1/data/order/{id}', - bodyExtra: { archived: true }, -}); -``` - -除 `script` 之外的型別都需要一個 `target`。無論哪種型別,該 action 在每個介面上都是同等的一級公民。 - -## 呼叫一個 action - -### REST - -```bash -# the record id can go in the body, or in the path -curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice/inv_123 \ - -H 'Authorization: Bearer ' \ - -H 'Content-Type: application/json' \ - -d '{"note": "LGTM"}' -``` - -引數以扁平形式放在請求體中傳送。記錄 id 既可以作為路徑末尾的片段提供(`.../approve_invoice/:recordId`),也可以放在請求體中。響應是你的指令碼 body 的返回值(對於 `api` 型別的 action 則是呼叫結果)。 - -### Console - -預設情況下,Console 會將 action 顯示為記錄詳情頁面上的按鈕,並按 action 的 `visible` 謂詞進行過濾。你可以在檢視配置中覆蓋其位置: - -```ts -defineView({ - name: 'invoice_detail', - object: 'invoice', - actions: ['approve_invoice', 'reject_invoice', 'send_to_customer'], -}); -``` - -### 來自流程 - -```ts -{ - type: 'action', - action: 'approve_invoice', - inputs: { note: 'Auto-approved by SLA flow' }, - record: '{!trigger.record.id}', -} -``` - -### 來自 AI Agent - -如果 `approve_invoice` 包含在該 agent 擁有的任意技能中,LLM 就可以呼叫它。輸入來自對話內容;許可權的執行方式與使用者直接呼叫時完全一致。 - -> *"Approve invoice INV-2042 with note 'verified by phone.'"* - -## 許可權 - -Action 以發起呼叫的使用者的許可權執行。平臺會檢查: - -1. **物件許可權** —— 使用者的[許可權集](/docs/configure/permissions/permission-sets)必須授予該 action 所需的物件級訪問許可權(例如 update)。 -2. **欄位許可權** —— 對於該 action 寫入的任何欄位,使用者必須擁有寫入訪問許可權(FLS)。 -3. **UI 控制** —— `visible` 和 `disabled` 謂詞(CEL,針對 `record`、`os.user` 和引數求值)控制按鈕在 Console 中是渲染還是置灰。 - -未通過的許可權檢查會返回 `403`,並附帶一個 `PERMISSION_DENIED` 錯誤。 - -## 內建 action - -每個物件都會免費獲得以下 action: - -| Action | 作用 | -|---|---| -| `create` | 插入一條記錄 | -| `update` | 更新一條記錄 | -| `delete` | 刪除(或軟刪除)一條記錄 | -| `restore` | 撤銷軟刪除 | -| `clone` | 深複製一條記錄 | -| `share` | 直接與某個使用者/角色共享 | - -不要重新宣告這些 —— 它們遵循物件的[生命週期與能力標誌](/docs/build/data)。 - -## 審計 - -平臺事件會落入 `sys_audit_log`,這是一條不可變的軌跡,其欄位包括: - -- `user_id` —— 發起操作的使用者 -- `action` —— action 名稱 -- `object_name` 和 `record_id` —— 被操作的物件 -- `old_value` / `new_value` —— 變更內容 -- `ip_address` / `user_agent` —— 請求來源 -- `created_at` —— 發生時間 - -當你需要回答*"是誰按下了這個按鈕?"*之類的問題時,這裡是首選的查證之處。 - -## 使用 AI Builder 生成 action - -> *"Create an action `escalate_ticket` on `support_ticket` that sets -> priority to urgent and assigns it to the on-call engineer."* - -[AI Builder](/docs/build/ai-builder) 會生成該 action 的後設資料,並將變更排隊等待審批。審批通過後,該 action 即可從 REST、Console、流程,以及 —— 遞迴地 —— AI 自身進行呼叫。 - -## 後續去向 - -- [Flows](/docs/build/automation/flows) —— 將多個 action 組合成業務邏輯 -- [Agents](/docs/build/agents) —— 將 action 暴露為 AI 工具 -- [API Access](/docs/configure/api-access) —— 從外部系統呼叫 action -- [Permissions](/docs/configure/permissions) —— 控制誰可以呼叫什麼