@stonecrop/stonecrop 0.12.8 → 0.13.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.
Files changed (55) hide show
  1. package/dist/composable.js +1 -0
  2. package/dist/composables/lazy-link.js +125 -0
  3. package/dist/composables/operation-log.js +224 -0
  4. package/dist/composables/stonecrop.js +504 -0
  5. package/dist/composables/use-lazy-link-state.js +125 -0
  6. package/dist/composables/use-stonecrop.js +476 -0
  7. package/dist/doctype.js +242 -0
  8. package/dist/exceptions.js +16 -0
  9. package/dist/field-triggers.js +575 -0
  10. package/dist/index.js +27 -0
  11. package/dist/operation-log-DB-dGNT9.js +593 -0
  12. package/dist/operation-log-DB-dGNT9.js.map +1 -0
  13. package/dist/plugins/index.js +99 -0
  14. package/dist/registry.js +423 -0
  15. package/dist/schema-validator.js +407 -0
  16. package/dist/src/composable.d.ts +11 -0
  17. package/dist/src/composable.d.ts.map +1 -0
  18. package/dist/src/composable.js +477 -0
  19. package/dist/src/composables/use-lazy-link-state.d.ts +25 -0
  20. package/dist/src/composables/use-lazy-link-state.d.ts.map +1 -0
  21. package/dist/src/composables/use-stonecrop.d.ts +93 -0
  22. package/dist/src/composables/use-stonecrop.d.ts.map +1 -0
  23. package/dist/src/composables/useNestedSchema.d.ts +110 -0
  24. package/dist/src/composables/useNestedSchema.d.ts.map +1 -0
  25. package/dist/src/composables/useNestedSchema.js +155 -0
  26. package/dist/src/stores/data.d.ts +11 -0
  27. package/dist/src/stores/data.d.ts.map +1 -0
  28. package/dist/src/stores/xstate.d.ts +31 -0
  29. package/dist/src/stores/xstate.d.ts.map +1 -0
  30. package/dist/src/tsdoc-metadata.json +11 -0
  31. package/dist/src/utils.d.ts +24 -0
  32. package/dist/src/utils.d.ts.map +1 -0
  33. package/dist/stonecrop.css +1 -0
  34. package/dist/stonecrop.umd.cjs +6 -0
  35. package/dist/stonecrop.umd.cjs.map +1 -0
  36. package/dist/stores/data.js +7 -0
  37. package/dist/stores/hst.js +496 -0
  38. package/dist/stores/index.js +12 -0
  39. package/dist/stores/operation-log.js +580 -0
  40. package/dist/stores/xstate.js +29 -0
  41. package/dist/tests/setup.d.ts +5 -0
  42. package/dist/tests/setup.d.ts.map +1 -0
  43. package/dist/tests/setup.js +15 -0
  44. package/dist/types/composable.js +0 -0
  45. package/dist/types/doctype.js +0 -0
  46. package/dist/types/field-triggers.js +4 -0
  47. package/dist/types/hst.js +0 -0
  48. package/dist/types/index.js +10 -0
  49. package/dist/types/operation-log.js +0 -0
  50. package/dist/types/plugin.js +0 -0
  51. package/dist/types/registry.js +0 -0
  52. package/dist/types/schema-validator.js +13 -0
  53. package/dist/types/stonecrop.js +0 -0
  54. package/dist/utils.js +46 -0
  55. package/package.json +4 -4
