@memberjunction/actions 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +416 -0
- package/package.json +10 -10
- package/readme.md +321 -429
package/README.md
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# @memberjunction/actions
|
|
2
|
+
|
|
3
|
+
Server-side action execution engine for MemberJunction. This package provides the runtime infrastructure for executing actions — including input validation, filter evaluation, ClassFactory-based action dispatch, execution logging, OAuth token management, and entity-bound action invocation. It is intended for server-side use only.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @memberjunction/actions
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
The Actions Engine sits between external consumers (AI agents, workflows, APIs) and the actual action implementations registered via `@RegisterClass`. It handles the full execution lifecycle: validating inputs, running pre-execution filters, dispatching to the correct `BaseAction` subclass via ClassFactory, and logging results.
|
|
14
|
+
|
|
15
|
+
The package contains two subsystems:
|
|
16
|
+
|
|
17
|
+
- **Generic Action Engine** — Executes standalone actions with validation, filtering, and logging
|
|
18
|
+
- **Entity Action Engine** — Executes actions bound to entity records, supporting CRUD lifecycle hooks, list/view batch operations, and record validation
|
|
19
|
+
|
|
20
|
+
```mermaid
|
|
21
|
+
flowchart TD
|
|
22
|
+
subgraph Consumers["External Consumers"]
|
|
23
|
+
Agent["AI Agents"]
|
|
24
|
+
WF["Workflows"]
|
|
25
|
+
API["GraphQL API"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
subgraph Engine["@memberjunction/actions"]
|
|
29
|
+
AES["ActionEngineServer"]
|
|
30
|
+
EAES["EntityActionEngineServer"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
subgraph Pipeline["Execution Pipeline"]
|
|
34
|
+
Validate["Validate Inputs"]
|
|
35
|
+
Filter["Run Filters"]
|
|
36
|
+
Dispatch["ClassFactory Dispatch"]
|
|
37
|
+
Log["Execution Logging"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
subgraph Actions["Registered Actions"]
|
|
41
|
+
BA["BaseAction Subclasses"]
|
|
42
|
+
OAuth["BaseOAuthAction Subclasses"]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Consumers --> Engine
|
|
46
|
+
AES --> Validate --> Filter --> Dispatch --> Log
|
|
47
|
+
Dispatch --> BA
|
|
48
|
+
Dispatch --> OAuth
|
|
49
|
+
EAES --> AES
|
|
50
|
+
|
|
51
|
+
style Consumers fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
52
|
+
style Engine fill:#7c5295,stroke:#563a6b,color:#fff
|
|
53
|
+
style Pipeline fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
54
|
+
style Actions fill:#b8762f,stroke:#8a5722,color:#fff
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Key Features
|
|
58
|
+
|
|
59
|
+
- **Action Execution Pipeline** — Validates inputs, evaluates filters, dispatches via ClassFactory, and logs all executions
|
|
60
|
+
- **ClassFactory Dispatch** — Looks up `BaseAction` subclasses by `DriverClass` or action name at runtime
|
|
61
|
+
- **Pre-Execution Filters** — `BaseActionFilter` subclasses can gate whether an action should run
|
|
62
|
+
- **Execution Logging** — Automatic start/end logging to `Action Execution Logs` entity with params and result codes
|
|
63
|
+
- **OAuth Token Management** — `BaseOAuthAction` provides token lifecycle (refresh, retry on auth failure, persistence)
|
|
64
|
+
- **OAuth2Manager** — Standalone OAuth2 client supporting authorization code, client credentials, and refresh token flows
|
|
65
|
+
- **Entity Action Invocation** — Bind actions to entity CRUD lifecycle events (BeforeCreate, AfterUpdate, etc.)
|
|
66
|
+
- **Batch Entity Actions** — Run actions against Lists or Views of records with consolidated results
|
|
67
|
+
- **Script Evaluation** — Entity action params support runtime script evaluation with entity context
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
### Running a Standalone Action
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { ActionEngineServer } from '@memberjunction/actions';
|
|
75
|
+
|
|
76
|
+
// Configure the engine (typically done once at startup)
|
|
77
|
+
await ActionEngineServer.Instance.Config(false, contextUser);
|
|
78
|
+
|
|
79
|
+
// Find the action by name
|
|
80
|
+
const action = ActionEngineServer.Instance.Actions.find(a => a.Name === 'Send Email');
|
|
81
|
+
|
|
82
|
+
// Execute it
|
|
83
|
+
const result = await ActionEngineServer.Instance.RunAction({
|
|
84
|
+
Action: action,
|
|
85
|
+
ContextUser: contextUser,
|
|
86
|
+
Params: [
|
|
87
|
+
{ Name: 'to', Value: 'user@example.com', Type: 'Input' },
|
|
88
|
+
{ Name: 'subject', Value: 'Hello', Type: 'Input' },
|
|
89
|
+
{ Name: 'body', Value: 'Message content', Type: 'Input' }
|
|
90
|
+
],
|
|
91
|
+
Filters: []
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (result.Success) {
|
|
95
|
+
console.log('Action completed:', result.Message);
|
|
96
|
+
} else {
|
|
97
|
+
console.error('Action failed:', result.Message);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Creating a Custom Action
|
|
102
|
+
|
|
103
|
+
All actions extend `BaseAction` and implement `InternalRunAction`. Register them with `@RegisterClass` so the engine can discover them via ClassFactory:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
107
|
+
import { BaseAction } from '@memberjunction/actions';
|
|
108
|
+
import { RunActionParams, ActionResultSimple } from '@memberjunction/actions-base';
|
|
109
|
+
|
|
110
|
+
@RegisterClass(BaseAction, 'My Custom Action')
|
|
111
|
+
export class MyCustomAction extends BaseAction {
|
|
112
|
+
protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> {
|
|
113
|
+
const inputValue = params.Params.find(p => p.Name === 'input')?.Value;
|
|
114
|
+
|
|
115
|
+
// Your action logic here
|
|
116
|
+
const result = await this.doWork(inputValue);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
Success: true,
|
|
120
|
+
ResultCode: 'SUCCESS',
|
|
121
|
+
Message: `Processed: ${result}`
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async doWork(input: string): Promise<string> {
|
|
126
|
+
// Delegate to service classes for real logic
|
|
127
|
+
return `Done with ${input}`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Creating an OAuth-Authenticated Action
|
|
133
|
+
|
|
134
|
+
For actions that need to call external APIs with OAuth2 credentials:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
138
|
+
import { BaseOAuthAction } from '@memberjunction/actions';
|
|
139
|
+
import { RunActionParams, ActionResultSimple } from '@memberjunction/actions-base';
|
|
140
|
+
|
|
141
|
+
@RegisterClass(BaseAction, 'Fetch External Data')
|
|
142
|
+
export class FetchExternalDataAction extends BaseOAuthAction {
|
|
143
|
+
protected async refreshAccessToken(): Promise<void> {
|
|
144
|
+
// Platform-specific token refresh logic
|
|
145
|
+
const response = await fetch('https://api.example.com/oauth/token', {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
body: new URLSearchParams({
|
|
148
|
+
grant_type: 'refresh_token',
|
|
149
|
+
refresh_token: this.getRefreshToken(),
|
|
150
|
+
})
|
|
151
|
+
});
|
|
152
|
+
const data = await response.json();
|
|
153
|
+
await this.updateStoredTokens(data.access_token, data.refresh_token, data.expires_in);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> {
|
|
157
|
+
const companyIntegrationId = params.Params.find(
|
|
158
|
+
p => p.Name === 'CompanyIntegrationID'
|
|
159
|
+
)?.Value as string;
|
|
160
|
+
|
|
161
|
+
// Initialize OAuth (loads tokens, refreshes if expired)
|
|
162
|
+
if (!await this.initializeOAuth(companyIntegrationId)) {
|
|
163
|
+
return this.handleOAuthError(new Error('OAuth initialization failed'));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Make authenticated request with automatic retry on 401
|
|
167
|
+
const data = await this.makeAuthenticatedRequest(async (token) => {
|
|
168
|
+
const res = await fetch('https://api.example.com/data', {
|
|
169
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
170
|
+
});
|
|
171
|
+
return res.json();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return { Success: true, ResultCode: 'SUCCESS', Message: JSON.stringify(data) };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Using OAuth2Manager Directly
|
|
180
|
+
|
|
181
|
+
For standalone OAuth2 token management outside the action framework:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { OAuth2Manager } from '@memberjunction/actions';
|
|
185
|
+
|
|
186
|
+
const oauth = new OAuth2Manager({
|
|
187
|
+
clientId: 'your-client-id',
|
|
188
|
+
clientSecret: 'your-client-secret',
|
|
189
|
+
tokenEndpoint: 'https://api.example.com/oauth/token',
|
|
190
|
+
scopes: ['read', 'write'],
|
|
191
|
+
onTokenUpdate: async (tokens) => {
|
|
192
|
+
// Persist tokens to your storage
|
|
193
|
+
await saveTokens(tokens);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Get a valid token (auto-refreshes if expired)
|
|
198
|
+
const token = await oauth.getAccessToken();
|
|
199
|
+
|
|
200
|
+
// Or use client credentials flow
|
|
201
|
+
const tokenData = await oauth.getClientCredentialsToken();
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Architecture
|
|
205
|
+
|
|
206
|
+
### Action Execution Pipeline
|
|
207
|
+
|
|
208
|
+
The `ActionEngineServer.RunAction()` method follows this sequence:
|
|
209
|
+
|
|
210
|
+
```mermaid
|
|
211
|
+
sequenceDiagram
|
|
212
|
+
participant Caller
|
|
213
|
+
participant Engine as ActionEngineServer
|
|
214
|
+
participant Filter as BaseActionFilter
|
|
215
|
+
participant CF as ClassFactory
|
|
216
|
+
participant Action as BaseAction Subclass
|
|
217
|
+
participant Log as Execution Log
|
|
218
|
+
|
|
219
|
+
Caller->>Engine: RunAction(params)
|
|
220
|
+
Engine->>Engine: ValidateInputs(params)
|
|
221
|
+
alt Validation fails
|
|
222
|
+
Engine->>Log: StartAndEndActionLog()
|
|
223
|
+
Engine-->>Caller: {Success: false}
|
|
224
|
+
end
|
|
225
|
+
Engine->>Filter: RunFilters(params)
|
|
226
|
+
alt Filters block execution
|
|
227
|
+
Engine->>Log: StartAndEndActionLog()
|
|
228
|
+
Engine-->>Caller: {Success: true, "Filters blocked"}
|
|
229
|
+
end
|
|
230
|
+
Engine->>Log: StartActionLog()
|
|
231
|
+
Engine->>CF: CreateInstance(BaseAction, driverClass)
|
|
232
|
+
CF-->>Engine: action instance
|
|
233
|
+
Engine->>Action: Run(params)
|
|
234
|
+
Action->>Action: InternalRunAction(params)
|
|
235
|
+
Action-->>Engine: ActionResultSimple
|
|
236
|
+
Engine->>Log: EndActionLog()
|
|
237
|
+
Engine-->>Caller: ActionResult
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Entity Action Invocation
|
|
241
|
+
|
|
242
|
+
Entity actions are bound to entity lifecycle events. The `EntityActionEngineServer` delegates to invocation-type-specific handlers via ClassFactory:
|
|
243
|
+
|
|
244
|
+
```mermaid
|
|
245
|
+
classDiagram
|
|
246
|
+
class EntityActionInvocationBase {
|
|
247
|
+
<<abstract>>
|
|
248
|
+
+InvokeAction(params) EntityActionResult
|
|
249
|
+
+MapParams(params, entityActionParams, entity) ActionParam[]
|
|
250
|
+
+SafeEvalScript(id, script, entity) any
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
class SingleRecord {
|
|
254
|
+
+InvokeAction(params) EntityActionResult
|
|
255
|
+
+ValidateParams(params) boolean
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
class MultipleRecords {
|
|
259
|
+
+InvokeAction(params) EntityActionResult
|
|
260
|
+
#GetRecordList() BaseEntity[]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
class Validate {
|
|
264
|
+
+InvokeAction(params) EntityActionResult
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
EntityActionInvocationBase <|-- SingleRecord
|
|
268
|
+
EntityActionInvocationBase <|-- MultipleRecords
|
|
269
|
+
SingleRecord <|-- Validate
|
|
270
|
+
|
|
271
|
+
note for SingleRecord "Registered for: Read, BeforeCreate,\nBeforeUpdate, BeforeDelete, AfterCreate,\nAfterUpdate, AfterDelete, SingleRecord"
|
|
272
|
+
note for MultipleRecords "Registered for: List, View"
|
|
273
|
+
note for Validate "Registered for: Validate"
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Class Hierarchy
|
|
277
|
+
|
|
278
|
+
```mermaid
|
|
279
|
+
classDiagram
|
|
280
|
+
class BaseAction {
|
|
281
|
+
<<abstract>>
|
|
282
|
+
+Run(params) ActionResultSimple
|
|
283
|
+
#InternalRunAction(params)* ActionResultSimple
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
class BaseOAuthAction {
|
|
287
|
+
<<abstract>>
|
|
288
|
+
#initializeOAuth(id) boolean
|
|
289
|
+
#getAccessToken() string
|
|
290
|
+
#makeAuthenticatedRequest(fn) T
|
|
291
|
+
#refreshAccessToken()* void
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
class BaseActionFilter {
|
|
295
|
+
<<abstract>>
|
|
296
|
+
+Run(params, filter) boolean
|
|
297
|
+
#InternalRun(params, filter)* boolean
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
class ActionEngineServer {
|
|
301
|
+
+RunAction(params) ActionResult
|
|
302
|
+
#ValidateInputs(params) boolean
|
|
303
|
+
#RunFilters(params) boolean
|
|
304
|
+
#InternalRunAction(params) ActionResult
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
class EntityActionEngineServer {
|
|
308
|
+
+RunEntityAction(params) EntityActionResult
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
class OAuth2Manager {
|
|
312
|
+
+getAccessToken() string
|
|
313
|
+
+getAuthorizationUrl() string
|
|
314
|
+
+exchangeAuthorizationCode(code) OAuth2TokenData
|
|
315
|
+
+getClientCredentialsToken() OAuth2TokenData
|
|
316
|
+
+refreshAccessToken() OAuth2TokenData
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
BaseAction <|-- BaseOAuthAction
|
|
320
|
+
ActionEngineServer --> BaseAction : dispatches to
|
|
321
|
+
ActionEngineServer --> BaseActionFilter : evaluates
|
|
322
|
+
EntityActionEngineServer --> ActionEngineServer : delegates to
|
|
323
|
+
BaseOAuthAction --> OAuth2Manager : can use
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## API Reference
|
|
327
|
+
|
|
328
|
+
### ActionEngineServer
|
|
329
|
+
|
|
330
|
+
Singleton engine that executes actions. Access via `ActionEngineServer.Instance`.
|
|
331
|
+
|
|
332
|
+
| Method | Description |
|
|
333
|
+
|--------|-------------|
|
|
334
|
+
| `Config(forceRefresh, contextUser)` | Initialize/refresh the engine's action and filter metadata |
|
|
335
|
+
| `RunAction(params)` | Execute an action through the full pipeline (validate, filter, dispatch, log) |
|
|
336
|
+
|
|
337
|
+
### BaseAction
|
|
338
|
+
|
|
339
|
+
Abstract base class for all action implementations.
|
|
340
|
+
|
|
341
|
+
| Method | Description |
|
|
342
|
+
|--------|-------------|
|
|
343
|
+
| `Run(params)` | Public entry point — calls `InternalRunAction` |
|
|
344
|
+
| `InternalRunAction(params)` | **Abstract** — implement your action logic here |
|
|
345
|
+
|
|
346
|
+
### BaseOAuthAction
|
|
347
|
+
|
|
348
|
+
Abstract base for actions requiring OAuth authentication. Extends `BaseAction`.
|
|
349
|
+
|
|
350
|
+
| Method | Description |
|
|
351
|
+
|--------|-------------|
|
|
352
|
+
| `initializeOAuth(companyIntegrationId)` | Load integration, check/refresh tokens |
|
|
353
|
+
| `getAccessToken()` | Get the current access token |
|
|
354
|
+
| `makeAuthenticatedRequest(fn)` | Execute a request with automatic retry on 401/403 |
|
|
355
|
+
| `refreshAccessToken()` | **Abstract** — implement platform-specific token refresh |
|
|
356
|
+
| `updateStoredTokens(access, refresh?, expiresIn?)` | Persist new tokens to the Company Integration entity |
|
|
357
|
+
| `handleOAuthError(error)` | Return a standardized error result for OAuth failures |
|
|
358
|
+
|
|
359
|
+
### BaseActionFilter
|
|
360
|
+
|
|
361
|
+
Abstract base for pre-execution filters.
|
|
362
|
+
|
|
363
|
+
| Method | Description |
|
|
364
|
+
|--------|-------------|
|
|
365
|
+
| `Run(params, filter)` | Public entry point — calls `InternalRun` |
|
|
366
|
+
| `InternalRun(params, filter)` | **Abstract** — implement filter logic, return `true` to allow execution |
|
|
367
|
+
|
|
368
|
+
### EntityActionEngineServer
|
|
369
|
+
|
|
370
|
+
Singleton engine for entity-bound actions. Access via `EntityActionEngineServer.Instance`.
|
|
371
|
+
|
|
372
|
+
| Method | Description |
|
|
373
|
+
|--------|-------------|
|
|
374
|
+
| `RunEntityAction(params)` | Execute an entity action, dispatching to the correct invocation type handler |
|
|
375
|
+
|
|
376
|
+
### OAuth2Manager
|
|
377
|
+
|
|
378
|
+
Standalone OAuth2 token manager supporting multiple grant types.
|
|
379
|
+
|
|
380
|
+
| Method | Description |
|
|
381
|
+
|--------|-------------|
|
|
382
|
+
| `getAccessToken()` | Get a valid token, auto-refreshing if needed (thread-safe) |
|
|
383
|
+
| `getAuthorizationUrl(state?)` | Build the authorization URL for auth code flow |
|
|
384
|
+
| `exchangeAuthorizationCode(code)` | Exchange an auth code for tokens |
|
|
385
|
+
| `getClientCredentialsToken()` | Obtain tokens via client credentials flow |
|
|
386
|
+
| `refreshAccessToken()` | Refresh using the stored refresh token |
|
|
387
|
+
| `setTokens(access, refresh?, expiresIn?)` | Set tokens obtained externally |
|
|
388
|
+
| `isTokenValid()` | Check if current token is valid (with buffer) |
|
|
389
|
+
|
|
390
|
+
## Dependencies
|
|
391
|
+
|
|
392
|
+
This package depends on:
|
|
393
|
+
|
|
394
|
+
- [@memberjunction/global](../../MJGlobal/README.md) — ClassFactory and `@RegisterClass` decorator
|
|
395
|
+
- [@memberjunction/core](../../MJCore/README.md) — `Metadata`, `RunView`, `BaseEntity`, logging utilities
|
|
396
|
+
- [@memberjunction/actions-base](../Base/README.md) — Shared types (`ActionEngineBase`, `RunActionParams`, `ActionResult`, etc.)
|
|
397
|
+
- [@memberjunction/core-entities](../../MJCoreEntities/README.md) — Generated entity classes (`ActionExecutionLogEntity`, `ActionFilterEntity`, etc.)
|
|
398
|
+
- [@memberjunction/ai](../../AI/Core/README.md) — AI model integration
|
|
399
|
+
- [@memberjunction/ai-core-plus](../../AI/CorePlus/README.md) — Extended AI utilities
|
|
400
|
+
- [@memberjunction/aiengine](../../AI/Engine/README.md) — AI engine orchestration
|
|
401
|
+
- [@memberjunction/ai-prompts](../../AI/Prompts/README.md) — AI prompt execution
|
|
402
|
+
|
|
403
|
+
## Related Packages
|
|
404
|
+
|
|
405
|
+
- [@memberjunction/actions-base](../Base/README.md) — Shared types and base classes used by both client and server
|
|
406
|
+
- [CoreActions](../CoreActions/) — Built-in action implementations (Create Record, generated actions, etc.)
|
|
407
|
+
- [ScheduledActions](../ScheduledActions/) — Scheduled action execution support
|
|
408
|
+
- [ApolloEnrichment](../ApolloEnrichment/) — Apollo data enrichment actions
|
|
409
|
+
- [ContentAutotag](../ContentAutotag/) — Content auto-tagging actions
|
|
410
|
+
- [CodeExecution](../CodeExecution/) — Dynamic code execution actions
|
|
411
|
+
|
|
412
|
+
For the Actions system philosophy and development guide, see the [Actions CLAUDE.md](../CLAUDE.md).
|
|
413
|
+
|
|
414
|
+
## Contributing
|
|
415
|
+
|
|
416
|
+
See the [MemberJunction Contributing Guide](../../../CONTRIBUTING.md) for development setup and guidelines.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/actions",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.1.0",
|
|
5
5
|
"description": "Main library for MemberJunction Actions. This library is only intended to be imported on the server side.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
"typescript": "^5.9.3"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@memberjunction/global": "4.
|
|
24
|
-
"@memberjunction/core": "4.
|
|
25
|
-
"@memberjunction/actions-base": "4.
|
|
26
|
-
"@memberjunction/core-entities": "4.
|
|
27
|
-
"@memberjunction/ai": "4.
|
|
28
|
-
"@memberjunction/ai-core-plus": "4.
|
|
29
|
-
"@memberjunction/aiengine": "4.
|
|
30
|
-
"@memberjunction/ai-prompts": "4.
|
|
31
|
-
"@memberjunction/doc-utils": "4.
|
|
23
|
+
"@memberjunction/global": "4.1.0",
|
|
24
|
+
"@memberjunction/core": "4.1.0",
|
|
25
|
+
"@memberjunction/actions-base": "4.1.0",
|
|
26
|
+
"@memberjunction/core-entities": "4.1.0",
|
|
27
|
+
"@memberjunction/ai": "4.1.0",
|
|
28
|
+
"@memberjunction/ai-core-plus": "4.1.0",
|
|
29
|
+
"@memberjunction/aiengine": "4.1.0",
|
|
30
|
+
"@memberjunction/ai-prompts": "4.1.0",
|
|
31
|
+
"@memberjunction/doc-utils": "4.1.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
package/readme.md
CHANGED
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
# @memberjunction/actions
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
The Actions Framework is a powerful system for creating reusable, parameterized business logic that can be executed on demand. Actions are "verbs" in the MemberJunction ecosystem - they perform specific tasks and can be triggered through various mechanisms including entity events, API calls, or scheduled jobs.
|
|
8
|
-
|
|
9
|
-
**IMPORTANT:** This library should only be imported on the server side.
|
|
10
|
-
|
|
11
|
-
## Key Features
|
|
12
|
-
|
|
13
|
-
- **Action Engine**: Central execution engine for running actions with parameter validation, filtering, and logging
|
|
14
|
-
- **Entity Actions**: Actions that can be triggered on entity lifecycle events (create, update, delete)
|
|
15
|
-
- **Code Generation**: AI-powered automatic code generation for actions based on natural language prompts
|
|
16
|
-
- **Action Filters**: Pre-execution filters to control when actions should run
|
|
17
|
-
- **Transaction Support**: Built-in transaction management for complex multi-step operations
|
|
18
|
-
- **Comprehensive Logging**: Automatic logging of all action executions with parameters and results
|
|
3
|
+
Server-side action execution engine for MemberJunction. This package provides the runtime infrastructure for executing actions — including input validation, filter evaluation, ClassFactory-based action dispatch, execution logging, OAuth token management, and entity-bound action invocation. It is intended for server-side use only.
|
|
19
4
|
|
|
20
5
|
## Installation
|
|
21
6
|
|
|
@@ -23,502 +8,409 @@ The Actions Framework is a powerful system for creating reusable, parameterized
|
|
|
23
8
|
npm install @memberjunction/actions
|
|
24
9
|
```
|
|
25
10
|
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
This package depends on several other MemberJunction packages:
|
|
29
|
-
- `@memberjunction/global` - Global utilities and class factory
|
|
30
|
-
- `@memberjunction/core` - Core MJ functionality and base classes
|
|
31
|
-
- `@memberjunction/actions-base` - Base types and interfaces for actions
|
|
32
|
-
- `@memberjunction/core-entities` - Entity definitions
|
|
33
|
-
- `@memberjunction/ai` - AI integration capabilities
|
|
34
|
-
- `@memberjunction/aiengine` - AI engine functionality
|
|
35
|
-
- `@memberjunction/doc-utils` - Documentation utilities
|
|
36
|
-
|
|
37
|
-
## Usage
|
|
38
|
-
|
|
39
|
-
### Creating a Custom Action
|
|
11
|
+
## Overview
|
|
40
12
|
|
|
41
|
-
|
|
13
|
+
The Actions Engine sits between external consumers (AI agents, workflows, APIs) and the actual action implementations registered via `@RegisterClass`. It handles the full execution lifecycle: validating inputs, running pre-execution filters, dispatching to the correct `BaseAction` subclass via ClassFactory, and logging results.
|
|
14
|
+
|
|
15
|
+
The package contains two subsystems:
|
|
16
|
+
|
|
17
|
+
- **Generic Action Engine** — Executes standalone actions with validation, filtering, and logging
|
|
18
|
+
- **Entity Action Engine** — Executes actions bound to entity records, supporting CRUD lifecycle hooks, list/view batch operations, and record validation
|
|
19
|
+
|
|
20
|
+
```mermaid
|
|
21
|
+
flowchart TD
|
|
22
|
+
subgraph Consumers["External Consumers"]
|
|
23
|
+
Agent["AI Agents"]
|
|
24
|
+
WF["Workflows"]
|
|
25
|
+
API["GraphQL API"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
subgraph Engine["@memberjunction/actions"]
|
|
29
|
+
AES["ActionEngineServer"]
|
|
30
|
+
EAES["EntityActionEngineServer"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
subgraph Pipeline["Execution Pipeline"]
|
|
34
|
+
Validate["Validate Inputs"]
|
|
35
|
+
Filter["Run Filters"]
|
|
36
|
+
Dispatch["ClassFactory Dispatch"]
|
|
37
|
+
Log["Execution Logging"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
subgraph Actions["Registered Actions"]
|
|
41
|
+
BA["BaseAction Subclasses"]
|
|
42
|
+
OAuth["BaseOAuthAction Subclasses"]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Consumers --> Engine
|
|
46
|
+
AES --> Validate --> Filter --> Dispatch --> Log
|
|
47
|
+
Dispatch --> BA
|
|
48
|
+
Dispatch --> OAuth
|
|
49
|
+
EAES --> AES
|
|
50
|
+
|
|
51
|
+
style Consumers fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
52
|
+
style Engine fill:#7c5295,stroke:#563a6b,color:#fff
|
|
53
|
+
style Pipeline fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
54
|
+
style Actions fill:#b8762f,stroke:#8a5722,color:#fff
|
|
55
|
+
```
|
|
42
56
|
|
|
43
|
-
|
|
44
|
-
import { BaseAction } from '@memberjunction/actions';
|
|
45
|
-
import { ActionResultSimple, RunActionParams } from '@memberjunction/actions-base';
|
|
46
|
-
import { RegisterClass } from '@memberjunction/global';
|
|
57
|
+
## Key Features
|
|
47
58
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// Return the result
|
|
58
|
-
return {
|
|
59
|
-
Success: true,
|
|
60
|
-
ResultCode: 'SUCCESS',
|
|
61
|
-
Message: 'Action completed successfully',
|
|
62
|
-
Params: params.Params // Include any output parameters
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private async performBusinessLogic(value: any): Promise<any> {
|
|
67
|
-
// Your custom logic here
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
```
|
|
59
|
+
- **Action Execution Pipeline** — Validates inputs, evaluates filters, dispatches via ClassFactory, and logs all executions
|
|
60
|
+
- **ClassFactory Dispatch** — Looks up `BaseAction` subclasses by `DriverClass` or action name at runtime
|
|
61
|
+
- **Pre-Execution Filters** — `BaseActionFilter` subclasses can gate whether an action should run
|
|
62
|
+
- **Execution Logging** — Automatic start/end logging to `Action Execution Logs` entity with params and result codes
|
|
63
|
+
- **OAuth Token Management** — `BaseOAuthAction` provides token lifecycle (refresh, retry on auth failure, persistence)
|
|
64
|
+
- **OAuth2Manager** — Standalone OAuth2 client supporting authorization code, client credentials, and refresh token flows
|
|
65
|
+
- **Entity Action Invocation** — Bind actions to entity CRUD lifecycle events (BeforeCreate, AfterUpdate, etc.)
|
|
66
|
+
- **Batch Entity Actions** — Run actions against Lists or Views of records with consolidated results
|
|
67
|
+
- **Script Evaluation** — Entity action params support runtime script evaluation with entity context
|
|
72
68
|
|
|
73
|
-
|
|
69
|
+
## Usage
|
|
74
70
|
|
|
75
|
-
|
|
71
|
+
### Running a Standalone Action
|
|
76
72
|
|
|
77
73
|
```typescript
|
|
78
74
|
import { ActionEngineServer } from '@memberjunction/actions';
|
|
79
|
-
import { RunActionParams } from '@memberjunction/actions-base';
|
|
80
75
|
|
|
81
|
-
//
|
|
82
|
-
|
|
76
|
+
// Configure the engine (typically done once at startup)
|
|
77
|
+
await ActionEngineServer.Instance.Config(false, contextUser);
|
|
83
78
|
|
|
84
|
-
//
|
|
85
|
-
|
|
79
|
+
// Find the action by name
|
|
80
|
+
const action = ActionEngineServer.Instance.Actions.find(a => a.Name === 'Send Email');
|
|
86
81
|
|
|
87
|
-
//
|
|
88
|
-
const result = await
|
|
89
|
-
|
|
90
|
-
ContextUser:
|
|
82
|
+
// Execute it
|
|
83
|
+
const result = await ActionEngineServer.Instance.RunAction({
|
|
84
|
+
Action: action,
|
|
85
|
+
ContextUser: contextUser,
|
|
91
86
|
Params: [
|
|
92
|
-
{ Name: '
|
|
93
|
-
|
|
87
|
+
{ Name: 'to', Value: 'user@example.com', Type: 'Input' },
|
|
88
|
+
{ Name: 'subject', Value: 'Hello', Type: 'Input' },
|
|
89
|
+
{ Name: 'body', Value: 'Message content', Type: 'Input' }
|
|
90
|
+
],
|
|
91
|
+
Filters: []
|
|
94
92
|
});
|
|
95
93
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
Params: [
|
|
102
|
-
{ Name: 'InputParam', Value: 'some value', Type: 'string' }
|
|
103
|
-
]
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const result = await engine.RunAction(params);
|
|
94
|
+
if (result.Success) {
|
|
95
|
+
console.log('Action completed:', result.Message);
|
|
96
|
+
} else {
|
|
97
|
+
console.error('Action failed:', result.Message);
|
|
98
|
+
}
|
|
107
99
|
```
|
|
108
100
|
|
|
109
|
-
###
|
|
101
|
+
### Creating a Custom Action
|
|
110
102
|
|
|
111
|
-
|
|
103
|
+
All actions extend `BaseAction` and implement `InternalRunAction`. Register them with `@RegisterClass` so the engine can discover them via ClassFactory:
|
|
112
104
|
|
|
113
105
|
```typescript
|
|
114
|
-
import { BaseAction } from '@memberjunction/actions';
|
|
115
|
-
import { ActionResultSimple, RunActionParams } from '@memberjunction/actions-base';
|
|
116
106
|
import { RegisterClass } from '@memberjunction/global';
|
|
107
|
+
import { BaseAction } from '@memberjunction/actions';
|
|
108
|
+
import { RunActionParams, ActionResultSimple } from '@memberjunction/actions-base';
|
|
117
109
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
timeout: number;
|
|
123
|
-
retryCount: number;
|
|
124
|
-
}
|
|
110
|
+
@RegisterClass(BaseAction, 'My Custom Action')
|
|
111
|
+
export class MyCustomAction extends BaseAction {
|
|
112
|
+
protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> {
|
|
113
|
+
const inputValue = params.Params.find(p => p.Name === 'input')?.Value;
|
|
125
114
|
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
export class APICallAction extends BaseAction {
|
|
129
|
-
protected async InternalRunAction(params: RunActionParams<APIContext>): Promise<ActionResultSimple> {
|
|
130
|
-
// Access typed context through params
|
|
131
|
-
const endpoint = params.Context?.apiEndpoint;
|
|
132
|
-
const apiKey = params.Context?.apiKey;
|
|
133
|
-
const timeout = params.Context?.timeout || 30000;
|
|
134
|
-
|
|
135
|
-
if (!endpoint || !apiKey) {
|
|
136
|
-
return {
|
|
137
|
-
Success: false,
|
|
138
|
-
ResultCode: 'MISSING_CONTEXT',
|
|
139
|
-
Message: 'API endpoint and key are required in context'
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Use context for API call
|
|
144
|
-
const requestData = params.Params.find(p => p.Name === 'RequestData')?.Value;
|
|
145
|
-
|
|
146
|
-
try {
|
|
147
|
-
const response = await this.callAPI(endpoint, apiKey, requestData, timeout);
|
|
148
|
-
|
|
149
|
-
// Set output parameter
|
|
150
|
-
const outputParam = params.Params.find(p => p.Name === 'ResponseData');
|
|
151
|
-
if (outputParam) {
|
|
152
|
-
outputParam.Value = response;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
Success: true,
|
|
157
|
-
ResultCode: 'SUCCESS',
|
|
158
|
-
Message: 'API call completed successfully',
|
|
159
|
-
Params: params.Params
|
|
160
|
-
};
|
|
161
|
-
} catch (error) {
|
|
162
|
-
return {
|
|
163
|
-
Success: false,
|
|
164
|
-
ResultCode: 'API_ERROR',
|
|
165
|
-
Message: error.message
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
private async callAPI(endpoint: string, apiKey: string, data: any, timeout: number): Promise<any> {
|
|
171
|
-
// Implementation details
|
|
172
|
-
const controller = new AbortController();
|
|
173
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
const response = await fetch(endpoint, {
|
|
177
|
-
method: 'POST',
|
|
178
|
-
headers: {
|
|
179
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
180
|
-
'Content-Type': 'application/json'
|
|
181
|
-
},
|
|
182
|
-
body: JSON.stringify(data),
|
|
183
|
-
signal: controller.signal
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
clearTimeout(timeoutId);
|
|
187
|
-
return await response.json();
|
|
188
|
-
} catch (error) {
|
|
189
|
-
clearTimeout(timeoutId);
|
|
190
|
-
throw error;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
```
|
|
115
|
+
// Your action logic here
|
|
116
|
+
const result = await this.doWork(inputValue);
|
|
195
117
|
|
|
196
|
-
|
|
118
|
+
return {
|
|
119
|
+
Success: true,
|
|
120
|
+
ResultCode: 'SUCCESS',
|
|
121
|
+
Message: `Processed: ${result}`
|
|
122
|
+
};
|
|
123
|
+
}
|
|
197
124
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// Define context type
|
|
203
|
-
interface APIContext {
|
|
204
|
-
apiEndpoint: string;
|
|
205
|
-
apiKey: string;
|
|
206
|
-
timeout: number;
|
|
207
|
-
retryCount: number;
|
|
125
|
+
private async doWork(input: string): Promise<string> {
|
|
126
|
+
// Delegate to service classes for real logic
|
|
127
|
+
return `Done with ${input}`;
|
|
128
|
+
}
|
|
208
129
|
}
|
|
209
|
-
|
|
210
|
-
// Configure parameters with context
|
|
211
|
-
const params = new RunActionParams<APIContext>();
|
|
212
|
-
params.Action = apiCallAction;
|
|
213
|
-
params.ContextUser = currentUser;
|
|
214
|
-
params.Params = [
|
|
215
|
-
{ Name: 'RequestData', Value: { orderId: '12345' }, Type: 'Input' },
|
|
216
|
-
{ Name: 'ResponseData', Value: null, Type: 'Output' }
|
|
217
|
-
];
|
|
218
|
-
|
|
219
|
-
// Set runtime context
|
|
220
|
-
params.Context = {
|
|
221
|
-
apiEndpoint: process.env.API_ENDPOINT,
|
|
222
|
-
apiKey: process.env.API_KEY,
|
|
223
|
-
timeout: 10000,
|
|
224
|
-
retryCount: 3
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
// Execute with typed context
|
|
228
|
-
const result = await ActionEngineServer.Instance.RunAction(params);
|
|
229
130
|
```
|
|
230
131
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
**Use Context for:**
|
|
234
|
-
- Environment-specific configuration (API endpoints, service URLs)
|
|
235
|
-
- Runtime credentials (API keys, tokens)
|
|
236
|
-
- Session information (user preferences, correlation IDs)
|
|
237
|
-
- Feature flags and toggles
|
|
238
|
-
- Timeout and retry policies
|
|
239
|
-
|
|
240
|
-
**Use Parameters for:**
|
|
241
|
-
- Business data (customer ID, order details)
|
|
242
|
-
- Action-specific inputs (email content, calculation values)
|
|
243
|
-
- Data that should be logged and audited
|
|
244
|
-
- Values that need to be stored in the database
|
|
245
|
-
- Output values that other actions may depend on
|
|
246
|
-
|
|
247
|
-
The context is particularly useful when actions are executed from AI agents, as it allows the agent to pass runtime information down through the entire execution hierarchy without modifying the action's formal parameter structure.
|
|
132
|
+
### Creating an OAuth-Authenticated Action
|
|
248
133
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
Entity Actions are triggered automatically during entity lifecycle events. To work with entity actions, use the `EntityActionEngineServer`:
|
|
134
|
+
For actions that need to call external APIs with OAuth2 credentials:
|
|
252
135
|
|
|
253
136
|
```typescript
|
|
254
|
-
import {
|
|
255
|
-
import {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
137
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
138
|
+
import { BaseOAuthAction } from '@memberjunction/actions';
|
|
139
|
+
import { RunActionParams, ActionResultSimple } from '@memberjunction/actions-base';
|
|
140
|
+
|
|
141
|
+
@RegisterClass(BaseAction, 'Fetch External Data')
|
|
142
|
+
export class FetchExternalDataAction extends BaseOAuthAction {
|
|
143
|
+
protected async refreshAccessToken(): Promise<void> {
|
|
144
|
+
// Platform-specific token refresh logic
|
|
145
|
+
const response = await fetch('https://api.example.com/oauth/token', {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
body: new URLSearchParams({
|
|
148
|
+
grant_type: 'refresh_token',
|
|
149
|
+
refresh_token: this.getRefreshToken(),
|
|
150
|
+
})
|
|
151
|
+
});
|
|
152
|
+
const data = await response.json();
|
|
153
|
+
await this.updateStoredTokens(data.access_token, data.refresh_token, data.expires_in);
|
|
154
|
+
}
|
|
269
155
|
|
|
270
|
-
|
|
156
|
+
protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> {
|
|
157
|
+
const companyIntegrationId = params.Params.find(
|
|
158
|
+
p => p.Name === 'CompanyIntegrationID'
|
|
159
|
+
)?.Value as string;
|
|
271
160
|
|
|
272
|
-
|
|
161
|
+
// Initialize OAuth (loads tokens, refreshes if expired)
|
|
162
|
+
if (!await this.initializeOAuth(companyIntegrationId)) {
|
|
163
|
+
return this.handleOAuthError(new Error('OAuth initialization failed'));
|
|
164
|
+
}
|
|
273
165
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
166
|
+
// Make authenticated request with automatic retry on 401
|
|
167
|
+
const data = await this.makeAuthenticatedRequest(async (token) => {
|
|
168
|
+
const res = await fetch('https://api.example.com/data', {
|
|
169
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
170
|
+
});
|
|
171
|
+
return res.json();
|
|
172
|
+
});
|
|
279
173
|
|
|
280
|
-
|
|
281
|
-
export class MyCustomFilter extends BaseActionFilter {
|
|
282
|
-
protected async InternalRun(
|
|
283
|
-
params: RunActionParams,
|
|
284
|
-
filter: ActionFilterEntity
|
|
285
|
-
): Promise<boolean> {
|
|
286
|
-
// Implement your filter logic
|
|
287
|
-
// Return true to allow action execution, false to skip
|
|
288
|
-
return params.ContextUser.IsActive === true;
|
|
174
|
+
return { Success: true, ResultCode: 'SUCCESS', Message: JSON.stringify(data) };
|
|
289
175
|
}
|
|
290
176
|
}
|
|
291
177
|
```
|
|
292
178
|
|
|
293
|
-
|
|
179
|
+
### Using OAuth2Manager Directly
|
|
294
180
|
|
|
295
|
-
|
|
181
|
+
For standalone OAuth2 token management outside the action framework:
|
|
296
182
|
|
|
297
|
-
|
|
183
|
+
```typescript
|
|
184
|
+
import { OAuth2Manager } from '@memberjunction/actions';
|
|
298
185
|
|
|
299
|
-
|
|
186
|
+
const oauth = new OAuth2Manager({
|
|
187
|
+
clientId: 'your-client-id',
|
|
188
|
+
clientSecret: 'your-client-secret',
|
|
189
|
+
tokenEndpoint: 'https://api.example.com/oauth/token',
|
|
190
|
+
scopes: ['read', 'write'],
|
|
191
|
+
onTokenUpdate: async (tokens) => {
|
|
192
|
+
// Persist tokens to your storage
|
|
193
|
+
await saveTokens(tokens);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
300
196
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
- `RunActionByID(params: RunActionByNameParams): Promise<ActionResult>` - Convenience method to run an action by its ID
|
|
304
|
-
- `Config(forceRefresh?: boolean, contextUser?: UserInfo): Promise<void>` - Configures the engine (inherited from base)
|
|
197
|
+
// Get a valid token (auto-refreshes if expired)
|
|
198
|
+
const token = await oauth.getAccessToken();
|
|
305
199
|
|
|
306
|
-
|
|
200
|
+
// Or use client credentials flow
|
|
201
|
+
const tokenData = await oauth.getClientCredentialsToken();
|
|
202
|
+
```
|
|
307
203
|
|
|
308
|
-
|
|
204
|
+
## Architecture
|
|
205
|
+
|
|
206
|
+
### Action Execution Pipeline
|
|
207
|
+
|
|
208
|
+
The `ActionEngineServer.RunAction()` method follows this sequence:
|
|
209
|
+
|
|
210
|
+
```mermaid
|
|
211
|
+
sequenceDiagram
|
|
212
|
+
participant Caller
|
|
213
|
+
participant Engine as ActionEngineServer
|
|
214
|
+
participant Filter as BaseActionFilter
|
|
215
|
+
participant CF as ClassFactory
|
|
216
|
+
participant Action as BaseAction Subclass
|
|
217
|
+
participant Log as Execution Log
|
|
218
|
+
|
|
219
|
+
Caller->>Engine: RunAction(params)
|
|
220
|
+
Engine->>Engine: ValidateInputs(params)
|
|
221
|
+
alt Validation fails
|
|
222
|
+
Engine->>Log: StartAndEndActionLog()
|
|
223
|
+
Engine-->>Caller: {Success: false}
|
|
224
|
+
end
|
|
225
|
+
Engine->>Filter: RunFilters(params)
|
|
226
|
+
alt Filters block execution
|
|
227
|
+
Engine->>Log: StartAndEndActionLog()
|
|
228
|
+
Engine-->>Caller: {Success: true, "Filters blocked"}
|
|
229
|
+
end
|
|
230
|
+
Engine->>Log: StartActionLog()
|
|
231
|
+
Engine->>CF: CreateInstance(BaseAction, driverClass)
|
|
232
|
+
CF-->>Engine: action instance
|
|
233
|
+
Engine->>Action: Run(params)
|
|
234
|
+
Action->>Action: InternalRunAction(params)
|
|
235
|
+
Action-->>Engine: ActionResultSimple
|
|
236
|
+
Engine->>Log: EndActionLog()
|
|
237
|
+
Engine-->>Caller: ActionResult
|
|
238
|
+
```
|
|
309
239
|
|
|
310
|
-
|
|
311
|
-
- `Run(params: RunActionParams): Promise<ActionResultSimple>` - Public method called by the engine
|
|
312
|
-
- `InternalRunAction(params: RunActionParams): Promise<ActionResultSimple>` - Abstract method to implement action logic
|
|
240
|
+
### Entity Action Invocation
|
|
313
241
|
|
|
314
|
-
|
|
242
|
+
Entity actions are bound to entity lifecycle events. The `EntityActionEngineServer` delegates to invocation-type-specific handlers via ClassFactory:
|
|
315
243
|
|
|
316
|
-
|
|
244
|
+
```mermaid
|
|
245
|
+
classDiagram
|
|
246
|
+
class EntityActionInvocationBase {
|
|
247
|
+
<<abstract>>
|
|
248
|
+
+InvokeAction(params) EntityActionResult
|
|
249
|
+
+MapParams(params, entityActionParams, entity) ActionParam[]
|
|
250
|
+
+SafeEvalScript(id, script, entity) any
|
|
251
|
+
}
|
|
317
252
|
|
|
318
|
-
|
|
319
|
-
|
|
253
|
+
class SingleRecord {
|
|
254
|
+
+InvokeAction(params) EntityActionResult
|
|
255
|
+
+ValidateParams(params) boolean
|
|
256
|
+
}
|
|
320
257
|
|
|
321
|
-
|
|
258
|
+
class MultipleRecords {
|
|
259
|
+
+InvokeAction(params) EntityActionResult
|
|
260
|
+
#GetRecordList() BaseEntity[]
|
|
261
|
+
}
|
|
322
262
|
|
|
323
|
-
|
|
263
|
+
class Validate {
|
|
264
|
+
+InvokeAction(params) EntityActionResult
|
|
265
|
+
}
|
|
324
266
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
267
|
+
EntityActionInvocationBase <|-- SingleRecord
|
|
268
|
+
EntityActionInvocationBase <|-- MultipleRecords
|
|
269
|
+
SingleRecord <|-- Validate
|
|
328
270
|
|
|
329
|
-
|
|
271
|
+
note for SingleRecord "Registered for: Read, BeforeCreate,\nBeforeUpdate, BeforeDelete, AfterCreate,\nAfterUpdate, AfterDelete, SingleRecord"
|
|
272
|
+
note for MultipleRecords "Registered for: List, View"
|
|
273
|
+
note for Validate "Registered for: Validate"
|
|
274
|
+
```
|
|
330
275
|
|
|
331
|
-
|
|
276
|
+
### Class Hierarchy
|
|
332
277
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
278
|
+
```mermaid
|
|
279
|
+
classDiagram
|
|
280
|
+
class BaseAction {
|
|
281
|
+
<<abstract>>
|
|
282
|
+
+Run(params) ActionResultSimple
|
|
283
|
+
#InternalRunAction(params)* ActionResultSimple
|
|
284
|
+
}
|
|
339
285
|
|
|
340
|
-
|
|
286
|
+
class BaseOAuthAction {
|
|
287
|
+
<<abstract>>
|
|
288
|
+
#initializeOAuth(id) boolean
|
|
289
|
+
#getAccessToken() string
|
|
290
|
+
#makeAuthenticatedRequest(fn) T
|
|
291
|
+
#refreshAccessToken()* void
|
|
292
|
+
}
|
|
341
293
|
|
|
342
|
-
|
|
294
|
+
class BaseActionFilter {
|
|
295
|
+
<<abstract>>
|
|
296
|
+
+Run(params, filter) boolean
|
|
297
|
+
#InternalRun(params, filter)* boolean
|
|
298
|
+
}
|
|
343
299
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
300
|
+
class ActionEngineServer {
|
|
301
|
+
+RunAction(params) ActionResult
|
|
302
|
+
#ValidateInputs(params) boolean
|
|
303
|
+
#RunFilters(params) boolean
|
|
304
|
+
#InternalRunAction(params) ActionResult
|
|
305
|
+
}
|
|
350
306
|
|
|
351
|
-
|
|
307
|
+
class EntityActionEngineServer {
|
|
308
|
+
+RunEntityAction(params) EntityActionResult
|
|
309
|
+
}
|
|
352
310
|
|
|
353
|
-
|
|
311
|
+
class OAuth2Manager {
|
|
312
|
+
+getAccessToken() string
|
|
313
|
+
+getAuthorizationUrl() string
|
|
314
|
+
+exchangeAuthorizationCode(code) OAuth2TokenData
|
|
315
|
+
+getClientCredentialsToken() OAuth2TokenData
|
|
316
|
+
+refreshAccessToken() OAuth2TokenData
|
|
317
|
+
}
|
|
354
318
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
- `BeforeDelete` - Before deleting a record
|
|
362
|
-
- `AfterDelete` - After deleting a record
|
|
319
|
+
BaseAction <|-- BaseOAuthAction
|
|
320
|
+
ActionEngineServer --> BaseAction : dispatches to
|
|
321
|
+
ActionEngineServer --> BaseActionFilter : evaluates
|
|
322
|
+
EntityActionEngineServer --> ActionEngineServer : delegates to
|
|
323
|
+
BaseOAuthAction --> OAuth2Manager : can use
|
|
324
|
+
```
|
|
363
325
|
|
|
364
|
-
|
|
365
|
-
- `List` - Actions operating on a list of records
|
|
366
|
-
- `View` - Actions operating on records from a view
|
|
326
|
+
## API Reference
|
|
367
327
|
|
|
368
|
-
###
|
|
369
|
-
- `Validate` - Special invocation type for validation logic
|
|
328
|
+
### ActionEngineServer
|
|
370
329
|
|
|
371
|
-
|
|
330
|
+
Singleton engine that executes actions. Access via `ActionEngineServer.Instance`.
|
|
372
331
|
|
|
373
|
-
|
|
332
|
+
| Method | Description |
|
|
333
|
+
|--------|-------------|
|
|
334
|
+
| `Config(forceRefresh, contextUser)` | Initialize/refresh the engine's action and filter metadata |
|
|
335
|
+
| `RunAction(params)` | Execute an action through the full pipeline (validate, filter, dispatch, log) |
|
|
374
336
|
|
|
375
|
-
|
|
376
|
-
2. **Automatic Code Generation**: The system generates TypeScript code based on your prompt
|
|
377
|
-
3. **Code Validation**: Generated code is automatically validated and improved
|
|
378
|
-
4. **Library Management**: Automatic tracking and importing of required libraries
|
|
337
|
+
### BaseAction
|
|
379
338
|
|
|
380
|
-
|
|
381
|
-
```typescript
|
|
382
|
-
// In your database, create an Action record with:
|
|
383
|
-
// Name: "SendWelcomeEmail"
|
|
384
|
-
// Type: "Generated"
|
|
385
|
-
// UserPrompt: "Send a welcome email to a new user with their name and registration date"
|
|
339
|
+
Abstract base class for all action implementations.
|
|
386
340
|
|
|
387
|
-
|
|
388
|
-
|
|
341
|
+
| Method | Description |
|
|
342
|
+
|--------|-------------|
|
|
343
|
+
| `Run(params)` | Public entry point — calls `InternalRunAction` |
|
|
344
|
+
| `InternalRunAction(params)` | **Abstract** — implement your action logic here |
|
|
389
345
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
1. **Action Naming**: Use clear, descriptive names for actions (e.g., `SendInvoiceEmail`, `CalculateOrderTotal`)
|
|
393
|
-
|
|
394
|
-
2. **Parameter Design**: Design action parameters to be reusable and flexible:
|
|
395
|
-
```typescript
|
|
396
|
-
params: [
|
|
397
|
-
{ Name: 'EmailTemplate', Type: 'string', ValueType: 'Scalar' },
|
|
398
|
-
{ Name: 'RecipientUser', Type: 'User', ValueType: 'BaseEntity Sub-Class' },
|
|
399
|
-
{ Name: 'EmailSent', Type: 'boolean', ValueType: 'Scalar', IsInput: false }
|
|
400
|
-
]
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
3. **Error Handling**: Always include proper error handling in your actions:
|
|
404
|
-
```typescript
|
|
405
|
-
try {
|
|
406
|
-
// Action logic
|
|
407
|
-
return { Success: true, ResultCode: 'SUCCESS', Message: 'Completed' };
|
|
408
|
-
} catch (error) {
|
|
409
|
-
return { Success: false, ResultCode: 'ERROR', Message: error.message };
|
|
410
|
-
}
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
4. **Logging**: The framework automatically logs action executions, but include additional logging for debugging:
|
|
414
|
-
```typescript
|
|
415
|
-
import { LogError, LogStatus } from '@memberjunction/core';
|
|
416
|
-
|
|
417
|
-
LogStatus('Starting custom action processing...');
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
5. **Transaction Management**: Use transaction groups for multi-step operations:
|
|
421
|
-
```typescript
|
|
422
|
-
const tg = await metadata.CreateTransactionGroup();
|
|
423
|
-
try {
|
|
424
|
-
// Multiple operations
|
|
425
|
-
await tg.Submit();
|
|
426
|
-
} catch (error) {
|
|
427
|
-
// Automatic rollback on error
|
|
428
|
-
}
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
## Integration with Other MJ Packages
|
|
432
|
-
|
|
433
|
-
This package integrates seamlessly with:
|
|
434
|
-
|
|
435
|
-
- **@memberjunction/core**: Provides base entity functionality and metadata access
|
|
436
|
-
- **@memberjunction/ai**: Enables AI-powered code generation
|
|
437
|
-
- **@memberjunction/core-entities**: Provides strongly-typed entity classes
|
|
438
|
-
- **@memberjunction/global**: Manages class registration and instantiation
|
|
439
|
-
|
|
440
|
-
## OAuth2Manager (Server-Side Only)
|
|
441
|
-
|
|
442
|
-
The package includes a generic OAuth2 token manager for server-side integrations:
|
|
346
|
+
### BaseOAuthAction
|
|
443
347
|
|
|
444
|
-
|
|
445
|
-
import { OAuth2Manager } from '@memberjunction/actions';
|
|
348
|
+
Abstract base for actions requiring OAuth authentication. Extends `BaseAction`.
|
|
446
349
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
// Persist updated tokens to database
|
|
456
|
-
await saveTokens(tokens);
|
|
457
|
-
}
|
|
458
|
-
});
|
|
350
|
+
| Method | Description |
|
|
351
|
+
|--------|-------------|
|
|
352
|
+
| `initializeOAuth(companyIntegrationId)` | Load integration, check/refresh tokens |
|
|
353
|
+
| `getAccessToken()` | Get the current access token |
|
|
354
|
+
| `makeAuthenticatedRequest(fn)` | Execute a request with automatic retry on 401/403 |
|
|
355
|
+
| `refreshAccessToken()` | **Abstract** — implement platform-specific token refresh |
|
|
356
|
+
| `updateStoredTokens(access, refresh?, expiresIn?)` | Persist new tokens to the Company Integration entity |
|
|
357
|
+
| `handleOAuthError(error)` | Return a standardized error result for OAuth failures |
|
|
459
358
|
|
|
460
|
-
|
|
461
|
-
const authUrl = oauth.getAuthorizationUrl('random-state-string');
|
|
359
|
+
### BaseActionFilter
|
|
462
360
|
|
|
463
|
-
|
|
464
|
-
const tokens = await oauth.exchangeAuthorizationCode(code);
|
|
361
|
+
Abstract base for pre-execution filters.
|
|
465
362
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
363
|
+
| Method | Description |
|
|
364
|
+
|--------|-------------|
|
|
365
|
+
| `Run(params, filter)` | Public entry point — calls `InternalRun` |
|
|
366
|
+
| `InternalRun(params, filter)` | **Abstract** — implement filter logic, return `true` to allow execution |
|
|
469
367
|
|
|
470
|
-
|
|
471
|
-
- Multiple grant type support (authorization_code, client_credentials, refresh_token)
|
|
472
|
-
- Automatic token refresh before expiration
|
|
473
|
-
- Thread-safe token refresh (prevents concurrent requests)
|
|
474
|
-
- Token persistence callbacks
|
|
475
|
-
- Provider customization hooks for non-standard OAuth2 implementations
|
|
368
|
+
### EntityActionEngineServer
|
|
476
369
|
|
|
477
|
-
|
|
370
|
+
Singleton engine for entity-bound actions. Access via `EntityActionEngineServer.Instance`.
|
|
478
371
|
|
|
479
|
-
|
|
372
|
+
| Method | Description |
|
|
373
|
+
|--------|-------------|
|
|
374
|
+
| `RunEntityAction(params)` | Execute an entity action, dispatching to the correct invocation type handler |
|
|
480
375
|
|
|
481
|
-
###
|
|
376
|
+
### OAuth2Manager
|
|
482
377
|
|
|
483
|
-
|
|
378
|
+
Standalone OAuth2 token manager supporting multiple grant types.
|
|
484
379
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
// Custom filter logic
|
|
495
|
-
return super.RunFilters(params);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
```
|
|
380
|
+
| Method | Description |
|
|
381
|
+
|--------|-------------|
|
|
382
|
+
| `getAccessToken()` | Get a valid token, auto-refreshing if needed (thread-safe) |
|
|
383
|
+
| `getAuthorizationUrl(state?)` | Build the authorization URL for auth code flow |
|
|
384
|
+
| `exchangeAuthorizationCode(code)` | Exchange an auth code for tokens |
|
|
385
|
+
| `getClientCredentialsToken()` | Obtain tokens via client credentials flow |
|
|
386
|
+
| `refreshAccessToken()` | Refresh using the stored refresh token |
|
|
387
|
+
| `setTokens(access, refresh?, expiresIn?)` | Set tokens obtained externally |
|
|
388
|
+
| `isTokenValid()` | Check if current token is valid (with buffer) |
|
|
499
389
|
|
|
500
|
-
|
|
390
|
+
## Dependencies
|
|
501
391
|
|
|
502
|
-
|
|
392
|
+
This package depends on:
|
|
503
393
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
394
|
+
- [@memberjunction/global](../../MJGlobal/README.md) — ClassFactory and `@RegisterClass` decorator
|
|
395
|
+
- [@memberjunction/core](../../MJCore/README.md) — `Metadata`, `RunView`, `BaseEntity`, logging utilities
|
|
396
|
+
- [@memberjunction/actions-base](../Base/README.md) — Shared types (`ActionEngineBase`, `RunActionParams`, `ActionResult`, etc.)
|
|
397
|
+
- [@memberjunction/core-entities](../../MJCoreEntities/README.md) — Generated entity classes (`ActionExecutionLogEntity`, `ActionFilterEntity`, etc.)
|
|
398
|
+
- [@memberjunction/ai](../../AI/Core/README.md) — AI model integration
|
|
399
|
+
- [@memberjunction/ai-core-plus](../../AI/CorePlus/README.md) — Extended AI utilities
|
|
400
|
+
- [@memberjunction/aiengine](../../AI/Engine/README.md) — AI engine orchestration
|
|
401
|
+
- [@memberjunction/ai-prompts](../../AI/Prompts/README.md) — AI prompt execution
|
|
402
|
+
|
|
403
|
+
## Related Packages
|
|
514
404
|
|
|
515
|
-
|
|
405
|
+
- [@memberjunction/actions-base](../Base/README.md) — Shared types and base classes used by both client and server
|
|
406
|
+
- [CoreActions](../CoreActions/) — Built-in action implementations (Create Record, generated actions, etc.)
|
|
407
|
+
- [ScheduledActions](../ScheduledActions/) — Scheduled action execution support
|
|
408
|
+
- [ApolloEnrichment](../ApolloEnrichment/) — Apollo data enrichment actions
|
|
409
|
+
- [ContentAutotag](../ContentAutotag/) — Content auto-tagging actions
|
|
410
|
+
- [CodeExecution](../CodeExecution/) — Dynamic code execution actions
|
|
516
411
|
|
|
517
|
-
|
|
518
|
-
2. **Code Generation Fails**: Check that AI models are configured and API keys are set
|
|
519
|
-
3. **Filter Not Running**: Verify filter is associated with the action in metadata
|
|
520
|
-
4. **Entity Action Not Triggering**: Confirm invocation type matches the entity operation
|
|
412
|
+
For the Actions system philosophy and development guide, see the [Actions CLAUDE.md](../CLAUDE.md).
|
|
521
413
|
|
|
522
|
-
##
|
|
414
|
+
## Contributing
|
|
523
415
|
|
|
524
|
-
|
|
416
|
+
See the [MemberJunction Contributing Guide](../../../CONTRIBUTING.md) for development setup and guidelines.
|