@stonecrop/stonecrop 0.10.16 → 0.11.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 (117) hide show
  1. package/README.md +72 -29
  2. package/dist/composable.js +1 -0
  3. package/dist/composables/lazy-link.js +125 -0
  4. package/dist/composables/stonecrop.js +123 -68
  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 +10 -2
  8. package/dist/field-triggers.js +15 -3
  9. package/dist/index.js +4 -3
  10. package/dist/operation-log-DB-dGNT9.js +593 -0
  11. package/dist/operation-log-DB-dGNT9.js.map +1 -0
  12. package/dist/registry.js +261 -101
  13. package/dist/schema-validator.js +105 -1
  14. package/dist/src/composable.d.ts +11 -0
  15. package/dist/src/composable.d.ts.map +1 -0
  16. package/dist/src/composable.js +477 -0
  17. package/dist/src/composables/lazy-link.d.ts +25 -0
  18. package/dist/src/composables/lazy-link.d.ts.map +1 -0
  19. package/dist/src/composables/operation-log.d.ts +5 -5
  20. package/dist/src/composables/operation-log.d.ts.map +1 -1
  21. package/dist/src/composables/operation-log.js +224 -0
  22. package/dist/src/composables/stonecrop.d.ts +11 -1
  23. package/dist/src/composables/stonecrop.d.ts.map +1 -1
  24. package/dist/src/composables/stonecrop.js +574 -0
  25. package/dist/src/composables/use-lazy-link-state.d.ts +25 -0
  26. package/dist/src/composables/use-lazy-link-state.d.ts.map +1 -0
  27. package/dist/src/composables/use-stonecrop.d.ts +93 -0
  28. package/dist/src/composables/use-stonecrop.d.ts.map +1 -0
  29. package/dist/src/composables/useNestedSchema.d.ts +110 -0
  30. package/dist/src/composables/useNestedSchema.d.ts.map +1 -0
  31. package/dist/src/composables/useNestedSchema.js +155 -0
  32. package/dist/src/doctype.d.ts +9 -1
  33. package/dist/src/doctype.d.ts.map +1 -1
  34. package/dist/src/doctype.js +234 -0
  35. package/dist/src/exceptions.js +16 -0
  36. package/dist/src/field-triggers.d.ts +6 -0
  37. package/dist/src/field-triggers.d.ts.map +1 -1
  38. package/dist/src/field-triggers.js +567 -0
  39. package/dist/src/index.d.ts +3 -2
  40. package/dist/src/index.d.ts.map +1 -1
  41. package/dist/src/index.js +23 -0
  42. package/dist/src/plugins/index.js +96 -0
  43. package/dist/src/registry.d.ts +102 -23
  44. package/dist/src/registry.d.ts.map +1 -1
  45. package/dist/src/registry.js +246 -0
  46. package/dist/src/schema-validator.d.ts +8 -1
  47. package/dist/src/schema-validator.d.ts.map +1 -1
  48. package/dist/src/schema-validator.js +315 -0
  49. package/dist/src/stonecrop.d.ts +73 -28
  50. package/dist/src/stonecrop.d.ts.map +1 -1
  51. package/dist/src/stonecrop.js +339 -0
  52. package/dist/src/stores/data.d.ts +11 -0
  53. package/dist/src/stores/data.d.ts.map +1 -0
  54. package/dist/src/stores/hst.d.ts +5 -75
  55. package/dist/src/stores/hst.d.ts.map +1 -1
  56. package/dist/src/stores/hst.js +495 -0
  57. package/dist/src/stores/index.js +12 -0
  58. package/dist/src/stores/operation-log.d.ts +14 -14
  59. package/dist/src/stores/operation-log.d.ts.map +1 -1
  60. package/dist/src/stores/operation-log.js +568 -0
  61. package/dist/src/stores/xstate.d.ts +31 -0
  62. package/dist/src/stores/xstate.d.ts.map +1 -0
  63. package/dist/src/tsdoc-metadata.json +11 -0
  64. package/dist/src/types/composable.d.ts +50 -12
  65. package/dist/src/types/composable.d.ts.map +1 -1
  66. package/dist/src/types/doctype.d.ts +6 -7
  67. package/dist/src/types/doctype.d.ts.map +1 -1
  68. package/dist/src/types/field-triggers.d.ts +1 -1
  69. package/dist/src/types/field-triggers.d.ts.map +1 -1
  70. package/dist/src/types/field-triggers.js +4 -0
  71. package/dist/src/types/hst.d.ts +70 -0
  72. package/dist/src/types/hst.d.ts.map +1 -0
  73. package/dist/src/types/index.d.ts +1 -0
  74. package/dist/src/types/index.d.ts.map +1 -1
  75. package/dist/src/types/index.js +4 -0
  76. package/dist/src/types/operation-log.d.ts +4 -4
  77. package/dist/src/types/operation-log.d.ts.map +1 -1
  78. package/dist/src/types/operation-log.js +0 -0
  79. package/dist/src/types/registry.js +0 -0
  80. package/dist/src/types/schema-validator.d.ts +2 -0
  81. package/dist/src/types/schema-validator.d.ts.map +1 -1
  82. package/dist/src/utils.d.ts +24 -0
  83. package/dist/src/utils.d.ts.map +1 -0
  84. package/dist/stonecrop.d.ts +317 -99
  85. package/dist/stonecrop.js +2191 -1897
  86. package/dist/stonecrop.js.map +1 -1
  87. package/dist/stonecrop.umd.cjs +6 -0
  88. package/dist/stonecrop.umd.cjs.map +1 -0
  89. package/dist/stores/data.js +7 -0
  90. package/dist/stores/hst.js +27 -25
  91. package/dist/stores/operation-log.js +59 -47
  92. package/dist/stores/xstate.js +29 -0
  93. package/dist/tests/setup.d.ts +5 -0
  94. package/dist/tests/setup.d.ts.map +1 -0
  95. package/dist/tests/setup.js +15 -0
  96. package/dist/types/hst.js +0 -0
  97. package/dist/types/index.js +1 -0
  98. package/dist/utils.js +46 -0
  99. package/package.json +5 -5
  100. package/src/composables/lazy-link.ts +146 -0
  101. package/src/composables/operation-log.ts +1 -1
  102. package/src/composables/stonecrop.ts +142 -73
  103. package/src/doctype.ts +13 -4
  104. package/src/field-triggers.ts +18 -4
  105. package/src/index.ts +4 -2
  106. package/src/registry.ts +289 -111
  107. package/src/schema-validator.ts +120 -1
  108. package/src/stonecrop.ts +230 -106
  109. package/src/stores/hst.ts +29 -104
  110. package/src/stores/operation-log.ts +64 -50
  111. package/src/types/composable.ts +55 -12
  112. package/src/types/doctype.ts +6 -7
  113. package/src/types/field-triggers.ts +1 -1
  114. package/src/types/hst.ts +77 -0
  115. package/src/types/index.ts +1 -0
  116. package/src/types/operation-log.ts +4 -4
  117. package/src/types/schema-validator.ts +2 -0