@@ -0,0 +1,7 @@
1
+ import { defineStore } from 'pinia';
2
+ import { ref } from 'vue';
3
+ export const useDataStore = defineStore('data', () => {
4
+ const records = ref([]);
5
+ const record = ref({});
6
+ return { records, record };
7
+ });
@@ -0,0 +1,496 @@
1
+ import { getGlobalTriggerEngine } from '../field-triggers';
2
+ import { useOperationLogStore } from './operation-log';
3
+ /**
4
+ * Get the operation log store if available
5
+ */
6
+ function getOperationLogStore() {
7
+ try {
8
+ return useOperationLogStore();
9
+ }
10
+ catch {
11
+ // Operation log is optional
12
+ return null;
13
+ }
14
+ }
15
+ /**
16
+ * Global HST Manager (Singleton)
17
+ * Manages hierarchical state trees and provides access to the global registry.
18
+ *
19
+ * @public
20
+ */
21
+ class HST {
22
+ static instance;
23
+ /**
24
+ * Gets the singleton instance of HST
25
+ * @returns The HST singleton instance
26
+ */
27
+ static getInstance() {
28
+ if (!HST.instance) {
29
+ HST.instance = new HST();
30
+ }
31
+ return HST.instance;
32
+ }
33
+ /**
34
+ * Gets the global registry instance
35
+ * @returns The global registry object or undefined if not found
36
+ */
37
+ getRegistry() {
38
+ // In test environment, try different ways to access Registry
39
+ // First, try the global Registry if it exists
40
+ if (typeof globalThis !== 'undefined') {
41
+ const globalRegistry = globalThis.Registry?._root;
42
+ if (globalRegistry) {
43
+ return globalRegistry;
44
+ }
45
+ }
46
+ // Try to access through window (browser environment)
47
+ if (typeof window !== 'undefined') {
48
+ const windowRegistry = window.Registry?._root;
49
+ if (windowRegistry) {
50
+ return windowRegistry;
51
+ }
52
+ }
53
+ // Try to access through global (Node environment)
54
+ if (typeof global !== 'undefined' && global) {
55
+ const nodeRegistry = global.Registry?._root;
56
+ if (nodeRegistry) {
57
+ return nodeRegistry;
58
+ }
59
+ }
60
+ // If we can't find it globally, it might not be set up
61
+ // This is expected in test environments where Registry is created locally
62
+ return undefined;
63
+ }
64
+ /**
65
+ * Helper method to get doctype metadata from the registry
66
+ * @param doctype - The name of the doctype to retrieve metadata for
67
+ * @returns The doctype metadata object or undefined if not found
68
+ */
69
+ getDoctypeMeta(doctype) {
70
+ const registry = this.getRegistry();
71
+ if (registry && typeof registry === 'object' && 'registry' in registry) {
72
+ return registry.registry[doctype];
73
+ }
74
+ return undefined;
75
+ }
76
+ }
77
+ // Enhanced HST Proxy with tree navigation
78
+ class HSTProxy {
79
+ target;
80
+ ancestorPath;
81
+ rootNode;
82
+ doctype;
83
+ hst;
84
+ constructor(target, doctype, ancestorPath = '', rootNode = null) {
85
+ this.target = target;
86
+ this.ancestorPath = ancestorPath;
87
+ this.rootNode = rootNode || this;
88
+ this.doctype = doctype;
89
+ this.hst = HST.getInstance();
90
+ return new Proxy(this, {
91
+ get(hst, prop) {
92
+ // Return HST methods directly
93
+ if (prop in hst)
94
+ return hst[prop];
95
+ // Handle property access - return tree nodes for navigation
96
+ const path = String(prop);
97
+ return hst.getNode(path);
98
+ },
99
+ set(hst, prop, value) {
100
+ const path = String(prop);
101
+ hst.set(path, value);
102
+ return true;
103
+ },
104
+ });
105
+ }
106
+ get(path) {
107
+ return this.resolveValue(path);
108
+ }
109
+ // Method to get a tree-wrapped node for navigation
110
+ getNode(path) {
111
+ const fullPath = this.resolvePath(path);
112
+ const value = this.resolveValue(path);
113
+ // Determine the correct doctype for this node based on the path
114
+ const pathSegments = fullPath.split('.');
115
+ let nodeDoctype = this.doctype;
116
+ // If we're at the root level and this is a StonecropStore, use the first path segment as the doctype
117
+ if (this.doctype === 'StonecropStore' && pathSegments.length >= 1) {
118
+ nodeDoctype = pathSegments[0];
119
+ }
120
+ // Always wrap in HSTProxy for tree navigation
121
+ if (typeof value === 'object' && value !== null && !this.isPrimitive(value)) {
122
+ return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode);
123
+ }
124
+ // For primitives, return a minimal wrapper that throws on tree operations
125
+ return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode);
126
+ }
127
+ set(path, value, source = 'user') {
128
+ // Get current value for change context
129
+ const fullPath = this.resolvePath(path);
130
+ if (fullPath === undefined) {
131
+ // eslint-disable-next-line no-console
132
+ console.warn('HST.set: resolved path is undefined, skipping operation');
133
+ return;
134
+ }
135
+ const beforeValue = this.has(path) ? this.get(path) : undefined;
136
+ // Log operation if not from undo/redo and store is available
137
+ if (source !== 'undo' && source !== 'redo') {
138
+ const logStore = getOperationLogStore();
139
+ if (logStore && typeof logStore.addOperation === 'function') {
140
+ const pathSegments = fullPath.split('.');
141
+ const doctype = this.doctype === 'StonecropStore' && pathSegments.length >= 1 ? pathSegments[0] : this.doctype;
142
+ const recordId = pathSegments.length >= 2 ? pathSegments[1] : undefined;
143
+ const fieldname = pathSegments.slice(2).join('.') || pathSegments[pathSegments.length - 1];
144
+ // Detect if this is a DELETE operation (setting to undefined when a value existed)
145
+ const isDelete = value === undefined && beforeValue !== undefined;
146
+ const operationType = isDelete ? 'delete' : 'set';
147
+ logStore.addOperation({
148
+ type: operationType,
149
+ path: fullPath,
150
+ fieldname,
151
+ beforeValue,
152
+ afterValue: value,
153
+ doctype,
154
+ recordId,
155
+ reversible: true, // Default to reversible, can be changed by field triggers
156
+ }, source);
157
+ }
158
+ }
159
+ // Update the value
160
+ this.updateValue(path, value);
161
+ // Trigger field actions asynchronously (don't block the set operation)
162
+ void this.triggerFieldActions(fullPath, beforeValue, value);
163
+ }
164
+ has(path) {
165
+ try {
166
+ // Handle empty path case
167
+ if (path === '') {
168
+ return true; // empty path refers to the root object
169
+ }
170
+ const segments = this.parsePath(path);
171
+ let current = this.target;
172
+ for (let i = 0; i < segments.length; i++) {
173
+ const segment = segments[i];
174
+ if (current === null || current === undefined) {
175
+ return false;
176
+ }
177
+ // Check if this is the last segment
178
+ if (i === segments.length - 1) {
179
+ // For the final property, check if it exists
180
+ if (this.isImmutable(current)) {
181
+ return current.has(segment);
182
+ }
183
+ else if (this.isPiniaStore(current)) {
184
+ return (current.$state && segment in current.$state) || segment in current;
185
+ }
186
+ else {
187
+ return segment in current;
188
+ }
189
+ }
190
+ // Navigate to the next level
191
+ current = this.getProperty(current, segment);
192
+ }
193
+ return false;
194
+ }
195
+ catch {
196
+ return false;
197
+ }
198
+ }
199
+ // Tree navigation methods
200
+ getAncestor() {
201
+ if (!this.ancestorPath)
202
+ return null;
203
+ const ancestorSegments = this.ancestorPath.split('.').slice(0, -1);
204
+ const ancestorPath = ancestorSegments.join('.');
205
+ if (ancestorPath === '') {
206
+ return this.rootNode;
207
+ }
208
+ // Return a wrapped node, not raw data
209
+ return this.rootNode.getNode(ancestorPath);
210
+ }
211
+ getRoot() {
212
+ return this.rootNode;
213
+ }
214
+ getPath() {
215
+ return this.ancestorPath;
216
+ }
217
+ getDepth() {
218
+ return this.ancestorPath ? this.ancestorPath.split('.').length : 0;
219
+ }
220
+ getBreadcrumbs() {
221
+ return this.ancestorPath ? this.ancestorPath.split('.') : [];
222
+ }
223
+ /**
224
+ * Trigger an XState transition with optional context data
225
+ */
226
+ async triggerTransition(transition, context) {
227
+ const triggerEngine = getGlobalTriggerEngine();
228
+ // Determine doctype and recordId from the current path
229
+ const pathSegments = this.ancestorPath.split('.');
230
+ let doctype = this.doctype;
231
+ let recordId;
232
+ // If we're at the root level and this is a StonecropStore, use the first path segment as the doctype
233
+ if (this.doctype === 'StonecropStore' && pathSegments.length >= 1) {
234
+ doctype = pathSegments[0];
235
+ }
236
+ // Extract recordId from path if it follows the expected pattern
237
+ if (pathSegments.length >= 2) {
238
+ recordId = pathSegments[1];
239
+ }
240
+ // Build transition context
241
+ const transitionContext = {
242
+ path: this.ancestorPath,
243
+ fieldname: '', // No specific field for transitions
244
+ beforeValue: undefined,
245
+ afterValue: undefined,
246
+ operation: 'set',
247
+ doctype,
248
+ recordId,
249
+ timestamp: new Date(),
250
+ store: this.rootNode || undefined,
251
+ transition,
252
+ currentState: context?.currentState,
253
+ targetState: context?.targetState,
254
+ fsmContext: context?.fsmContext,
255
+ };
256
+ // Log FSM transition operation
257
+ const logStore = getOperationLogStore();
258
+ if (logStore && typeof logStore.addOperation === 'function') {
259
+ logStore.addOperation({
260
+ type: 'transition',
261
+ path: this.ancestorPath,
262
+ fieldname: transition,
263
+ beforeValue: context?.currentState,
264
+ afterValue: context?.targetState,
265
+ doctype,
266
+ recordId,
267
+ reversible: false, // FSM transitions are generally not reversible
268
+ metadata: {
269
+ transition,
270
+ currentState: context?.currentState,
271
+ targetState: context?.targetState,
272
+ fsmContext: context?.fsmContext,
273
+ },
274
+ }, 'user');
275
+ }
276
+ // Execute transition actions
277
+ return await triggerEngine.executeTransitionActions(transitionContext);
278
+ }
279
+ // Private helper methods
280
+ resolvePath(path) {
281
+ if (path === '')
282
+ return this.ancestorPath ?? '';
283
+ return this.ancestorPath ? `${this.ancestorPath}.${path}` : path;
284
+ }
285
+ resolveValue(path) {
286
+ // Handle empty path - return the target object
287
+ if (path === '') {
288
+ return this.target;
289
+ }
290
+ const segments = this.parsePath(path);
291
+ let current = this.target;
292
+ for (const segment of segments) {
293
+ if (current === null || current === undefined) {
294
+ return undefined;
295
+ }
296
+ current = this.getProperty(current, segment);
297
+ }
298
+ return current;
299
+ }
300
+ updateValue(path, value) {
301
+ // Handle empty path case - should throw error
302
+ if (path === '') {
303
+ throw new Error('Cannot set value on empty path');
304
+ }
305
+ const segments = this.parsePath(path);
306
+ const lastSegment = segments.pop();
307
+ let current = this.target;
308
+ // Navigate to ancestor object
309
+ for (const segment of segments) {
310
+ current = this.getProperty(current, segment);
311
+ if (current === null || current === undefined) {
312
+ throw new Error(`Cannot set property on null/undefined path: ${path}`);
313
+ }
314
+ }
315
+ // Set the final property
316
+ this.setProperty(current, lastSegment, value);
317
+ }
318
+ getProperty(obj, key) {
319
+ // Immutable objects
320
+ if (this.isImmutable(obj)) {
321
+ return obj.get(key);
322
+ }
323
+ // Vue reactive object
324
+ if (this.isVueReactive(obj)) {
325
+ return obj[key];
326
+ }
327
+ // Pinia store
328
+ if (this.isPiniaStore(obj)) {
329
+ return obj.$state?.[key] ?? obj[key];
330
+ }
331
+ // Plain object
332
+ return obj[key];
333
+ }
334
+ setProperty(obj, key, value) {
335
+ // Immutable objects
336
+ if (this.isImmutable(obj)) {
337
+ throw new Error('Cannot directly mutate immutable objects. Use immutable update methods instead.');
338
+ }
339
+ // Pinia store
340
+ if (this.isPiniaStore(obj)) {
341
+ if (obj.$patch) {
342
+ obj.$patch({ [key]: value });
343
+ }
344
+ else {
345
+ ;
346
+ obj[key] = value;
347
+ }
348
+ return;
349
+ }
350
+ // Vue reactive or plain object
351
+ ;
352
+ obj[key] = value;
353
+ }
354
+ async triggerFieldActions(fullPath, beforeValue, afterValue) {
355
+ try {
356
+ // Guard against undefined or null fullPath
357
+ if (!fullPath || typeof fullPath !== 'string') {
358
+ return;
359
+ }
360
+ // Skip triggering when the value did not actually change
361
+ if (Object.is(beforeValue, afterValue)) {
362
+ return;
363
+ }
364
+ const pathSegments = fullPath.split('.');
365
+ // Only trigger field actions for actual field changes (at least 3 levels deep: doctype.recordId.fieldname)
366
+ // Skip triggering for doctype-level or record-level changes
367
+ if (pathSegments.length < 3) {
368
+ return;
369
+ }
370
+ const triggerEngine = getGlobalTriggerEngine();
371
+ const fieldname = pathSegments.slice(2).join('.') || pathSegments[pathSegments.length - 1];
372
+ // Determine the correct doctype for this path using the same logic as getNode()
373
+ // The path should be in format: "doctype.recordId.fieldname"
374
+ let doctype = this.doctype;
375
+ // If we're at the root level and this is a StonecropStore, use the first path segment as the doctype
376
+ if (this.doctype === 'StonecropStore' && pathSegments.length >= 1) {
377
+ doctype = pathSegments[0];
378
+ }
379
+ let recordId;
380
+ // Extract recordId from path if it follows the expected pattern
381
+ if (pathSegments.length >= 2) {
382
+ recordId = pathSegments[1];
383
+ }
384
+ const context = {
385
+ path: fullPath,
386
+ fieldname,
387
+ beforeValue,
388
+ afterValue,
389
+ operation: 'set',
390
+ doctype,
391
+ recordId,
392
+ timestamp: new Date(),
393
+ store: this.rootNode || undefined, // Pass the root store for snapshot/rollback capabilities
394
+ };
395
+ await triggerEngine.executeFieldTriggers(context);
396
+ }
397
+ catch (error) {
398
+ // Silently handle trigger errors to not break the main flow
399
+ // In production, you might want to log this error
400
+ if (error instanceof Error) {
401
+ // eslint-disable-next-line no-console
402
+ console.warn('Field trigger error:', error.message);
403
+ // Optional: emit an event or call error handler
404
+ }
405
+ }
406
+ }
407
+ isVueReactive(obj) {
408
+ return (obj &&
409
+ typeof obj === 'object' &&
410
+ '__v_isReactive' in obj &&
411
+ obj.__v_isReactive === true);
412
+ }
413
+ isPiniaStore(obj) {
414
+ return obj && typeof obj === 'object' && ('$state' in obj || '$patch' in obj || '$id' in obj);
415
+ }
416
+ isImmutable(obj) {
417
+ if (!obj || typeof obj !== 'object') {
418
+ return false;
419
+ }
420
+ const hasGetMethod = 'get' in obj && typeof obj.get === 'function';
421
+ const hasSetMethod = 'set' in obj && typeof obj.set === 'function';
422
+ const hasHasMethod = 'has' in obj && typeof obj.has === 'function';
423
+ const hasImmutableMarkers = '__ownerID' in obj ||
424
+ '_map' in obj ||
425
+ '_list' in obj ||
426
+ '_origin' in obj ||
427
+ '_capacity' in obj ||
428
+ '_defaultValues' in obj ||
429
+ '_tail' in obj ||
430
+ '_root' in obj ||
431
+ ('size' in obj && hasGetMethod && hasSetMethod);
432
+ let constructorName;
433
+ try {
434
+ const objWithConstructor = obj;
435
+ if ('constructor' in objWithConstructor &&
436
+ objWithConstructor.constructor &&
437
+ typeof objWithConstructor.constructor === 'object' &&
438
+ 'name' in objWithConstructor.constructor) {
439
+ const nameValue = objWithConstructor.constructor.name;
440
+ constructorName = typeof nameValue === 'string' ? nameValue : undefined;
441
+ }
442
+ }
443
+ catch {
444
+ constructorName = undefined;
445
+ }
446
+ const isImmutableConstructor = constructorName &&
447
+ (constructorName.includes('Map') ||
448
+ constructorName.includes('List') ||
449
+ constructorName.includes('Set') ||
450
+ constructorName.includes('Stack') ||
451
+ constructorName.includes('Seq')) &&
452
+ (hasGetMethod || hasSetMethod);
453
+ return Boolean((hasGetMethod && hasSetMethod && hasHasMethod && hasImmutableMarkers) ||
454
+ (hasGetMethod && hasSetMethod && isImmutableConstructor));
455
+ }
456
+ isPrimitive(value) {
457
+ // Don't wrap primitive values, functions, or null/undefined
458
+ return (value === null ||
459
+ value === undefined ||
460
+ typeof value === 'string' ||
461
+ typeof value === 'number' ||
462
+ typeof value === 'boolean' ||
463
+ typeof value === 'function' ||
464
+ typeof value === 'symbol' ||
465
+ typeof value === 'bigint');
466
+ }
467
+ /**
468
+ * Parse a path string into segments, handling both dot notation and array bracket notation
469
+ * @param path - The path string to parse (e.g., "order.456.line_items[0].product")
470
+ * @returns Array of path segments (e.g., ['order', '456', 'line_items', '0', 'product'])
471
+ */
472
+ parsePath(path) {
473
+ if (!path)
474
+ return [];
475
+ // Replace array bracket notation with dot notation
476
+ // items[0] → items.0
477
+ // items[0][1] → items.0.1
478
+ const normalizedPath = path.replace(/\[(\d+)\]/g, '.$1');
479
+ return normalizedPath.split('.').filter(segment => segment.length > 0);
480
+ }
481
+ }
482
+ /**
483
+ * Factory function for HST creation
484
+ * Creates a new HSTNode proxy for hierarchical state tree navigation.
485
+ *
486
+ * @param target - The target object to wrap with HST functionality
487
+ * @param doctype - The document type identifier
488
+ * @returns A new HSTNode proxy instance
489
+ *
490
+ * @public
491
+ */
492
+ function createHST(target, doctype) {
493
+ return new HSTProxy(target, doctype, '', null);
494
+ }
495
+ // Export everything
496
+ export { HSTProxy, HST, createHST };
@@ -0,0 +1,12 @@
1
+ import { createPinia } from 'pinia';
2
+ import { PiniaSharedState } from 'pinia-shared-state';
3
+ import { HST } from './hst';
4
+ const hst = HST.getInstance();
5
+ const pinia = createPinia();
6
+ // Pass the plugin to your application's pinia plugin
7
+ pinia.use(PiniaSharedState({
8
+ enable: true,
9
+ initialize: true,
10
+ }));
11
+ export { hst, pinia };
12
+ export { useOperationLogStore } from './operation-log';