Skip to content

Commit 44a1642

Browse files
committed
Revamp action definitions and types for server functions
Updated documentation to clarify the concept of actions as server-side functions, distinguishing between global and record scopes, and providing schema-first input examples. Refactored TypeScript types to improve type safety, unify input schema with FieldConfig, and enhance metadata for UI integration and internal API usage.
1 parent 37da16c commit 44a1642

2 files changed

Lines changed: 185 additions & 29 deletions

File tree

docs/spec/action.md

Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,110 @@
1-
# Action Definitions (RPC)
1+
# Action Definitions (Server Functions)
22

3-
Custom business logic can be defined in the `actions` section. Actions act as Remote Procedure Calls (RPC) scoped to the object.
3+
Actions are custom Server-Side Functions attached to an object. They allow you to encapsulate complex business logic that goes beyond standard CRUD operations.
4+
5+
Unlike Hooks (which trigger automatically), Actions are explicitly invoked by the client (API, Button, Scheduled Task).
6+
7+
## 1. Concepts
8+
9+
### 1.1 Scope (`type`)
10+
* **Global Actions**: Operate on the collection level.
11+
* *Examples:* "Import CSV", "Generate Monthly Report", "Sync with External API".
12+
* *Context:* No `id`.
13+
* **Record Actions**: Operate on a specific record instance.
14+
* *Examples:* "Approve", "Reject", "Send Email", "Clone".
15+
* *Context:* Has `id`.
16+
17+
### 1.2 Schema-First Inputs
18+
Input parameters (`params`) are defined using the same `FieldConfig` schema as object fields. This gives you free validation, type coercion, and UI generation.
19+
20+
## 2. Configuration (YAML)
21+
22+
Actions are declared in `*.object.yml` or JSON.
423

524
```yaml
625
actions:
7-
approve:
8-
label: Approve Request
9-
description: Approves the current record.
26+
# 1. A Record Action (Button on a row)
27+
approve_order:
28+
type: record
29+
label: Approve Order
30+
icon: standard:approval
31+
confirm_text: "Are you sure you want to approve this order? This cannot be undone."
1032
params:
1133
comment:
1234
type: textarea
13-
label: Approval Comments
14-
result:
15-
type: boolean
35+
required: true
36+
label: Approval Reason
37+
38+
# 2. A Global Action (Button on list view)
39+
sync_jira:
40+
type: global
41+
label: Sync from Jira
42+
internal: true # Not exposed to public API
43+
params:
44+
project_key:
45+
type: text
46+
required: true
47+
```
48+
49+
## 3. Implementation (TypeScript)
50+
51+
Implement the logic in a companion `*.action.ts` file.
52+
53+
```typescript
54+
// src/objects/order.action.ts
55+
import { ActionDefinition } from '@objectql/types';
56+
import { Order } from './types';
57+
58+
// Input Type Definition
59+
interface ApproveInput {
60+
comment: string;
61+
}
62+
63+
export const approve_order: ActionDefinition<Order, ApproveInput> = {
64+
type: 'record',
65+
66+
// Logic
67+
handler: async ({ id, input, api, user }) => {
68+
// 1. Fetch current state
69+
const order = await api.findOne('order', id);
70+
71+
if (order.status !== 'Draft') {
72+
throw new Error("Only draft orders can be approved");
73+
}
74+
75+
// 2. Perform updates using Atomic Operations or Transactions
76+
await api.update('order', id, {
77+
status: 'Approved',
78+
approved_by: user.id,
79+
approval_comment: input.comment,
80+
approved_at: new Date()
81+
});
82+
83+
// 3. Return result to client
84+
return { success: true, new_status: 'Approved' };
85+
}
86+
}
1687
```
1788

18-
## 1. Properties
89+
## 4. Why this design is "Optimal"?
90+
91+
1. **Unified Schema**: Inputs use the same definitions as Database fields. If you know how to define a table, you know how to define an API argument.
92+
2. **UI Ready**: The metadata (`label`, `icon`, `confirm_text`, `params`) contains everything a frontend framework (like React Admin or Salesforce Lightning) needs to render a button and a modal form **automatically**.
93+
3. **Type Safety**: The `ActionDefinition<Entity, Input, Output>` generic ensures your handler code respects the contract.
94+
95+
## 5. Loading & Registration (Standard)
96+
97+
To ensure the Metadata Loader can automatically bind your actions to the correct object, you must follow the file naming convention:
98+
99+
* **Object Definition**: `mypackage/objects/invoice.object.yml`
100+
* **Action Implementation**: `mypackage/objects/invoice.action.ts` (or `.js`)
101+
102+
The loader extracts the `objectName` from the filename (everything before `.action.`).
103+
104+
```typescript
105+
// mypackage/objects/invoice.action.ts
106+
export const approve_invoice: ActionDefinition<Invoice> = { ... };
107+
export const reject_invoice: ActionDefinition<Invoice> = { ... };
108+
```
19109