package/README.md CHANGED
@@ -1,13 +1,14 @@
1
1
  # Stonecrop
2
+
2
3
  _This package is under active development / design._
3
4
 
4
5
  ## Features
5
6
 
7
+ - **Schema-Driven Relationships**: `links` on doctype schemas declare relationships with cardinality and direction
6
8
  - **Hierarchical State Tree (HST)**: Advanced state management with tree navigation
7
9
  - **Operation Log**: Global undo/redo with time-travel debugging, automatic FSM transition tracking, and action execution tracking
8
10
  - **Action Tracking**: Audit trail for stateless action executions (print, email, archive, etc.)
9
11
  - **Field Triggers**: Event-driven field actions integrated with XState
10
- - **VueUse Integration**: Leverages battle-tested VueUse composables for keyboard shortcuts and persistence
11
12
 
12
13
  ## Installation & Usage
13
14
 
@@ -15,7 +16,8 @@ _This package is under active development / design._
15
16
 
16
17
  ```typescript
17
18
  import { createApp } from 'vue'
18
- import Stonecrop from '@stonecrop/stonecrop'
19
+ import Stonecrop, { Stonecrop as StonecropClass } from '@stonecrop/stonecrop'
20
+ import { StonecropClient } from '@stonecrop/graphql-client'
19
21
  import router from './router'
20
22
 
21
23
  const app = createApp(App)
@@ -30,32 +32,66 @@ app.use(Stonecrop, {
30
32
  return await fetchDoctypeMeta(segments[0])
31
33
  },
32
34
 
33
- // Optional: replace the default REST fetch() stub with your own transport.
34
- // When provided, Stonecrop.getRecord() calls this instead of fetch(`/${slug}/${id}`).
35
- fetchRecord: async (doctype, id) => {
36
- return await myApiClient.getRecord(doctype, id)
37
- },
38
-
39
- // Optional: replace the default REST fetch() stub for lists.
40
- fetchRecords: async (doctype) => {
41
- return await myApiClient.getRecords(doctype)
35
+ // Wire up the client after plugin initialization.
36
+ // The callback receives registry and stonecrop instances directly.
37
+ onRouterInitialized: (registry, stonecrop) => {
38
+ const client = new StonecropClient({
39
+ endpoint: 'http://localhost:4000/graphql',
40
+ headers: { Authorization: `Bearer ${token}` },
41
+ registry: buildMetaMap(registry),
42
+ })
43
+ stonecrop.setClient(client)
42
44
  },
43
45
  })
46
+ ```
47
+
48
+ ### Accessing Stonecrop Outside Vue Components
44
49
 
