Skip to content

Commit 5162da9

Browse files
feat(approvals): publish approval node config schema + xRef reference hints (ADR-0018/0019) (#1412)
Make the engine the single source of truth for the Approval node's property form so the Studio flow designer renders from the backend rather than a hardcoded client form. - plugin-approvals: publish `configSchema` (getApprovalNodeConfigJsonSchema()) on the approval ActionDescriptor (ADR-0018 §configSchema). - spec: annotate `approvalStatusField` with `.meta({ xRef: { kind: 'object-field', objectSource: '$trigger' } })` so the designer renders an object-field picker (scoped to the flow's trigger object) instead of free text. Single `.meta()` carries both description and annotation so neither is dropped through z.toJSONSchema. - examples (app-crm, app-showcase): require the `approvals` capability so the `approval` flow node is contributed to the engine and surfaces in the palette. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
1 parent 21b5d0f commit 5162da9

4 files changed

Lines changed: 48 additions & 4 deletions

File tree

examples/app-crm/objectstack.config.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,11 @@ export default defineStack({
6565
},
6666

6767
// Auto-resolved by the CLI; `ui` enables the Studio shell, `automation` loads
68-
// AutomationServicePlugin + node packs so screen flows can execute.
69-
requires: ['ui', 'automation'],
68+
// AutomationServicePlugin + node packs so screen flows can execute, and
69+
// `approvals` loads ApprovalsServicePlugin so the `approval` flow node is
70+
// contributed to the engine (ADR-0019) — required for the discount-approval
71+
// flow to compile/register and to surface the node in the designer palette.
72+
requires: ['ui', 'automation', 'approvals'],
7073

7174
// Infrastructure
7275
datasources: [CrmDatasource, CrmAnalyticsDatasource],

examples/app-showcase/objectstack.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ export default defineStack({
4949
description: 'Kitchen-sink workspace covering all metadata types, all view types, and the major capability chains.',
5050
},
5151

52-
requires: ['ui', 'automation'],
52+
// `approvals` loads ApprovalsServicePlugin so the `approval` flow node
53+
// (ADR-0019) is contributed to the engine — the showcase flows use it.
54+
requires: ['ui', 'automation', 'approvals'],
5355

5456
// Infrastructure
5557
datasources: allDatasources,

packages/plugins/plugin-approvals/src/approval-node.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import {
2121
defineActionDescriptor,
2222
ApprovalNodeConfigSchema,
23+
getApprovalNodeConfigJsonSchema,
2324
APPROVAL_NODE_TYPE,
2425
type ApprovalNodeConfig,
2526
} from '@objectstack/spec/automation';
@@ -74,6 +75,10 @@ export function registerApprovalNode(
7475
// Human decision: the run suspends here awaiting an external reply.
7576
supportsPause: true,
7677
isAsync: true,
78+
// Publish the node's config contract (ADR-0018 §configSchema) so the
79+
// Studio flow designer renders the Approval property form from the engine
80+
// rather than a hardcoded client form — the engine owns the shape.
81+
configSchema: getApprovalNodeConfigJsonSchema(),
7782
}),
7883
async execute(node, variables, context) {
7984
const parsed = ApprovalNodeConfigSchema.safeParse(node.config ?? {});

packages/spec/src/automation/approval.zod.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,43 @@ export const ApprovalNodeConfigSchema = lazySchema(() => z.object({
118118
* object. Omitted ⇒ status is exposed only via `sys_approval_request`.
119119
*/
120120
approvalStatusField: z.string().optional()
121-
.describe('Business-object field to mirror request status onto'),
121+
// `xRef` marks this string as a typed reference (ADR-0018 §configSchema):
122+
// the Studio designer renders an object-field picker instead of free text.
123+
// `objectSource: '$trigger'` resolves the field catalog from the flow's
124+
// trigger object (the record this approval acts on). A single `.meta()`
125+
// carries both description and the annotation so neither is dropped.
126+
.meta({
127+
description: 'Business-object field to mirror request status onto',
128+
xRef: { kind: 'object-field', objectSource: '$trigger' },
129+
}),
122130

123131
/** Optional per-node SLA escalation. */
124132
escalation: ApprovalEscalationSchema.optional().describe('Per-node SLA escalation'),
125133
}));
126134
export type ApprovalNodeConfig = z.infer<typeof ApprovalNodeConfigSchema>;
135+
136+
/**
137+
* JSON Schema for {@link ApprovalNodeConfigSchema}, memoized.
138+
*
139+
* Published on the Approval action descriptor's `configSchema`
140+
* (ADR-0018/0019) so the **engine** is the single source of truth for the
141+
* node's config contract: the Studio flow designer renders the Approval node's
142+
* property form from this schema (rather than a hardcoded client form), and the
143+
* same schema backs `registerFlow()` config validation. Derived with Zod v4's
144+
* `z.toJSONSchema` in `input` mode (the author-facing shape — default-bearing
145+
* fields are optional). Lazily computed so the wrapped schema is only resolved
146+
* when a descriptor is actually built.
147+
*/
148+
let cachedApprovalNodeConfigJsonSchema: unknown;
149+
export function getApprovalNodeConfigJsonSchema(): unknown {
150+
if (cachedApprovalNodeConfigJsonSchema === undefined) {
151+
cachedApprovalNodeConfigJsonSchema = z.toJSONSchema(ApprovalNodeConfigSchema, {
152+
target: 'draft-2020-12',
153+
io: 'input',
154+
// Approval config has no unrepresentable constructs today; keep the
155+
// designer resilient if one is ever added rather than throwing at boot.
156+
unrepresentable: 'any',
157+
});
158+
}
159+
return cachedApprovalNodeConfigJsonSchema;
160+
}

0 commit comments

Comments
 (0)