@stonecrop/stonecrop 0.4.36 → 0.5.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 +92 -3
- package/dist/src/composable.d.ts +74 -8
- package/dist/src/composable.d.ts.map +1 -1
- package/dist/src/composable.js +348 -0
- package/dist/src/composables/operation-log.d.ts +136 -0
- package/dist/src/composables/operation-log.d.ts.map +1 -0
- package/dist/src/composables/operation-log.js +221 -0
- package/dist/src/doctype.d.ts +9 -1
- package/dist/src/doctype.d.ts.map +1 -1
- package/dist/{doctype.js → src/doctype.js} +9 -3
- package/dist/src/exceptions.d.ts +2 -3
- package/dist/src/exceptions.d.ts.map +1 -1
- package/dist/{exceptions.js → src/exceptions.js} +5 -11
- package/dist/src/field-triggers.d.ts +178 -0
- package/dist/src/field-triggers.d.ts.map +1 -0
- package/dist/src/field-triggers.js +564 -0
- package/dist/src/index.d.ts +12 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +18 -0
- package/dist/src/plugins/index.d.ts +11 -13
- package/dist/src/plugins/index.d.ts.map +1 -1
- package/dist/src/plugins/index.js +90 -0
- package/dist/src/registry.d.ts +9 -3
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/{registry.js → src/registry.js} +14 -1
- package/dist/src/stonecrop.d.ts +350 -114
- package/dist/src/stonecrop.d.ts.map +1 -1
- package/dist/src/stonecrop.js +251 -0
- package/dist/src/stores/hst.d.ts +157 -0
- package/dist/src/stores/hst.d.ts.map +1 -0
- package/dist/src/stores/hst.js +483 -0
- package/dist/src/stores/index.d.ts +5 -1
- package/dist/src/stores/index.d.ts.map +1 -1
- package/dist/{stores → src/stores}/index.js +4 -1
- package/dist/src/stores/operation-log.d.ts +268 -0
- package/dist/src/stores/operation-log.d.ts.map +1 -0
- package/dist/src/stores/operation-log.js +571 -0
- package/dist/src/types/field-triggers.d.ts +186 -0
- package/dist/src/types/field-triggers.d.ts.map +1 -0
- package/dist/src/types/field-triggers.js +4 -0
- package/dist/src/types/index.d.ts +13 -2
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/types/index.js +4 -0
- package/dist/src/types/operation-log.d.ts +165 -0
- package/dist/src/types/operation-log.d.ts.map +1 -0
- package/dist/src/types/registry.d.ts +11 -0
- package/dist/src/types/registry.d.ts.map +1 -0
- package/dist/src/types/registry.js +0 -0
- package/dist/stonecrop.d.ts +1555 -159
- package/dist/stonecrop.js +1974 -7035
- package/dist/stonecrop.js.map +1 -1
- package/dist/stonecrop.umd.cjs +4 -8
- package/dist/stonecrop.umd.cjs.map +1 -1
- package/dist/tests/setup.d.ts +5 -0
- package/dist/tests/setup.d.ts.map +1 -0
- package/dist/tests/setup.js +15 -0
- package/package.json +18 -16
- package/src/composable.ts +481 -33
- package/src/composables/operation-log.ts +254 -0
- package/src/doctype.ts +9 -3
- package/src/exceptions.ts +5 -12
- package/src/field-triggers.ts +671 -0
- package/src/index.ts +50 -4
- package/src/plugins/index.ts +70 -22
- package/src/registry.ts +18 -3
- package/src/stonecrop.ts +246 -151
- package/src/stores/hst.ts +703 -0
- package/src/stores/index.ts +6 -1
- package/src/stores/operation-log.ts +671 -0
- package/src/types/field-triggers.ts +201 -0
- package/src/types/index.ts +17 -6
- package/src/types/operation-log.ts +205 -0
- package/src/types/registry.ts +10 -0
- package/dist/composable.js +0 -51
- package/dist/index.js +0 -6
- package/dist/plugins/index.js +0 -49
- package/dist/src/stores/data.d.ts +0 -11
- package/dist/src/stores/data.d.ts.map +0 -1
- package/dist/stores/data.js +0 -7
- package/src/stores/data.ts +0 -8
- /package/dist/{types/index.js → src/types/operation-log.js} +0 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import { useOperationLogStore } from './stores/operation-log';
|
|
2
|
+
/**
|
|
3
|
+
* Field trigger execution engine integrated with Registry
|
|
4
|
+
* Singleton pattern following Registry implementation
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export class FieldTriggerEngine {
|
|
8
|
+
/**
|
|
9
|
+
* The root FieldTriggerEngine instance
|
|
10
|
+
*/
|
|
11
|
+
static _root;
|
|
12
|
+
options;
|
|
13
|
+
doctypeActions = new Map(); // doctype -> action/field -> functions
|
|
14
|
+
doctypeTransitions = new Map(); // doctype -> transition -> functions
|
|
15
|
+
fieldRollbackConfig = new Map(); // doctype -> field -> rollback enabled
|
|
16
|
+
globalActions = new Map(); // action name -> function
|
|
17
|
+
globalTransitionActions = new Map(); // transition action name -> function
|
|
18
|
+
/**
|
|
19
|
+
* Creates a new FieldTriggerEngine instance (singleton pattern)
|
|
20
|
+
* @param options - Configuration options for the field trigger engine
|
|
21
|
+
*/
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
if (FieldTriggerEngine._root) {
|
|
24
|
+
return FieldTriggerEngine._root;
|
|
25
|
+
}
|
|
26
|
+
FieldTriggerEngine._root = this;
|
|
27
|
+
this.options = {
|
|
28
|
+
defaultTimeout: options.defaultTimeout ?? 5000,
|
|
29
|
+
debug: options.debug ?? false,
|
|
30
|
+
enableRollback: options.enableRollback ?? true,
|
|
31
|
+
errorHandler: options.errorHandler,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Register a global action function
|
|
36
|
+
* @param name - The name of the action
|
|
37
|
+
* @param fn - The action function
|
|
38
|
+
*/
|
|
39
|
+
registerAction(name, fn) {
|
|
40
|
+
this.globalActions.set(name, fn);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Register a global XState transition action function
|
|
44
|
+
* @param name - The name of the transition action
|
|
45
|
+
* @param fn - The transition action function
|
|
46
|
+
*/
|
|
47
|
+
registerTransitionAction(name, fn) {
|
|
48
|
+
this.globalTransitionActions.set(name, fn);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Configure rollback behavior for a specific field trigger
|
|
52
|
+
* @param doctype - The doctype name
|
|
53
|
+
* @param fieldname - The field name
|
|
54
|
+
* @param enableRollback - Whether to enable rollback
|
|
55
|
+
*/
|
|
56
|
+
setFieldRollback(doctype, fieldname, enableRollback) {
|
|
57
|
+
if (!this.fieldRollbackConfig.has(doctype)) {
|
|
58
|
+
this.fieldRollbackConfig.set(doctype, new Map());
|
|
59
|
+
}
|
|
60
|
+
this.fieldRollbackConfig.get(doctype).set(fieldname, enableRollback);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get rollback configuration for a specific field trigger
|
|
64
|
+
*/
|
|
65
|
+
getFieldRollback(doctype, fieldname) {
|
|
66
|
+
return this.fieldRollbackConfig.get(doctype)?.get(fieldname);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Register actions from a doctype - both regular actions and field triggers
|
|
70
|
+
* Separates XState transitions (uppercase) from field triggers (lowercase)
|
|
71
|
+
* @param doctype - The doctype name
|
|
72
|
+
* @param actions - The actions to register (supports Immutable Map, Map, or plain object)
|
|
73
|
+
*/
|
|
74
|
+
registerDoctypeActions(doctype, actions) {
|
|
75
|
+
if (!actions)
|
|
76
|
+
return;
|
|
77
|
+
const actionMap = new Map();
|
|
78
|
+
const transitionMap = new Map();
|
|
79
|
+
// Convert from different Map types to regular Map
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
if (typeof actions.entrySeq === 'function') {
|
|
82
|
+
// Immutable Map
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
84
|
+
;
|
|
85
|
+
actions.entrySeq().forEach(([key, value]) => {
|
|
86
|
+
this.categorizeAction(key, value, actionMap, transitionMap);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else if (actions instanceof Map) {
|
|
90
|
+
// Regular Map
|
|
91
|
+
for (const [key, value] of actions) {
|
|
92
|
+
this.categorizeAction(key, value, actionMap, transitionMap);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (actions && typeof actions === 'object') {
|
|
96
|
+
// Plain object
|
|
97
|
+
Object.entries(actions).forEach(([key, value]) => {
|
|
98
|
+
this.categorizeAction(key, value, actionMap, transitionMap);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Always set the maps, even if empty
|
|
102
|
+
this.doctypeActions.set(doctype, actionMap);
|
|
103
|
+
this.doctypeTransitions.set(doctype, transitionMap);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Categorize an action as either a field trigger or XState transition
|
|
107
|
+
* Uses uppercase convention: UPPERCASE = transition, lowercase/mixed = field trigger
|
|
108
|
+
*/
|
|
109
|
+
categorizeAction(key, value, actionMap, transitionMap) {
|
|
110
|
+
// Check if the key is all uppercase (XState transition convention)
|
|
111
|
+
if (this.isTransitionKey(key)) {
|
|
112
|
+
transitionMap.set(key, value);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
actionMap.set(key, value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Determine if a key represents an XState transition
|
|
120
|
+
* Transitions are identified by being all uppercase
|
|
121
|
+
*/
|
|
122
|
+
isTransitionKey(key) {
|
|
123
|
+
// Must be all uppercase letters/numbers/underscores
|
|
124
|
+
return /^[A-Z0-9_]+$/.test(key) && key.length > 0;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Execute field triggers for a changed field
|
|
128
|
+
* @param context - The field change context
|
|
129
|
+
* @param options - Execution options (timeout and enableRollback)
|
|
130
|
+
*/
|
|
131
|
+
async executeFieldTriggers(context, options = {}) {
|
|
132
|
+
const { doctype, fieldname } = context;
|
|
133
|
+
const triggers = this.findFieldTriggers(doctype, fieldname);
|
|
134
|
+
if (triggers.length === 0) {
|
|
135
|
+
return {
|
|
136
|
+
path: context.path,
|
|
137
|
+
actionResults: [],
|
|
138
|
+
totalExecutionTime: 0,
|
|
139
|
+
allSucceeded: true,
|
|
140
|
+
stoppedOnError: false,
|
|
141
|
+
rolledBack: false,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const startTime = performance.now();
|
|
145
|
+
const actionResults = [];
|
|
146
|
+
let stoppedOnError = false;
|
|
147
|
+
let rolledBack = false;
|
|
148
|
+
let snapshot = undefined;
|
|
149
|
+
// Determine if rollback is enabled (priority: execution option > field config > global setting)
|
|
150
|
+
const fieldRollbackConfig = this.getFieldRollback(doctype, fieldname);
|
|
151
|
+
const rollbackEnabled = options.enableRollback ?? fieldRollbackConfig ?? this.options.enableRollback;
|
|
152
|
+
// Capture snapshot before executing actions if rollback is enabled
|
|
153
|
+
if (rollbackEnabled && context.store) {
|
|
154
|
+
snapshot = this.captureSnapshot(context);
|
|
155
|
+
}
|
|
156
|
+
// Execute actions sequentially
|
|
157
|
+
for (const actionName of triggers) {
|
|
158
|
+
try {
|
|
159
|
+
const actionResult = await this.executeAction(actionName, context, options.timeout);
|
|
160
|
+
actionResults.push(actionResult);
|
|
161
|
+
if (!actionResult.success) {
|
|
162
|
+
stoppedOnError = true;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const actionError = error instanceof Error ? error : new Error(String(error));
|
|
168
|
+
const errorResult = {
|
|
169
|
+
success: false,
|
|
170
|
+
error: actionError,
|
|
171
|
+
executionTime: 0,
|
|
172
|
+
action: actionName,
|
|
173
|
+
};
|
|
174
|
+
actionResults.push(errorResult);
|
|
175
|
+
stoppedOnError = true;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Perform rollback if enabled, errors occurred, and we have a snapshot
|
|
180
|
+
if (rollbackEnabled && stoppedOnError && snapshot && context.store) {
|
|
181
|
+
try {
|
|
182
|
+
this.restoreSnapshot(context, snapshot);
|
|
183
|
+
rolledBack = true;
|
|
184
|
+
}
|
|
185
|
+
catch (rollbackError) {
|
|
186
|
+
// eslint-disable-next-line no-console
|
|
187
|
+
console.error('[FieldTriggers] Rollback failed:', rollbackError);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const totalExecutionTime = performance.now() - startTime;
|
|
191
|
+
// Call global error handler if configured and errors occurred
|
|
192
|
+
const failedResults = actionResults.filter(r => !r.success);
|
|
193
|
+
if (failedResults.length > 0 && this.options.errorHandler) {
|
|
194
|
+
for (const failedResult of failedResults) {
|
|
195
|
+
try {
|
|
196
|
+
this.options.errorHandler(failedResult.error, context, failedResult.action);
|
|
197
|
+
}
|
|
198
|
+
catch (handlerError) {
|
|
199
|
+
// eslint-disable-next-line no-console
|
|
200
|
+
console.error('[FieldTriggers] Error in global error handler:', handlerError);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const result = {
|
|
205
|
+
path: context.path,
|
|
206
|
+
actionResults,
|
|
207
|
+
totalExecutionTime,
|
|
208
|
+
allSucceeded: actionResults.every(r => r.success),
|
|
209
|
+
stoppedOnError,
|
|
210
|
+
rolledBack,
|
|
211
|
+
snapshot: this.options.debug && rollbackEnabled ? snapshot : undefined, // Only include snapshot in debug mode if rollback is enabled
|
|
212
|
+
};
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Execute XState transition actions
|
|
217
|
+
* Similar to field triggers but specifically for FSM state transitions
|
|
218
|
+
* @param context - The transition change context
|
|
219
|
+
* @param options - Execution options (timeout)
|
|
220
|
+
*/
|
|
221
|
+
async executeTransitionActions(context, options = {}) {
|
|
222
|
+
const { doctype, transition } = context;
|
|
223
|
+
const transitionActions = this.findTransitionActions(doctype, transition);
|
|
224
|
+
if (transitionActions.length === 0) {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
const results = [];
|
|
228
|
+
// Execute transition actions sequentially
|
|
229
|
+
for (const actionName of transitionActions) {
|
|
230
|
+
try {
|
|
231
|
+
const actionResult = await this.executeTransitionAction(actionName, context, options.timeout);
|
|
232
|
+
results.push(actionResult);
|
|
233
|
+
if (!actionResult.success) {
|
|
234
|
+
// Stop on first error for transitions
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
const actionError = error instanceof Error ? error : new Error(String(error));
|
|
240
|
+
const errorResult = {
|
|
241
|
+
success: false,
|
|
242
|
+
error: actionError,
|
|
243
|
+
executionTime: 0,
|
|
244
|
+
action: actionName,
|
|
245
|
+
transition,
|
|
246
|
+
};
|
|
247
|
+
results.push(errorResult);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Call global error handler if configured and errors occurred
|
|
252
|
+
const failedResults = results.filter(r => !r.success);
|
|
253
|
+
if (failedResults.length > 0 && this.options.errorHandler) {
|
|
254
|
+
for (const failedResult of failedResults) {
|
|
255
|
+
try {
|
|
256
|
+
// Call with FieldChangeContext (base context type)
|
|
257
|
+
this.options.errorHandler(failedResult.error, context, failedResult.action);
|
|
258
|
+
}
|
|
259
|
+
catch (handlerError) {
|
|
260
|
+
// eslint-disable-next-line no-console
|
|
261
|
+
console.error('[FieldTriggers] Error in global error handler:', handlerError);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return results;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Find transition actions for a specific doctype and transition
|
|
269
|
+
*/
|
|
270
|
+
findTransitionActions(doctype, transition) {
|
|
271
|
+
const doctypeTransitions = this.doctypeTransitions.get(doctype);
|
|
272
|
+
if (!doctypeTransitions)
|
|
273
|
+
return [];
|
|
274
|
+
return doctypeTransitions.get(transition) || [];
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Execute a single transition action by name
|
|
278
|
+
*/
|
|
279
|
+
async executeTransitionAction(actionName, context, timeout) {
|
|
280
|
+
const startTime = performance.now();
|
|
281
|
+
const actionTimeout = timeout ?? this.options.defaultTimeout;
|
|
282
|
+
try {
|
|
283
|
+
// Look up action in transition-specific registry first, then fall back to global
|
|
284
|
+
let actionFn = this.globalTransitionActions.get(actionName);
|
|
285
|
+
// If not found in transition registry, try regular action registry
|
|
286
|
+
// This allows sharing actions between field triggers and transitions
|
|
287
|
+
if (!actionFn) {
|
|
288
|
+
const regularActionFn = this.globalActions.get(actionName);
|
|
289
|
+
if (regularActionFn) {
|
|
290
|
+
// Wrap regular action to accept TransitionChangeContext
|
|
291
|
+
actionFn = regularActionFn;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (!actionFn) {
|
|
295
|
+
throw new Error(`Transition action "${actionName}" not found in registry`);
|
|
296
|
+
}
|
|
297
|
+
await this.executeWithTimeout(actionFn, context, actionTimeout);
|
|
298
|
+
const executionTime = performance.now() - startTime;
|
|
299
|
+
return {
|
|
300
|
+
success: true,
|
|
301
|
+
executionTime,
|
|
302
|
+
action: actionName,
|
|
303
|
+
transition: context.transition,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
const executionTime = performance.now() - startTime;
|
|
308
|
+
const actionError = error instanceof Error ? error : new Error(String(error));
|
|
309
|
+
return {
|
|
310
|
+
success: false,
|
|
311
|
+
error: actionError,
|
|
312
|
+
executionTime,
|
|
313
|
+
action: actionName,
|
|
314
|
+
transition: context.transition,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Find field triggers for a specific doctype and field
|
|
320
|
+
* Field triggers are identified by keys that look like field paths (contain dots or match field names)
|
|
321
|
+
*/
|
|
322
|
+
findFieldTriggers(doctype, fieldname) {
|
|
323
|
+
const doctypeActions = this.doctypeActions.get(doctype);
|
|
324
|
+
if (!doctypeActions)
|
|
325
|
+
return [];
|
|
326
|
+
const triggers = [];
|
|
327
|
+
for (const [key, actionNames] of doctypeActions) {
|
|
328
|
+
// Check if this key is a field trigger pattern
|
|
329
|
+
if (this.isFieldTriggerKey(key, fieldname)) {
|
|
330
|
+
triggers.push(...actionNames);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return triggers;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Determine if an action key represents a field trigger
|
|
337
|
+
* Field triggers can be:
|
|
338
|
+
* - Exact field name match: "emailAddress"
|
|
339
|
+
* - Wildcard patterns: "emailAddress.*", "*.is_primary"
|
|
340
|
+
* - Nested field paths: "address.street", "contact.email"
|
|
341
|
+
*/
|
|
342
|
+
isFieldTriggerKey(key, fieldname) {
|
|
343
|
+
// Exact match
|
|
344
|
+
if (key === fieldname)
|
|
345
|
+
return true;
|
|
346
|
+
// Contains dots - likely a field path pattern
|
|
347
|
+
if (key.includes('.')) {
|
|
348
|
+
return this.matchFieldPattern(key, fieldname);
|
|
349
|
+
}
|
|
350
|
+
// Contains wildcards
|
|
351
|
+
if (key.includes('*')) {
|
|
352
|
+
return this.matchFieldPattern(key, fieldname);
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Match a field pattern against a field name
|
|
358
|
+
* Supports wildcards (*) for dynamic segments
|
|
359
|
+
*/
|
|
360
|
+
matchFieldPattern(pattern, fieldname) {
|
|
361
|
+
const patternParts = pattern.split('.');
|
|
362
|
+
const fieldParts = fieldname.split('.');
|
|
363
|
+
if (patternParts.length !== fieldParts.length) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
367
|
+
const patternPart = patternParts[i];
|
|
368
|
+
const fieldPart = fieldParts[i];
|
|
369
|
+
if (patternPart === '*') {
|
|
370
|
+
// Wildcard matches any segment
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
else if (patternPart !== fieldPart) {
|
|
374
|
+
// Exact match required
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Execute a single action by name
|
|
382
|
+
*/
|
|
383
|
+
async executeAction(actionName, context, timeout) {
|
|
384
|
+
const startTime = performance.now();
|
|
385
|
+
const actionTimeout = timeout ?? this.options.defaultTimeout;
|
|
386
|
+
try {
|
|
387
|
+
// Look up action in global registry
|
|
388
|
+
const actionFn = this.globalActions.get(actionName);
|
|
389
|
+
if (!actionFn) {
|
|
390
|
+
throw new Error(`Action "${actionName}" not found in registry`);
|
|
391
|
+
}
|
|
392
|
+
await this.executeWithTimeout(actionFn, context, actionTimeout);
|
|
393
|
+
const executionTime = performance.now() - startTime;
|
|
394
|
+
return {
|
|
395
|
+
success: true,
|
|
396
|
+
executionTime,
|
|
397
|
+
action: actionName,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
const executionTime = performance.now() - startTime;
|
|
402
|
+
const actionError = error instanceof Error ? error : new Error(String(error));
|
|
403
|
+
return {
|
|
404
|
+
success: false,
|
|
405
|
+
error: actionError,
|
|
406
|
+
executionTime,
|
|
407
|
+
action: actionName,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Execute a function with timeout
|
|
413
|
+
*/
|
|
414
|
+
async executeWithTimeout(fn, context, timeout) {
|
|
415
|
+
return new Promise((resolve, reject) => {
|
|
416
|
+
const timeoutId = setTimeout(() => {
|
|
417
|
+
reject(new Error(`Action timeout after ${timeout}ms`));
|
|
418
|
+
}, timeout);
|
|
419
|
+
Promise.resolve(fn(context))
|
|
420
|
+
.then(result => {
|
|
421
|
+
clearTimeout(timeoutId);
|
|
422
|
+
resolve(result);
|
|
423
|
+
})
|
|
424
|
+
.catch(error => {
|
|
425
|
+
clearTimeout(timeoutId);
|
|
426
|
+
reject(error);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Capture a snapshot of the record state before executing actions
|
|
432
|
+
* This creates a deep copy of the record data for potential rollback
|
|
433
|
+
*/
|
|
434
|
+
captureSnapshot(context) {
|
|
435
|
+
if (!context.store || !context.doctype || !context.recordId) {
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
// Get the record path (doctype.recordId)
|
|
440
|
+
const recordPath = `${context.doctype}.${context.recordId}`;
|
|
441
|
+
// Get the current record data
|
|
442
|
+
const recordData = context.store.get(recordPath);
|
|
443
|
+
if (!recordData || typeof recordData !== 'object') {
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
// Create a deep copy to avoid reference issues
|
|
447
|
+
return JSON.parse(JSON.stringify(recordData));
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
if (this.options.debug) {
|
|
451
|
+
// eslint-disable-next-line no-console
|
|
452
|
+
console.warn('[FieldTriggers] Failed to capture snapshot:', error);
|
|
453
|
+
}
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Restore a previously captured snapshot
|
|
459
|
+
* This reverts the record to its state before actions were executed
|
|
460
|
+
*/
|
|
461
|
+
restoreSnapshot(context, snapshot) {
|
|
462
|
+
if (!context.store || !context.doctype || !context.recordId || !snapshot) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
// Get the record path (doctype.recordId)
|
|
467
|
+
const recordPath = `${context.doctype}.${context.recordId}`;
|
|
468
|
+
// Restore the entire record from snapshot
|
|
469
|
+
context.store.set(recordPath, snapshot);
|
|
470
|
+
if (this.options.debug) {
|
|
471
|
+
// eslint-disable-next-line no-console
|
|
472
|
+
console.log(`[FieldTriggers] Rolled back ${recordPath} to previous state`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
// eslint-disable-next-line no-console
|
|
477
|
+
console.error('[FieldTriggers] Failed to restore snapshot:', error);
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Get or create the global field trigger engine singleton
|
|
484
|
+
* @param options - Optional configuration for the field trigger engine
|
|
485
|
+
* @public
|
|
486
|
+
*/
|
|
487
|
+
export function getGlobalTriggerEngine(options) {
|
|
488
|
+
return new FieldTriggerEngine(options);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Register a global action function that can be used in field triggers
|
|
492
|
+
* @param name - The name of the action to register
|
|
493
|
+
* @param fn - The action function to execute when the trigger fires
|
|
494
|
+
* @public
|
|
495
|
+
*/
|
|
496
|
+
export function registerGlobalAction(name, fn) {
|
|
497
|
+
const engine = getGlobalTriggerEngine();
|
|
498
|
+
engine.registerAction(name, fn);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Register a global XState transition action function
|
|
502
|
+
* @param name - The name of the transition action to register
|
|
503
|
+
* @param fn - The transition action function to execute
|
|
504
|
+
* @public
|
|
505
|
+
*/
|
|
506
|
+
export function registerTransitionAction(name, fn) {
|
|
507
|
+
const engine = getGlobalTriggerEngine();
|
|
508
|
+
engine.registerTransitionAction(name, fn);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Configure rollback behavior for a specific field trigger
|
|
512
|
+
* @param doctype - The doctype name
|
|
513
|
+
* @param fieldname - The field name
|
|
514
|
+
* @param enableRollback - Whether to enable automatic rollback for this field
|
|
515
|
+
* @public
|
|
516
|
+
*/
|
|
517
|
+
export function setFieldRollback(doctype, fieldname, enableRollback) {
|
|
518
|
+
const engine = getGlobalTriggerEngine();
|
|
519
|
+
engine.setFieldRollback(doctype, fieldname, enableRollback);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Manually trigger an XState transition for a specific doctype/record
|
|
523
|
+
* This can be called directly when you need to execute transition actions programmatically
|
|
524
|
+
* @param doctype - The doctype name
|
|
525
|
+
* @param transition - The XState transition name to trigger
|
|
526
|
+
* @param options - Optional configuration for the transition
|
|
527
|
+
* @public
|
|
528
|
+
*/
|
|
529
|
+
export async function triggerTransition(doctype, transition, options) {
|
|
530
|
+
const engine = getGlobalTriggerEngine();
|
|
531
|
+
const context = {
|
|
532
|
+
path: options?.path || (options?.recordId ? `${doctype}.${options.recordId}` : doctype),
|
|
533
|
+
fieldname: '',
|
|
534
|
+
beforeValue: undefined,
|
|
535
|
+
afterValue: undefined,
|
|
536
|
+
operation: 'set',
|
|
537
|
+
doctype,
|
|
538
|
+
recordId: options?.recordId,
|
|
539
|
+
timestamp: new Date(),
|
|
540
|
+
transition,
|
|
541
|
+
currentState: options?.currentState,
|
|
542
|
+
targetState: options?.targetState,
|
|
543
|
+
fsmContext: options?.fsmContext,
|
|
544
|
+
};
|
|
545
|
+
return await engine.executeTransitionActions(context);
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Mark a specific operation as irreversible.
|
|
549
|
+
* Used to prevent undo of critical operations like publishing or deletion.
|
|
550
|
+
* @param operationId - The ID of the operation to mark as irreversible
|
|
551
|
+
* @param reason - Human-readable reason why the operation cannot be undone
|
|
552
|
+
* @public
|
|
553
|
+
*/
|
|
554
|
+
export function markOperationIrreversible(operationId, reason) {
|
|
555
|
+
if (!operationId)
|
|
556
|
+
return;
|
|
557
|
+
try {
|
|
558
|
+
const store = useOperationLogStore();
|
|
559
|
+
store.markIrreversible(operationId, reason);
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
// Operation log is optional
|
|
563
|
+
}
|
|
564
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
export type * from '@stonecrop/aform/types';
|
|
2
2
|
export type * from '@stonecrop/atable/types';
|
|
3
|
-
import {
|
|
3
|
+
import { useStonecrop } from './composable';
|
|
4
|
+
import { useOperationLog, useUndoRedoShortcuts, withBatch } from './composables/operation-log';
|
|
4
5
|
import DoctypeMeta from './doctype';
|
|
6
|
+
import { getGlobalTriggerEngine, markOperationIrreversible, registerGlobalAction, registerTransitionAction, setFieldRollback, triggerTransition } from './field-triggers';
|
|
7
|
+
import plugin from './plugins';
|
|
5
8
|
import Registry from './registry';
|
|
6
|
-
import Stonecrop from './
|
|
7
|
-
import {
|
|
9
|
+
import { Stonecrop } from './stonecrop';
|
|
10
|
+
import { HST, createHST, type HSTNode } from './stores/hst';
|
|
11
|
+
import { useOperationLogStore } from './stores/operation-log';
|
|
8
12
|
export type * from './types';
|
|
9
|
-
export {
|
|
13
|
+
export type { BaseStonecropReturn, HSTChangeData, HSTStonecropReturn, OperationLogAPI } from './composable';
|
|
14
|
+
export type { FieldTriggerEngine } from './field-triggers';
|
|
15
|
+
export type { FieldChangeContext, TransitionChangeContext, FieldTriggerExecutionResult, ActionExecutionResult, TransitionExecutionResult, FieldActionFunction, TransitionActionFunction, } from './types/field-triggers';
|
|
16
|
+
export { DoctypeMeta, Registry, Stonecrop, useStonecrop, HST, createHST, HSTNode, getGlobalTriggerEngine, registerGlobalAction, registerTransitionAction, setFieldRollback, triggerTransition, markOperationIrreversible, useOperationLog, useOperationLogStore, useUndoRedoShortcuts, withBatch, };
|
|
17
|
+
export default plugin;
|
|
10
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,wBAAwB,CAAA;AAC3C,mBAAmB,yBAAyB,CAAA;AAE5C,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,wBAAwB,CAAA;AAC3C,mBAAmB,yBAAyB,CAAA;AAE5C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAA;AAC9F,OAAO,WAAW,MAAM,WAAW,CAAA;AACnC,OAAO,EACN,sBAAsB,EACtB,yBAAyB,EACzB,oBAAoB,EACpB,wBAAwB,EACxB,gBAAgB,EAChB,iBAAiB,EACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,MAAM,MAAM,WAAW,CAAA;AAC9B,OAAO,QAAQ,MAAM,YAAY,CAAA;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,mBAAmB,SAAS,CAAA;AAC5B,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC3G,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAC1D,YAAY,EACX,kBAAkB,EAClB,uBAAuB,EACvB,2BAA2B,EAC3B,qBAAqB,EACrB,yBAAyB,EACzB,mBAAmB,EACnB,wBAAwB,GACxB,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EACN,WAAW,EACX,QAAQ,EACR,SAAS,EACT,YAAY,EAEZ,GAAG,EACH,SAAS,EACT,OAAO,EAEP,sBAAsB,EACtB,oBAAoB,EACpB,wBAAwB,EACxB,gBAAgB,EAChB,iBAAiB,EACjB,yBAAyB,EAEzB,eAAe,EACf,oBAAoB,EACpB,oBAAoB,EACpB,SAAS,GACT,CAAA;AAGD,eAAe,MAAM,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useStonecrop } from './composable';
|
|
2
|
+
import { useOperationLog, useUndoRedoShortcuts, withBatch } from './composables/operation-log';
|
|
3
|
+
import DoctypeMeta from './doctype';
|
|
4
|
+
import { getGlobalTriggerEngine, markOperationIrreversible, registerGlobalAction, registerTransitionAction, setFieldRollback, triggerTransition, } from './field-triggers';
|
|
5
|
+
import plugin from './plugins';
|
|
6
|
+
import Registry from './registry';
|
|
7
|
+
import { Stonecrop } from './stonecrop';
|
|
8
|
+
import { HST, createHST } from './stores/hst';
|
|
9
|
+
import { useOperationLogStore } from './stores/operation-log';
|
|
10
|
+
export { DoctypeMeta, Registry, Stonecrop, useStonecrop,
|
|
11
|
+
// HST exports for advanced usage
|
|
12
|
+
HST, createHST,
|
|
13
|
+
// Field trigger system exports
|
|
14
|
+
getGlobalTriggerEngine, registerGlobalAction, registerTransitionAction, setFieldRollback, triggerTransition, markOperationIrreversible,
|
|
15
|
+
// Operation log exports
|
|
16
|
+
useOperationLog, useOperationLogStore, useUndoRedoShortcuts, withBatch, };
|
|
17
|
+
// Default export is the Vue plugin
|
|
18
|
+
export default plugin;
|
|
@@ -5,26 +5,24 @@ import { type Plugin } from 'vue';
|
|
|
5
5
|
* @param options - The plugin options
|
|
6
6
|
* @example
|
|
7
7
|
* ```ts
|
|
8
|
-
*
|
|
9
8
|
* import { createApp } from 'vue'
|
|
10
|
-
* import Stonecrop from 'stonecrop'
|
|
11
|
-
*
|
|
12
|
-
* import App from './App.vue'
|
|
9
|
+
* import Stonecrop from '@stonecrop/stonecrop'
|
|
10
|
+
* import router from './router'
|
|
13
11
|
*
|
|
14
12
|
* const app = createApp(App)
|
|
15
13
|
* app.use(Stonecrop, {
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
14
|
+
* router,
|
|
15
|
+
* getMeta: async (routeContext) => {
|
|
16
|
+
* // routeContext contains: { path, segments }
|
|
17
|
+
* // fetch doctype meta from your API using the route context
|
|
18
|
+
* },
|
|
19
|
+
* autoInitializeRouter: true,
|
|
20
|
+
* onRouterInitialized: async (registry, stonecrop) => {
|
|
21
|
+
* // your custom initialization logic here
|
|
22
|
+
* }
|
|
23
23
|
* })
|
|
24
|
-
*
|
|
25
24
|
* app.mount('#app')
|
|
26
25
|
* ```
|
|
27
|
-
*
|
|
28
26
|
* @public
|
|
29
27
|
*/
|
|
30
28
|
declare const plugin: Plugin;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/plugins/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/plugins/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,MAAM,EAAY,MAAM,KAAK,CAAA;AA2BhD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,MAAM,EAAE,MAiDb,CAAA;AAED,eAAe,MAAM,CAAA"}
|