45
- app.mount('#app')
50
+ Inside a component, use `useStonecrop()`. Outside a component (e.g., workflow action handlers, utilities), use `getStonecrop()`:
51
+
52
+ ```typescript
53
+ import { getStonecrop } from '@stonecrop/stonecrop'
54
+
55
+ // In a workflow action handler or non-component utility:
56
+ const stonecrop = getStonecrop()
57
+ if (stonecrop) {
58
+ const payload = stonecrop.collectRecordPayload(doctype, recordId)
59
+ // ...
60
+ }
61
+ ```
62
+
63
+ ### Building the DoctypeMeta Map
64
+
65
+ `StonecropClient` expects a `Map<string, DoctypeMeta>`, but the Registry stores `Doctype` instances. Convert between them:
66
+
67
+ ```typescript
68
+ import type { DoctypeMeta } from '@stonecrop/schema'
69
+ import type { Registry, Doctype } from '@stonecrop/stonecrop'
70
+
71
+ function buildMetaMap(registry: Registry): Map<string, DoctypeMeta> {
72
+ const metaMap = new Map<string, DoctypeMeta>()
73
+ for (const [slug, doctype] of Object.entries(registry.registry)) {
74
+ metaMap.set(slug, {
75
+ name: doctype.doctype,
76
+ slug,
77
+ tableName: slug.replace(/-/g, '_'),
78
+ fields: doctype.getSchemaArray(),
79
+ links: doctype.links || {},
80
+ })
81
+ }
82
+ return metaMap
83
+ }
46
84
  ```
47
85
 
48
86
  ### Plugin Options
49
87
 
50
- | Option | Type | Description |
51
- |--------|------|-------------|
52
- | `router` | `Router` | Vue Router instance. Required for route-based doctype resolution. |
53
- | `getMeta` | `(ctx: RouteContext) => Doctype \| Promise<Doctype>` | Lazy-loads doctype metadata for the current route. `ctx` has `path` and `segments`. |
54
- | `fetchRecord` | `(doctype, id) => Promise<Record \| null>` | Injectable replacement for `Stonecrop.getRecord()`'s default REST fetch. Use this to plug in GraphQL or any other transport. |
55
- | `fetchRecords` | `(doctype) => Promise<Record[]>` | Injectable replacement for `Stonecrop.getRecords()`'s default REST fetch. |
56
- | `components` | `Record<string, Component>` | Additional Vue components to register globally. |
57
- | `autoInitializeRouter` | `boolean` | Call `onRouterInitialized` automatically after mount. Default: `false`. |
58
- | `onRouterInitialized` | `(registry, stonecrop) => void` | Callback invoked after plugin install + mount. Receives the Registry and Stonecrop instances. |
88
+ | Option | Type | Description |
89
+ | ---------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------- |
90
+ | `router` | `Router` | Vue Router instance. Required for route-based doctype resolution. |
91
+ | `getMeta` | `(ctx: RouteContext) => Doctype \| Promise<Doctype>` | Lazy-loads doctype metadata for the current route. `ctx` has `path` and `segments`. |
92
+ | `components` | `Record<string, Component>` | Additional Vue components to register globally. |
93
+ | `autoInitializeRouter` | `boolean` | Call `onRouterInitialized` automatically after mount. Default: `false`. |
94
+ | `onRouterInitialized` | `(registry, stonecrop) => void` | Callback invoked after plugin install + mount. Receives the Registry and Stonecrop instances. |
59
95
 
60
96
  ### Available Imports
61
97
 
@@ -69,6 +105,7 @@ import {
69
105
  Registry, // Doctype registry (singleton)
70
106
  Doctype, // Doctype definition class
71
107
  useStonecrop, // Vue composable — primary integration point
108
+ getStonecrop, // Access singleton outside Vue components
72
109
  HST, // HST store class
73
110
  createHST, // HST factory function
74
111
  } from '@stonecrop/stonecrop'