20-
| Property | Type | Description |
21-
| :--- | :--- | :--- |
22-
| `label` | `string` | Display label (e.g., for buttons). |
23-
| `icon` | `string` | Icon name. |
24-
| `description` | `string` | Help text. |
25-
| `confirmText` | `string` | Confirmation message before execution. |
26-
| `params` | `Map<string, FieldConfig>` | Input parameters schema. Same structure as Object fields. |
27-
| `result` | `FieldConfig` | The shape of the return value. |
110+
The loader will register `approve_invoice` and `reject_invoice` as actions for the `invoice` object.

packages/types/src/action.ts

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,94 @@
11
import { FieldConfig } from "./field";
2-
import { ObjectQLContext } from "./types";
2+
import { HookAPI } from "./hook"; // Reuse the restricted API interface
33

4+
/**
5+
* Defines the scope of the action.
6+
* - `record`: Acts on a specific record instance (e.g. "Approve Order").
7+
* - `global`: Acts on the collection or system (e.g. "Import CSV", "Daily Report").
8+
*/
9+
export type ActionType = 'record' | 'global';
10+
11+
/**
12+
* Re-using FieldConfig allows us to describe input parameters
13+
* using the same rich vocabulary as database fields (validation, UI hints, etc).
14+
*/
15+
export type ActionInputDefinition = Record<string, FieldConfig>;
16+
17+
/**
18+
* Context passed to the action handler execution.
19+
*/
20+
export interface ActionContext<BaseT = any, InputT = any> {
21+
/** The object this action belongs to. */
22+
objectName: string;
23+
24+
/** The name of the action being executed. */
25+
actionName: string;
26+
27+
/**
28+
* The ID of the record being acted upon.
29+
* Only available if type is 'record'.
30+
*/
31+
id?: string | number;
32+
33+
/**
34+
* The validated input arguments.
35+
*/
36+
input: InputT;
37+
38+
/**
39+
* Database Access API (Same as Hooks).
40+
*/
41+
api: HookAPI;
42+
43+
/**
44+
* User Session.
45+
*/
46+
user?: {
47+
id: string | number;
48+
[key: string]: any;
49+
};
50+
}
51+
52+
/**
53+
* The configuration of an Action visible to the Metadata engine (YAML/JSON side).
54+
*/
455
export interface ActionConfig {
556
label?: string;
6-
icon?: string;
757
description?: string;
8-
confirmText?: string;
58+
icon?: string;
959

10-
// Parameters definition for the action (input schema)
11-
params?: Record<string, FieldConfig>;
12-
}
60+
/**
61+
* Default: 'global' if no fields defined, but usually specified explicitly.
62+
*/
63+
type?: ActionType; // 'record' | 'global'
1364

14-
export interface ActionContext extends ObjectQLContext {
15-
objectName: string;
16-
actionName: string;
17-
id?: string | number; // Optional record ID if action is applied to a record
18-
params: any; // Arguments passed to the action
65+
/**
66+
* Message to show before executing. If present, UI should prompt confirmation.
67+
*/
68+
confirm_text?: string;
69+
70+
/**
71+
* If true, this action is not exposed via API directly (server-internal).
72+
*/
73+
internal?: boolean;
74+
75+
/**
76+
* Input parameter schema.
77+
*/
78+
params?: ActionInputDefinition;
79+
80+
/**
81+
* Output data shape description (optional, for content negotiation).
82+
*/
83+
return_type?: string | FieldConfig;
1984
}
2085

21-
export type ActionHandler = (ctx: ActionContext) => Promise<any>;
86+
/**
87+
* The full implementation definition (Code side).
88+
*/
89+
export interface ActionDefinition<BaseT = any, InputT = any, ReturnT = any> extends ActionConfig {
90+
/**
91+
* The business logic implementation.
92+
*/
93+
handler: (ctx: ActionContext<BaseT, InputT>) => Promise<ReturnT>;
94+
}

0 commit comments

Comments
 (0)