@stonecrop/stonecrop 0.4.37 → 0.6.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/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 -7028
- 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 +6 -5
- package/src/composable.ts +481 -31
- package/src/composables/operation-log.ts +254 -0
- package/src/doctype.ts +9 -3
- 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 -155
- 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 -50
- 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/{exceptions.js → src/exceptions.js} +0 -0
- /package/dist/{types/index.js → src/types/operation-log.js} +0 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
import { getGlobalTriggerEngine } from '../field-triggers'
|
|
2
|
+
import type { FieldChangeContext, TransitionChangeContext } from '../types/field-triggers'
|
|
3
|
+
import { useOperationLogStore } from './operation-log'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get the operation log store if available
|
|
7
|
+
*/
|
|
8
|
+
function getOperationLogStore() {
|
|
9
|
+
try {
|
|
10
|
+
return useOperationLogStore()
|
|
11
|
+
} catch {
|
|
12
|
+
// Operation log is optional
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Core HST Interface - enhanced with tree navigation
|
|
19
|
+
* Provides a hierarchical state tree interface for navigating and manipulating nested data structures.
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
interface HSTNode {
|
|
24
|
+
/**
|
|
25
|
+
* Gets a value at the specified path
|
|
26
|
+
* @param path - The dot-separated path to the value
|
|
27
|
+
* @returns The value at the specified path
|
|
28
|
+
*/
|
|
29
|
+
get(path: string): any
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sets a value at the specified path
|
|
33
|
+
* @param path - The dot-separated path where to set the value
|
|
34
|
+
* @param value - The value to set
|
|
35
|
+
* @param source - Optional source of the operation (user, system, sync, undo, redo)
|
|
36
|
+
*/
|
|
37
|
+
set(path: string, value: any, source?: 'user' | 'system' | 'sync' | 'undo' | 'redo'): void
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Checks if a value exists at the specified path
|
|
41
|
+
* @param path - The dot-separated path to check
|
|
42
|
+
* @returns True if the path exists, false otherwise
|
|
43
|
+
*/
|
|
44
|
+
has(path: string): boolean
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Gets the parent node in the tree hierarchy
|
|
48
|
+
* @returns The parent HSTNode or null if this is the root
|
|
49
|
+
*/
|
|
50
|
+
getParent(): HSTNode | null
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Gets the root node of the tree
|
|
54
|
+
* @returns The root HSTNode
|
|
55
|
+
*/
|
|
56
|
+
getRoot(): HSTNode
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Gets the full path from root to this node
|
|
60
|
+
* @returns The dot-separated path string
|
|
61
|
+
*/
|
|
62
|
+
getPath(): string
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Gets the depth level of this node in the tree
|
|
66
|
+
* @returns The depth as a number (0 for root)
|
|
67
|
+
*/
|
|
68
|
+
getDepth(): number
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Gets an array of path segments from root to this node
|
|
72
|
+
* @returns Array of path segments representing breadcrumbs
|
|
73
|
+
*/
|
|
74
|
+
getBreadcrumbs(): string[]
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Gets a child node at the specified relative path
|
|
78
|
+
* @param path - The relative path to the child node
|
|
79
|
+
* @returns The child HSTNode
|
|
80
|
+
*/
|
|
81
|
+
getNode(path: string): HSTNode
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Trigger an XState transition with optional context data
|
|
85
|
+
* @param transition - The transition name (should be uppercase per convention)
|
|
86
|
+
* @param context - Optional additional FSM context data
|
|
87
|
+
* @returns Promise resolving to the transition execution results
|
|
88
|
+
*/
|
|
89
|
+
triggerTransition(
|
|
90
|
+
transition: string,
|
|
91
|
+
context?: { currentState?: string; targetState?: string; fsmContext?: Record<string, any> }
|
|
92
|
+
): Promise<any>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Type definitions for global Registry
|
|
96
|
+
interface RegistryGlobal {
|
|
97
|
+
Registry?: {
|
|
98
|
+
_root?: {
|
|
99
|
+
registry: Record<string, any>
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Interface for Immutable-like objects
|
|
105
|
+
interface ImmutableLike {
|
|
106
|
+
get(key: string): any
|
|
107
|
+
set(key: string, value: any): ImmutableLike
|
|
108
|
+
has(key: string): boolean
|
|
109
|
+
size?: number
|
|
110
|
+
__ownerID?: any
|
|
111
|
+
_map?: any
|
|
112
|
+
_list?: any
|
|
113
|
+
_origin?: any
|
|
114
|
+
_capacity?: any
|
|
115
|
+
_defaultValues?: any
|
|
116
|
+
_tail?: any
|
|
117
|
+
_root?: any
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Interface for Vue reactive objects
|
|
121
|
+
interface VueReactive {
|
|
122
|
+
__v_isReactive: boolean
|
|
123
|
+
[key: string]: any
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Interface for Pinia stores
|
|
127
|
+
interface PiniaStore {
|
|
128
|
+
$state?: Record<string, any>
|
|
129
|
+
$patch?: (partial: Record<string, any>) => void
|
|
130
|
+
$id?: string
|
|
131
|
+
[key: string]: any
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Interface for objects with property access
|
|
135
|
+
interface PropertyAccessible {
|
|
136
|
+
[key: string]: any
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Extend global interfaces
|
|
140
|
+
declare global {
|
|
141
|
+
interface Window extends RegistryGlobal {}
|
|
142
|
+
const global: RegistryGlobal | undefined
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Global HST Manager (Singleton)
|
|
147
|
+
* Manages hierarchical state trees and provides access to the global registry.
|
|
148
|
+
*
|
|
149
|
+
* @public
|
|
150
|
+
*/
|
|
151
|
+
class HST {
|
|
152
|
+
private static instance: HST
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Gets the singleton instance of HST
|
|
156
|
+
* @returns The HST singleton instance
|
|
157
|
+
*/
|
|
158
|
+
static getInstance(): HST {
|
|
159
|
+
if (!HST.instance) {
|
|
160
|
+
HST.instance = new HST()
|
|
161
|
+
}
|
|
162
|
+
return HST.instance
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Gets the global registry instance
|
|
167
|
+
* @returns The global registry object or undefined if not found
|
|
168
|
+
*/
|
|
169
|
+
getRegistry(): any {
|
|
170
|
+
// In test environment, try different ways to access Registry
|
|
171
|
+
// First, try the global Registry if it exists
|
|
172
|
+
if (typeof globalThis !== 'undefined') {
|
|
173
|
+
const globalRegistry = (globalThis as RegistryGlobal).Registry?._root
|
|
174
|
+
if (globalRegistry) {
|
|
175
|
+
return globalRegistry
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Try to access through window (browser environment)
|
|
180
|
+
if (typeof window !== 'undefined') {
|
|
181
|
+
const windowRegistry = window.Registry?._root
|
|
182
|
+
if (windowRegistry) {
|
|
183
|
+
return windowRegistry
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Try to access through global (Node environment)
|
|
188
|
+
if (typeof global !== 'undefined' && global) {
|
|
189
|
+
const nodeRegistry = global.Registry?._root
|
|
190
|
+
if (nodeRegistry) {
|
|
191
|
+
return nodeRegistry
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// If we can't find it globally, it might not be set up
|
|
196
|
+
// This is expected in test environments where Registry is created locally
|
|
197
|
+
return undefined
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Helper method to get doctype metadata from the registry
|
|
202
|
+
* @param doctype - The name of the doctype to retrieve metadata for
|
|
203
|
+
* @returns The doctype metadata object or undefined if not found
|
|
204
|
+
*/
|
|
205
|
+
getDoctypeMeta(doctype: string) {
|
|
206
|
+
const registry = this.getRegistry()
|
|
207
|
+
if (registry && typeof registry === 'object' && 'registry' in registry) {
|
|
208
|
+
return (registry as { registry: Record<string, any> }).registry[doctype]
|
|
209
|
+
}
|
|
210
|
+
return undefined
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Enhanced HST Proxy with tree navigation
|
|
215
|
+
class HSTProxy implements HSTNode {
|
|
216
|
+
private target: any
|
|
217
|
+
private parentPath: string
|
|
218
|
+
private rootNode: HSTNode | null
|
|
219
|
+
private doctype: string
|
|
220
|
+
private parentDoctype?: string
|
|
221
|
+
private hst: HST
|
|
222
|
+
|
|
223
|
+
constructor(target: any, doctype: string, parentPath = '', rootNode: HSTNode | null = null, parentDoctype?: string) {
|
|
224
|
+
this.target = target
|
|
225
|
+
this.parentPath = parentPath
|
|
226
|
+
this.rootNode = rootNode || this
|
|
227
|
+
this.doctype = doctype
|
|
228
|
+
this.parentDoctype = parentDoctype
|
|
229
|
+
this.hst = HST.getInstance()
|
|
230
|
+
|
|
231
|
+
return new Proxy(this, {
|
|
232
|
+
get(hst, prop) {
|
|
233
|
+
// Return HST methods directly
|
|
234
|
+
if (prop in hst) return hst[prop]
|
|
235
|
+
|
|
236
|
+
// Handle property access - return tree nodes for navigation
|
|
237
|
+
const path = String(prop)
|
|
238
|
+
return hst.getNode(path)
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
set(hst, prop, value) {
|
|
242
|
+
const path = String(prop)
|
|
243
|
+
hst.set(path, value)
|
|
244
|
+
return true
|
|
245
|
+
},
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
get(path: string): any {
|
|
250
|
+
return this.resolveValue(path)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Method to get a tree-wrapped node for navigation
|
|
254
|
+
getNode(path: string): HSTNode {
|
|
255
|
+
const fullPath = this.resolvePath(path)
|
|
256
|
+
const value = this.resolveValue(path)
|
|
257
|
+
|
|
258
|
+
// Determine the correct doctype for this node based on the path
|
|
259
|
+
const pathSegments = fullPath.split('.')
|
|
260
|
+
let nodeDoctype = this.doctype
|
|
261
|
+
|
|
262
|
+
// If we're at the root level and this is a StonecropStore, use the first path segment as the doctype
|
|
263
|
+
if (this.doctype === 'StonecropStore' && pathSegments.length >= 1) {
|
|
264
|
+
nodeDoctype = pathSegments[0]
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Always wrap in HSTProxy for tree navigation
|
|
268
|
+
if (typeof value === 'object' && value !== null && !this.isPrimitive(value)) {
|
|
269
|
+
return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode, this.parentDoctype)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// For primitives, return a minimal wrapper that throws on tree operations
|
|
273
|
+
return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode, this.parentDoctype)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
set(path: string, value: any, source: 'user' | 'system' | 'sync' | 'undo' | 'redo' = 'user'): void {
|
|
277
|
+
// Get current value for change context
|
|
278
|
+
const fullPath = this.resolvePath(path)
|
|
279
|
+
const beforeValue = this.has(path) ? this.get(path) : undefined
|
|
280
|
+
|
|
281
|
+
// Log operation if not from undo/redo and store is available
|
|
282
|
+
if (source !== 'undo' && source !== 'redo') {
|
|
283
|
+
const logStore = getOperationLogStore()
|
|
284
|
+
if (logStore && typeof logStore.addOperation === 'function') {
|
|
285
|
+
const pathSegments = fullPath.split('.')
|
|
286
|
+
const doctype = this.doctype === 'StonecropStore' && pathSegments.length >= 1 ? pathSegments[0] : this.doctype
|
|
287
|
+
const recordId = pathSegments.length >= 2 ? pathSegments[1] : undefined
|
|
288
|
+
const fieldname = pathSegments.slice(2).join('.') || pathSegments[pathSegments.length - 1]
|
|
289
|
+
|
|
290
|
+
// Detect if this is a DELETE operation (setting to undefined when a value existed)
|
|
291
|
+
const isDelete = value === undefined && beforeValue !== undefined
|
|
292
|
+
const operationType: 'set' | 'delete' = isDelete ? 'delete' : 'set'
|
|
293
|
+
|
|
294
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
295
|
+
logStore.addOperation(
|
|
296
|
+
{
|
|
297
|
+
type: operationType,
|
|
298
|
+
path: fullPath,
|
|
299
|
+
fieldname,
|
|
300
|
+
beforeValue,
|
|
301
|
+
afterValue: value,
|
|
302
|
+
doctype,
|
|
303
|
+
recordId,
|
|
304
|
+
reversible: true, // Default to reversible, can be changed by field triggers
|
|
305
|
+
},
|
|
306
|
+
source
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Update the value
|
|
312
|
+
this.updateValue(path, value)
|
|
313
|
+
|
|
314
|
+
// Trigger field actions asynchronously (don't block the set operation)
|
|
315
|
+
void this.triggerFieldActions(fullPath, beforeValue, value)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
has(path: string): boolean {
|
|
319
|
+
try {
|
|
320
|
+
// Handle empty path case
|
|
321
|
+
if (path === '') {
|
|
322
|
+
return true // empty path refers to the root object
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const segments = this.parsePath(path)
|
|
326
|
+
let current = this.target
|
|
327
|
+
|
|
328
|
+
for (let i = 0; i < segments.length; i++) {
|
|
329
|
+
const segment = segments[i]
|
|
330
|
+
|
|
331
|
+
if (current === null || current === undefined) {
|
|
332
|
+
return false
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Check if this is the last segment
|
|
336
|
+
if (i === segments.length - 1) {
|
|
337
|
+
// For the final property, check if it exists
|
|
338
|
+
if (this.isImmutable(current)) {
|
|
339
|
+
return current.has(segment)
|
|
340
|
+
} else if (this.isPiniaStore(current)) {
|
|
341
|
+
return (current.$state && segment in current.$state) || segment in current
|
|
342
|
+
} else {
|
|
343
|
+
return segment in current
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Navigate to the next level
|
|
348
|
+
current = this.getProperty(current, segment)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return false
|
|
352
|
+
} catch {
|
|
353
|
+
return false
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Tree navigation methods
|
|
358
|
+
getParent(): HSTNode | null {
|
|
359
|
+
if (!this.parentPath) return null
|
|
360
|
+
|
|
361
|
+
const parentSegments = this.parentPath.split('.').slice(0, -1)
|
|
362
|
+
const parentPath = parentSegments.join('.')
|
|
363
|
+
|
|
364
|
+
if (parentPath === '') {
|
|
365
|
+
return this.rootNode
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Return a wrapped node, not raw data
|
|
369
|
+
return this.rootNode!.getNode(parentPath)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
getRoot(): HSTNode {
|
|
373
|
+
return this.rootNode!
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
getPath(): string {
|
|
377
|
+
return this.parentPath
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
getDepth(): number {
|
|
381
|
+
return this.parentPath ? this.parentPath.split('.').length : 0
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
getBreadcrumbs(): string[] {
|
|
385
|
+
return this.parentPath ? this.parentPath.split('.') : []
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Trigger an XState transition with optional context data
|
|
390
|
+
*/
|
|
391
|
+
async triggerTransition(
|
|
392
|
+
transition: string,
|
|
393
|
+
context?: { currentState?: string; targetState?: string; fsmContext?: Record<string, any> }
|
|
394
|
+
): Promise<any> {
|
|
395
|
+
const triggerEngine = getGlobalTriggerEngine()
|
|
396
|
+
|
|
397
|
+
// Determine doctype and recordId from the current path
|
|
398
|
+
const pathSegments = this.parentPath.split('.')
|
|
399
|
+
let doctype = this.doctype
|
|
400
|
+
let recordId: string | undefined
|
|
401
|
+
|
|
402
|
+
// If we're at the root level and this is a StonecropStore, use the first path segment as the doctype
|
|
403
|
+
if (this.doctype === 'StonecropStore' && pathSegments.length >= 1) {
|
|
404
|
+
doctype = pathSegments[0]
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Extract recordId from path if it follows the expected pattern
|
|
408
|
+
if (pathSegments.length >= 2) {
|
|
409
|
+
recordId = pathSegments[1]
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Build transition context
|
|
413
|
+
const transitionContext: TransitionChangeContext = {
|
|
414
|
+
path: this.parentPath,
|
|
415
|
+
fieldname: '', // No specific field for transitions
|
|
416
|
+
beforeValue: undefined,
|
|
417
|
+
afterValue: undefined,
|
|
418
|
+
operation: 'set',
|
|
419
|
+
doctype,
|
|
420
|
+
recordId,
|
|
421
|
+
timestamp: new Date(),
|
|
422
|
+
store: this.rootNode || undefined,
|
|
423
|
+
transition,
|
|
424
|
+
currentState: context?.currentState,
|
|
425
|
+
targetState: context?.targetState,
|
|
426
|
+
fsmContext: context?.fsmContext,
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Log FSM transition operation
|
|
430
|
+
const logStore = getOperationLogStore()
|
|
431
|
+
if (logStore && typeof logStore.addOperation === 'function') {
|
|
432
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
433
|
+
logStore.addOperation(
|
|
434
|
+
{
|
|
435
|
+
type: 'transition' as const,
|
|
436
|
+
path: this.parentPath,
|
|
437
|
+
fieldname: transition,
|
|
438
|
+
beforeValue: context?.currentState,
|
|
439
|
+
afterValue: context?.targetState,
|
|
440
|
+
doctype,
|
|
441
|
+
recordId,
|
|
442
|
+
reversible: false, // FSM transitions are generally not reversible
|
|
443
|
+
metadata: {
|
|
444
|
+
transition,
|
|
445
|
+
currentState: context?.currentState,
|
|
446
|
+
targetState: context?.targetState,
|
|
447
|
+
fsmContext: context?.fsmContext,
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
'user'
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Execute transition actions
|
|
455
|
+
return await triggerEngine.executeTransitionActions(transitionContext)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Private helper methods
|
|
459
|
+
private resolvePath(path: string): string {
|
|
460
|
+
if (path === '') return this.parentPath
|
|
461
|
+
return this.parentPath ? `${this.parentPath}.${path}` : path
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private resolveValue(path: string): any {
|
|
465
|
+
// Handle empty path - return the target object
|
|
466
|
+
if (path === '') {
|
|
467
|
+
return this.target
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const segments = this.parsePath(path)
|
|
471
|
+
let current = this.target
|
|
472
|
+
|
|
473
|
+
for (const segment of segments) {
|
|
474
|
+
if (current === null || current === undefined) {
|
|
475
|
+
return undefined
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
current = this.getProperty(current, segment)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return current
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private updateValue(path: string, value: any): void {
|
|
485
|
+
// Handle empty path case - should throw error
|
|
486
|
+
if (path === '') {
|
|
487
|
+
throw new Error('Cannot set value on empty path')
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const segments = this.parsePath(path)
|
|
491
|
+
const lastSegment = segments.pop()!
|
|
492
|
+
let current = this.target
|
|
493
|
+
|
|
494
|
+
// Navigate to parent object
|
|
495
|
+
for (const segment of segments) {
|
|
496
|
+
current = this.getProperty(current, segment)
|
|
497
|
+
if (current === null || current === undefined) {
|
|
498
|
+
throw new Error(`Cannot set property on null/undefined path: ${path}`)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Set the final property
|
|
503
|
+
this.setProperty(current, lastSegment, value)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private getProperty(obj: any, key: string): any {
|
|
507
|
+
// Immutable objects
|
|
508
|
+
if (this.isImmutable(obj)) {
|
|
509
|
+
return obj.get(key)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Vue reactive object
|
|
513
|
+
if (this.isVueReactive(obj)) {
|
|
514
|
+
return obj[key]
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Pinia store
|
|
518
|
+
if (this.isPiniaStore(obj)) {
|
|
519
|
+
return obj.$state?.[key] ?? obj[key]
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Plain object
|
|
523
|
+
return (obj as PropertyAccessible)[key]
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private setProperty(obj: any, key: string, value: any): void {
|
|
527
|
+
// Immutable objects
|
|
528
|
+
if (this.isImmutable(obj)) {
|
|
529
|
+
throw new Error('Cannot directly mutate immutable objects. Use immutable update methods instead.')
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Pinia store
|
|
533
|
+
if (this.isPiniaStore(obj)) {
|
|
534
|
+
if (obj.$patch) {
|
|
535
|
+
obj.$patch({ [key]: value })
|
|
536
|
+
} else {
|
|
537
|
+
;(obj as PropertyAccessible)[key] = value
|
|
538
|
+
}
|
|
539
|
+
return
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Vue reactive or plain object
|
|
543
|
+
;(obj as PropertyAccessible)[key] = value
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private async triggerFieldActions(fullPath: string, beforeValue: any, afterValue: any): Promise<void> {
|
|
547
|
+
try {
|
|
548
|
+
// Guard against undefined or null fullPath
|
|
549
|
+
if (!fullPath || typeof fullPath !== 'string') {
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const pathSegments = fullPath.split('.')
|
|
554
|
+
|
|
555
|
+
// Only trigger field actions for actual field changes (at least 3 levels deep: doctype.recordId.fieldname)
|
|
556
|
+
// Skip triggering for doctype-level or record-level changes
|
|
557
|
+
if (pathSegments.length < 3) {
|
|
558
|
+
return
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const triggerEngine = getGlobalTriggerEngine()
|
|
562
|
+
const fieldname = pathSegments.slice(2).join('.') || pathSegments[pathSegments.length - 1]
|
|
563
|
+
|
|
564
|
+
// Determine the correct doctype for this path using the same logic as getNode()
|
|
565
|
+
// The path should be in format: "doctype.recordId.fieldname"
|
|
566
|
+
let doctype = this.doctype
|
|
567
|
+
|
|
568
|
+
// If we're at the root level and this is a StonecropStore, use the first path segment as the doctype
|
|
569
|
+
if (this.doctype === 'StonecropStore' && pathSegments.length >= 1) {
|
|
570
|
+
doctype = pathSegments[0]
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
let recordId: string | undefined
|
|
574
|
+
|
|
575
|
+
// Extract recordId from path if it follows the expected pattern
|
|
576
|
+
if (pathSegments.length >= 2) {
|
|
577
|
+
recordId = pathSegments[1]
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const context: FieldChangeContext = {
|
|
581
|
+
path: fullPath,
|
|
582
|
+
fieldname,
|
|
583
|
+
beforeValue,
|
|
584
|
+
afterValue,
|
|
585
|
+
operation: 'set',
|
|
586
|
+
doctype,
|
|
587
|
+
recordId,
|
|
588
|
+
timestamp: new Date(),
|
|
589
|
+
store: this.rootNode || undefined, // Pass the root store for snapshot/rollback capabilities
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
await triggerEngine.executeFieldTriggers(context)
|
|
593
|
+
} catch (error) {
|
|
594
|
+
// Silently handle trigger errors to not break the main flow
|
|
595
|
+
// In production, you might want to log this error
|
|
596
|
+
if (error instanceof Error) {
|
|
597
|
+
// eslint-disable-next-line no-console
|
|
598
|
+
console.warn('Field trigger error:', error.message)
|
|
599
|
+
// Optional: emit an event or call error handler
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
private isVueReactive(obj: any): obj is VueReactive {
|
|
604
|
+
return (
|
|
605
|
+
obj &&
|
|
606
|
+
typeof obj === 'object' &&
|
|
607
|
+
'__v_isReactive' in obj &&
|
|
608
|
+
(obj as { __v_isReactive: boolean }).__v_isReactive === true
|
|
609
|
+
)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private isPiniaStore(obj: any): obj is PiniaStore {
|
|
613
|
+
return obj && typeof obj === 'object' && ('$state' in obj || '$patch' in obj || '$id' in obj)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private isImmutable(obj: any): obj is ImmutableLike {
|
|
617
|
+
if (!obj || typeof obj !== 'object') {
|
|
618
|
+
return false
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const hasGetMethod = 'get' in obj && typeof (obj as Record<string, unknown>).get === 'function'
|
|
622
|
+
const hasSetMethod = 'set' in obj && typeof (obj as Record<string, unknown>).set === 'function'
|
|
623
|
+
const hasHasMethod = 'has' in obj && typeof (obj as Record<string, unknown>).has === 'function'
|
|
624
|
+
|
|
625
|
+
const hasImmutableMarkers =
|
|
626
|
+
'__ownerID' in obj ||
|
|
627
|
+
'_map' in obj ||
|
|
628
|
+
'_list' in obj ||
|
|
629
|
+
'_origin' in obj ||
|
|
630
|
+
'_capacity' in obj ||
|
|
631
|
+
'_defaultValues' in obj ||
|
|
632
|
+
'_tail' in obj ||
|
|
633
|
+
'_root' in obj ||
|
|
634
|
+
('size' in obj && hasGetMethod && hasSetMethod)
|
|
635
|
+
|
|
636
|
+
let constructorName: string | undefined
|
|
637
|
+
try {
|
|
638
|
+
const objWithConstructor = obj as Record<string, unknown>
|
|
639
|
+
if (
|
|
640
|
+
'constructor' in objWithConstructor &&
|
|
641
|
+
objWithConstructor.constructor &&
|
|
642
|
+
typeof objWithConstructor.constructor === 'object' &&
|
|
643
|
+
'name' in objWithConstructor.constructor
|
|
644
|
+
) {
|
|
645
|
+
const nameValue = (objWithConstructor.constructor as { name: unknown }).name
|
|
646
|
+
constructorName = typeof nameValue === 'string' ? nameValue : undefined
|
|
647
|
+
}
|
|
648
|
+
} catch {
|
|
649
|
+
constructorName = undefined
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const isImmutableConstructor =
|
|
653
|
+
constructorName &&
|
|
654
|
+
(constructorName.includes('Map') ||
|
|
655
|
+
constructorName.includes('List') ||
|
|
656
|
+
constructorName.includes('Set') ||
|
|
657
|
+
constructorName.includes('Stack') ||
|
|
658
|
+
constructorName.includes('Seq')) &&
|
|
659
|
+
(hasGetMethod || hasSetMethod)
|
|
660
|
+
|
|
661
|
+
return Boolean(
|
|
662
|
+
(hasGetMethod && hasSetMethod && hasHasMethod && hasImmutableMarkers) ||
|
|
663
|
+
(hasGetMethod && hasSetMethod && isImmutableConstructor)
|
|
664
|
+
)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private isPrimitive(value: any): boolean {
|
|
668
|
+
// Don't wrap primitive values, functions, or null/undefined
|
|
669
|
+
return (
|
|
670
|
+
value === null ||
|
|
671
|
+
value === undefined ||
|
|
672
|
+
typeof value === 'string' ||
|
|
673
|
+
typeof value === 'number' ||
|
|
674
|
+
typeof value === 'boolean' ||
|
|
675
|
+
typeof value === 'function' ||
|
|
676
|
+
typeof value === 'symbol' ||
|
|
677
|
+
typeof value === 'bigint'
|
|
678
|
+
)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private parsePath(path: string): string[] {
|
|
682
|
+
if (!path) return []
|
|
683
|
+
return path.split('.').filter(segment => segment.length > 0)
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Factory function for HST creation
|
|
689
|
+
* Creates a new HSTNode proxy for hierarchical state tree navigation.
|
|
690
|
+
*
|
|
691
|
+
* @param target - The target object to wrap with HST functionality
|
|
692
|
+
* @param doctype - The document type identifier
|
|
693
|
+
* @param parentDoctype - Optional parent document type identifier
|
|
694
|
+
* @returns A new HSTNode proxy instance
|
|
695
|
+
*
|
|
696
|
+
* @public
|
|
697
|
+
*/
|
|
698
|
+
function createHST(target: any, doctype: string, parentDoctype?: string): HSTNode {
|
|
699
|
+
return new HSTProxy(target, doctype, '', null, parentDoctype)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Export everything
|
|
703
|
+
export { HSTProxy, HST, createHST, type HSTNode }
|
package/src/stores/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createPinia } from 'pinia'
|
|
2
2
|
import { PiniaSharedState } from 'pinia-shared-state'
|
|
3
3
|
|
|
4
|
+
import { HST } from './hst'
|
|
5
|
+
|
|
6
|
+
const hst = HST.getInstance()
|
|
4
7
|
const pinia = createPinia()
|
|
5
8
|
|
|
6
9
|
// Pass the plugin to your application's pinia plugin
|
|
@@ -11,4 +14,6 @@ pinia.use(
|
|
|
11
14
|
})
|
|
12
15
|
)
|
|
13
16
|
|
|
14
|
-
export { pinia }
|
|
17
|
+
export { hst, pinia }
|
|
18
|
+
export { useOperationLogStore } from './operation-log'
|
|
19
|
+
export type { HSTNode } from './hst'
|