@@ -118,7 +155,7 @@ When you pass a string doctype slug instead of a `Doctype` instance, `useStonecr
118
155
 
119
156
  ```typescript
120
157
  const { isLoading, error, resolvedDoctype, formData } = useStonecrop({
121
- doctype: 'plan', // string slug - triggers lazy-loading
158
+ doctype: 'plan', // string slug - triggers lazy-loading
122
159
  recordId: '123',
123
160
  })
124
161
 
@@ -131,12 +168,15 @@ const { isLoading, error, resolvedDoctype, formData } = useStonecrop({
131
168
  This pattern eliminates the timing mismatch when loading doctypes asynchronously in Nuxt plugins.
132
169
 
133
170
  ## Design
134
- A Doctype defines schema, workflow, and actions.
135
- - **Schema** describes the data model and field layout — used by AForm for rendering.
136
- - **Workflow** is an XState machine config expressing the states and transitions a record can go through.
137
- - **Actions** are an ordered map of named functions, triggered by field changes (lowercase keys) or FSM transitions (UPPERCASE keys).
138
- - **Registry** is the singleton catalog — all doctypes live here. Optional Vue Router integration allows automatic route creation per doctype.
139
- - **Stem/`useStonecrop()`** is the Vue composable that wires components to HST and provides `formData`, `provideHSTPath`, `handleHSTChange`, and the operation log API.
171
+
172
+ A Doctype defines schema, links, workflow, and actions.
173
+
174
+ - **Schema** describes the data model and field layout used by AForm for rendering.
175
+ - **Links** declare relationships to other doctypes with cardinality and direction (`noneOrMany`, `atMostOne`, etc.).
176
+ - **Workflow** is an XState machine config expressing the states and transitions a record can go through.
177
+ - **Actions** are an ordered map of named functions, triggered by field changes (lowercase keys) or FSM transitions (UPPERCASE keys).
178
+ - **Registry** is the singleton catalog — all doctypes live here. Optional Vue Router integration allows automatic route creation per doctype.
179
+ - **`useStonecrop()`** is the Vue composable that wires components to HST and provides `formData`, `provideHSTPath`, `handleHSTChange`, and the operation log API.
140
180
 
141
181
  The data model is **two operations**: get data and run actions. There is no CRUD. Records change state through FSM transitions; those transitions have side effects (persistence, notifications, etc.) defined in action handlers registered by the application. The framework provides the pipeline; applications define what actions exist and what they do.
142
182
 
@@ -152,16 +192,19 @@ doctype.recordId.nested.field // deep nesting supported
152
192
  ## Core Requirements
153
193
 
154
194
  ### 1. Data Structure Compatibility
195
+
155
196
  - **Vue Reactive Objects**: Must work seamlessly with `reactive()`, `ref()`, and `computed()` primitives
156
197
  - **Pinia Store Integration**: Compatible with both Options API and Composition API Pinia stores
157
198
  - **Immutable Objects**: Support for frozen/immutable configuration objects without breaking reactivity
158
199
 
159
200
  ### 2. Path-Based Addressing System
201
+
160
202
  - **Dot Notation**: Full support for dot-notation paths (e.g., `"users.123.profile.settings"`)
161
203
  - **Dynamic Paths**: Support for programmatically generated path strings (particularly component to HST)
162
204
 
163
205
  ### 3. Hierarchical Navigation
164
- - **Parent/Child Relationships**: Maintain bidirectional parent-child references
206
+
207
+ - **Ancestor/Descendant Relationships**: Maintain bidirectional ancestor-descendant references
165
208
  - **Sibling Access**: Efficient navigation between sibling nodes
166
209
  - **Root Access**: Always accessible reference to tree root from any node
167
210
  - **Depth Tracking**: Know the depth level of any node in the hierarchy
@@ -0,0 +1 @@
1
+ export { useStonecrop } from './composables/stonecrop';
@@ -0,0 +1,125 @@
1
+ import { computed, inject, ref } from 'vue';
2
+ import { Stonecrop } from '../stonecrop';
3
+ /**
4
+ * Get the lazy link state for a specific link field on a doctype record.
5
+ *
6
+ * This composable provides reactive state for lazy-loaded links:
7
+ * - `loading`: true while fetching
8
+ * - `loaded`: true after successful fetch (permanent until reload)
9
+ * - `error`: error state if any
10
+ * - `reload()`: explicitly trigger a fetch
11
+ * - `data`: computed from HST, or undefined if not loaded
12
+ *
13
+ * The reload() function respects the link's fetch strategy:
14
+ * - `sync`: fetches via GraphQL query through fetchNestedData
15
+ * - `lazy`: fetches via GraphQL query through fetchNestedData
16
+ * - `custom`: invokes the serialized handler function directly
17
+ *
18
+ * @param doctype - The doctype instance
19
+ * @param recordId - The record ID
20
+ * @param linkFieldname - The link fieldname to load
21
+ * @returns LazyLink with loading, loaded, error, reload, and data
22
+ * @public
23
+ */
24
+ export function useLazyLink(doctype, recordId, linkFieldname) {
25
+ const stonecropInstance = inject('$stonecrop') || Stonecrop._root;
26
+ if (!stonecropInstance) {
27
+ throw new Error('Stonecrop instance not available. Ensure useStonecrop() has been called first.');
28
+ }
29
+ const loading = ref(false);
30
+ const loaded = ref(false);
31
+ const error = ref(null);
32
+ const hstStore = stonecropInstance.getStore();
33
+ /**
34
+ * Build the HST path for a lazy link field
35
+ */
36
+ const getLinkPath = () => {
37
+ const slug = doctype.slug || doctype.doctype;
38
+ return `${slug}.${recordId}.${linkFieldname}`;
39
+ };
40
+ /**
41
+ * Get the link declaration from the doctype schema
42
+ */
43
+ const getLinkDeclaration = () => {
44
+ return doctype.links?.[linkFieldname]?.fetch;
45
+ };
46
+ /**
47
+ * Invoke a custom fetch handler
48
+ * The handler is a serialized function string that we execute via new Function()
49
+ */
50
+ const invokeCustomHandler = async (handler) => {
51
+ try {
52
+ // Create function from serialized string and invoke it
53
+ // The function receives the stonecrop instance and path as parameters
54
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
55
+ const fn = new Function('stonecrop', 'path', 'hst', `
56
+ return (${handler})(stonecrop, path, hst)
57
+ `);
58
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
59
+ return await fn(stonecropInstance, getLinkPath(), hstStore);
60
+ }
61
+ catch (err) {
62
+ throw new Error(`Custom handler failed: ${err instanceof Error ? err.message : String(err)}`);
63
+ }
64
+ };
65
+ /**
66
+ * Fetch the link data using the appropriate strategy
67
+ */
68
+ const fetchLinkData = async () => {
69
+ const linkFetch = getLinkDeclaration();
70
+ const ancestorPath = `${doctype.slug || doctype.doctype}.${recordId}`;
71
+ if (linkFetch?.method === 'custom') {
72
+ // Ensure ancestor path exists before invoking custom handler
73
+ if (!hstStore.has(ancestorPath)) {
74
+ hstStore.set(ancestorPath, {}, 'system');
75
+ }
76
+ // Custom handler - invoke directly
77
+ const result = await invokeCustomHandler(linkFetch.handler);
78
+ // Store result in HST at the link path
79
+ hstStore.set(getLinkPath(), result, 'system');
80
+ }
81
+ else {
82
+ // sync or lazy (both use fetchNestedData but with different includeNested)
83
+ // For lazy links, we still use fetchNestedData but only for this specific link
84
+ await stonecropInstance.fetchNestedData(ancestorPath, doctype, recordId, { includeNested: [linkFieldname] });
85
+ }
86
+ };
87
+ /**
88
+ * Explicitly reload the lazy link data
89
+ */
90
+ const reload = async () => {
91
+ if (loading.value)
92
+ return;
93
+ loading.value = true;
94
+ error.value = null;
95
+ try {
96
+ await fetchLinkData();
97
+ loaded.value = true;
98
+ }
99
+ catch (err) {
100
+ error.value = err instanceof Error ? err : new Error(String(err));
101
+ throw err;
102
+ }
103
+ finally {
104
+ loading.value = false;
105
+ }
106
+ };
107
+ /**
108
+ * Computed property that returns the loaded data from HST
109
+ */
110
+ const data = computed(() => {
111
+ if (!loaded.value)
112
+ return undefined;
113
+ return hstStore.get(getLinkPath());
114
+ });
115
+ return {
116
+ // State
117
+ loading,
118
+ loaded,
119
+ error,
120
+ // Computed
121
+ data,
122
+ // Actions
123
+ reload,
124
+ };
125
+ }
@@ -21,6 +21,21 @@ export function useStonecrop(options) {
21
21
  const isLoading = ref(false);
22
22
  const error = ref(null);
23
23
  const resolvedDoctype = ref();
24
+ // Workflow readiness computed properties
25
+ const isWorkflowReady = computed(() => {
26
+ if (!stonecrop.value || !resolvedDoctype.value || !options.recordId || options.recordId === 'new') {
27
+ return true;
28
+ }
29
+ const status = stonecrop.value.isWorkflowReady(resolvedDoctype.value, options.recordId);
30
+ return status.ready;
31
+ });
32
+ const blockedLinks = computed(() => {
33
+ if (!stonecrop.value || !resolvedDoctype.value || !options.recordId || options.recordId === 'new') {
34
+ return [];
35
+ }
36
+ const status = stonecrop.value.isWorkflowReady(resolvedDoctype.value, options.recordId);
37
+ return status.blockedLinks ?? [];
38
+ });
24
39
  // Initialize stonecrop instance synchronously using singleton pattern
25
40
  // Use injected instance if available, otherwise fall back to the singleton root
26
41
  const stonecropInstance = providedStonecrop || Stonecrop._root;
@@ -31,7 +46,7 @@ export function useStonecrop(options) {
31
46
  if (options?.doctype && typeof options.doctype !== 'string') {
32
47
  resolvedDoctype.value = options.doctype;
33
48
  }
34
- // Operation log state and methods - will be populated after stonecrop instance is created
49
+ // Operation log state and methods
35
50
  const operations = ref([]);
36
51
  const currentIndex = ref(-1);
37
52
  const canUndo = computed(() => stonecrop.value?.getOperationLogStore().canUndo ?? false);
@@ -85,12 +100,9 @@ export function useStonecrop(options) {
85
100
  const configure = (config) => {
86
101
  stonecrop.value?.getOperationLogStore().configure(config);
87
102
  };
88
- // Initialize Stonecrop instance
89
- onMounted(async () => {
90
- if (!registry || !stonecrop.value) {
91
- return;
92
- }
93
- // Set up reactive refs from operation log store - only if Pinia is available
103
+ // Wire operation log reactive state synchronously — no lifecycle hook needed.
104
+ // storeToRefs and watch are both safe to call in setup() body.
105
+ if (registry && stonecrop.value) {
94
106
  try {
95
107
  const opLogStore = stonecrop.value.getOperationLogStore();
96
108
  const opLogRefs = storeToRefs(opLogStore);
@@ -105,8 +117,29 @@ export function useStonecrop(options) {
105
117
  });
106
118
  }
107
119
  catch {
108
- // Pinia not available (e.g., in tests) - operation log features will not be available
109
- // Silently fail - operation log is optional
120
+ // Pinia not available operation log is optional, silently skip
121
+ }
122
+ }
123
+ // Synchronous HST initialisation for an explicit Doctype instance.
124
+ // When the caller passes a Doctype object (not a slug string), every piece of
125
+ // setup that doesn't require network I/O runs here during setup() so that
126
+ // hstStore, resolvedSchema, and formData are populated before the first render
127
+ // and are immediately available to callers without any await.
128
+ if (options.doctype && typeof options.doctype !== 'string' && registry && stonecrop.value) {
129
+ hstStore.value = stonecrop.value.getStore();
130
+ resolvedSchema.value = registry.resolveSchema(options.doctype);
131
+ if (!options.recordId || options.recordId === 'new') {
132
+ formData.value = registry.initializeRecord(resolvedSchema.value);
133
+ }
134
+ if (hstStore.value) {
135
+ setupDeepReactivity(options.doctype, options.recordId || 'new', formData, hstStore.value);
136
+ }
137
+ }
138
+ // onMounted handles only work that is genuinely async: lazy-loading a doctype
139
+ // by slug, fetching an existing record from the server, and router-based setup.
140
+ onMounted(async () => {
141
+ if (!registry || !stonecrop.value) {
142
+ return;
110
143
  }
111
144
  // Handle router-based setup if no specific doctype provided
112
145
  if (!options.doctype && registry.router) {
@@ -132,12 +165,7 @@ export function useStonecrop(options) {
132
165
  hstStore.value = stonecrop.value.getStore();
133
166
  // Resolve schema for router-loaded doctype
134
167
  if (registry) {
135
- const schemaArray = doctype.schema
136
- ? Array.isArray(doctype.schema)
137
- ? doctype.schema
138
- : Array.from(doctype.schema)
139
- : [];
140
- resolvedSchema.value = registry.resolveSchema(schemaArray);
168
+ resolvedSchema.value = registry.resolveSchema(doctype);
141
169
  }
142
170
  if (recordId && recordId !== 'new') {
143
171
  const existingRecord = stonecrop.value.getRecordById(doctype, recordId);
@@ -169,15 +197,14 @@ export function useStonecrop(options) {
169
197
  }
170
198
  // Handle HST integration if doctype is provided explicitly
171
199
  if (options.doctype) {
172
- hstStore.value = stonecrop.value.getStore();
173
200
  const recordId = options.recordId;
174
- // Resolve doctype - handle string (lazy-load) or Doctype instance
175
- let doctype;
176
201
  if (typeof options.doctype === 'string') {
177
- // String doctype - check registry first, then lazy-load
202
+ // String doctype resolve lazily, then do full sync-equivalent setup here.
178
203
  const doctypeSlug = options.doctype;
204
+ hstStore.value = stonecrop.value.getStore();
179
205
  isLoading.value = true;
180
206
  error.value = null;
207
+ let doctype;
181
208
  try {
182
209
  // Check if already in registry
183
210
  doctype = registry.getDoctype(doctypeSlug);
@@ -202,47 +229,57 @@ export function useStonecrop(options) {
202
229
  finally {
203
230
  isLoading.value = false;
204
231
  }
205
- }
206
- else {
207
- // Doctype instance provided directly
208
- doctype = options.doctype;
209
- }
210
- // Set resolved doctype for consumers
211
- resolvedDoctype.value = doctype;
212
- if (!doctype) {
213
- // Error already set above, just return
214
- return;
215
- }
216
- // Resolve schema for the doctype
217
- const schemaArray = doctype.schema
218
- ? Array.isArray(doctype.schema)
219
- ? doctype.schema
220
- : Array.from(doctype.schema)
221
- : [];
222
- resolvedSchema.value = registry.resolveSchema(schemaArray);
223
- if (recordId && recordId !== 'new') {
224
- const existingRecord = stonecrop.value.getRecordById(doctype, recordId);
225
- if (existingRecord) {
226
- formData.value = existingRecord.get('') || {};
227
- }
228
- else {
229
- try {
230
- await stonecrop.value.getRecord(doctype, recordId);
231
- const loadedRecord = stonecrop.value.getRecordById(doctype, recordId);
232
- if (loadedRecord) {
233
- formData.value = loadedRecord.get('') || {};
234
- }
232
+ resolvedDoctype.value = doctype;
233
+ if (!doctype)
234
+ return;
235
+ resolvedSchema.value = registry.resolveSchema(doctype);
236
+ if (recordId && recordId !== 'new') {
237
+ const existingRecord = stonecrop.value.getRecordById(doctype, recordId);
238
+ if (existingRecord) {
239
+ formData.value = existingRecord.get('') || {};
235
240
  }
236
- catch {
237
- formData.value = registry.initializeRecord(resolvedSchema.value);
241
+ else {
242
+ try {
243
+ await stonecrop.value.getRecord(doctype, recordId);
244
+ const loadedRecord = stonecrop.value.getRecordById(doctype, recordId);
245
+ if (loadedRecord) {
246
+ formData.value = loadedRecord.get('') || {};
247
+ }
248
+ }
249
+ catch {
250
+ formData.value = registry.initializeRecord(resolvedSchema.value);
251
+ }
238
252
  }
239
253
  }
254
+ else {
255
+ formData.value = registry.initializeRecord(resolvedSchema.value);
256
+ }
257
+ if (hstStore.value) {
258
+ setupDeepReactivity(doctype, recordId || 'new', formData, hstStore.value);
259
+ }
240
260
  }
241
261
  else {
242
- formData.value = registry.initializeRecord(resolvedSchema.value);
243
- }
244
- if (hstStore.value) {
245
- setupDeepReactivity(doctype, recordId || 'new', formData, hstStore.value);
262
+ // Doctype instance — sync init was done during setup().
263
+ // Only handle the async path: fetching an existing record from the server.
264
+ if (recordId && recordId !== 'new') {
265
+ const doctype = options.doctype;
266
+ const existingRecord = stonecrop.value.getRecordById(doctype, recordId);
267
+ if (existingRecord) {
268
+ formData.value = existingRecord.get('') || {};
269
+ }
270
+ else {
271
+ try {
272
+ await stonecrop.value.getRecord(doctype, recordId);
273
+ const loadedRecord = stonecrop.value.getRecordById(doctype, recordId);
274
+ if (loadedRecord) {
275
+ formData.value = loadedRecord.get('') || {};
276
+ }
277
+ }
278
+ catch {
279
+ formData.value = registry.initializeRecord(resolvedSchema.value);
280
+ }
281
+ }
282
+ }
246
283
  }
247
284
  }
248
285
  });
@@ -302,18 +339,30 @@ export function useStonecrop(options) {
302
339
  provide('hstChangeHandler', handleHSTChange);
303
340
  }
304
341
  /**
305
- * Load nested doctype data from API or initialize empty structure
306
- * Delegates to Stonecrop.loadNestedData method
307
- * @param parentPath - The parent path (e.g., "customer.123.address")
308
- * @param childDoctype - The child doctype metadata
309
- * @param recordId - Optional record ID to load
310
- * @returns The loaded or initialized data
342
+ * Scaffold empty descendant records from defaults for all descendant links.
343
+ * Delegates to Stonecrop.initializeNestedData method.
344
+ * @param path - The HST path where initialized data should be stored
345
+ * @param doctype - The doctype to initialize
346
+ */
347
+ const initializeNestedData = (path, doctype) => {
348
+ if (!stonecrop.value) {
349
+ throw new Error('Stonecrop instance not available');
350
+ }
351
+ return stonecrop.value.initializeNestedData(path, doctype);
352
+ };
353
+ /**
354
+ * Fetch a record and its nested data from the server.
355
+ * Delegates to Stonecrop.fetchNestedData method.
356
+ * @param path - The HST path (e.g., "recipe.r1")
357
+ * @param doctype - The doctype to fetch
358
+ * @param recordId - Record ID to fetch
359
+ * @param options - Query options (includeNested to control which links are fetched)
311
360
  */
312
- const loadNestedData = (parentPath, childDoctype, recordId) => {
361
+ const fetchNestedData = async (path, doctype, recordId, options) => {
313
362
  if (!stonecrop.value) {
314
363
  throw new Error('Stonecrop instance not available');
315
364
  }
316
- return stonecrop.value.loadNestedData(parentPath, childDoctype, recordId);
365
+ return stonecrop.value.fetchNestedData(path, doctype, recordId, options);
317
366
  };
318
367
  /**
319
368
  * Collect a record payload with all nested doctype fields from HST
@@ -329,12 +378,12 @@ export function useStonecrop(options) {
329
378
  return stonecrop.value.collectRecordPayload(doctype, recordId);
330
379
  };
331
380
  /**
332
- * Create a nested context for child forms
381
+ * Create a nested context for descendant forms
333
382
  * @param basePath - The base path for the nested context (e.g., "customer.123.address")
334
- * @param _childDoctype - The child doctype metadata (unused but kept for API consistency)
383
+ * @param _descendantDoctype - The descendant doctype metadata (unused but kept for API consistency)
335
384
  * @returns Object with scoped provideHSTPath and handleHSTChange
336
385
  */
337
- const createNestedContext = (basePath, _childDoctype) => {
386
+ const createNestedContext = (basePath, _descendantDoctype) => {
338
387
  const nestedProvideHSTPath = (fieldname) => {
339
388
  return `${basePath}.${fieldname}`;
340
389
  };
@@ -383,12 +432,15 @@ export function useStonecrop(options) {
383
432
  hstStore,
384
433
  formData,
385
434
  resolvedSchema,
386
- loadNestedData,
435
+ initializeNestedData,
436
+ fetchNestedData,
387
437
  collectRecordPayload,
388
438
  createNestedContext,
389
439
  isLoading,
390
440
  error,
391
441
  resolvedDoctype,
442
+ isWorkflowReady,
443
+ blockedLinks,
392
444
  };
393
445
  }
394
446
  else if (!options.doctype && registry?.router) {
@@ -401,12 +453,15 @@ export function useStonecrop(options) {
401
453
  hstStore,
402
454
  formData,
403
455
  resolvedSchema,
404
- loadNestedData,
456
+ initializeNestedData,
457
+ fetchNestedData,
405
458
  collectRecordPayload,
406
459
  createNestedContext,
407
460
  isLoading,
408
461
  error,
409
462
  resolvedDoctype,
463
+ isWorkflowReady,
464
+ blockedLinks,
410
465
  };
411
466
  }
412
467
  // No doctype and no router - basic mode