@stonecrop/stonecrop 0.13.3 → 0.13.5
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/dist/src/composables/lazy-link.d.ts.map +1 -1
- package/dist/src/composables/lazy-link.js +1 -2
- package/dist/src/composables/stonecrop.js +8 -8
- package/dist/src/doctype.d.ts.map +1 -1
- package/dist/src/doctype.js +2 -0
- package/dist/src/field-triggers.d.ts.map +1 -1
- package/dist/src/field-triggers.js +11 -9
- package/dist/src/plugins/index.d.ts.map +1 -1
- package/dist/src/plugins/index.js +0 -1
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +7 -6
- package/dist/src/schema-validator.d.ts.map +1 -1
- package/dist/src/schema-validator.js +10 -0
- package/dist/src/stonecrop.d.ts.map +1 -1
- package/dist/src/stonecrop.js +9 -1
- package/dist/src/stores/hst.d.ts.map +1 -1
- package/dist/src/stores/hst.js +5 -14
- package/dist/src/stores/operation-log.d.ts.map +1 -1
- package/dist/src/stores/operation-log.js +21 -29
- package/dist/src/types/composable.d.ts +1 -1
- package/dist/src/types/composable.d.ts.map +1 -1
- package/dist/stonecrop.d.ts +1 -1
- package/dist/stonecrop.js +1328 -1334
- package/dist/stonecrop.js.map +1 -1
- package/package.json +12 -15
- package/src/composables/lazy-link.ts +1 -2
- package/src/composables/stonecrop.ts +10 -10
- package/src/doctype.ts +2 -0
- package/src/field-triggers.ts +11 -9
- package/src/plugins/index.ts +0 -1
- package/src/registry.ts +11 -8
- package/src/schema-validator.ts +10 -0
- package/src/stonecrop.ts +10 -3
- package/src/stores/hst.ts +13 -30
- package/src/stores/operation-log.ts +25 -31
- package/src/types/composable.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stonecrop/stonecrop",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"pinia-shared-state": "^1.0.1",
|
|
35
35
|
"pinia-xstate": "^3.0.0",
|
|
36
36
|
"xstate": "^5.25.0",
|
|
37
|
-
"@stonecrop/schema": "0.13.
|
|
37
|
+
"@stonecrop/schema": "0.13.5"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"pinia": "^3.0.4",
|
|
@@ -42,27 +42,23 @@
|
|
|
42
42
|
"vue-router": "^5.0.6"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@eslint/js": "^10.0.1",
|
|
46
45
|
"@microsoft/api-documenter": "^7.30.5",
|
|
47
46
|
"@rushstack/heft": "^1.2.17",
|
|
48
47
|
"@vitejs/plugin-vue": "^6.0.6",
|
|
49
48
|
"@vitest/coverage-istanbul": "^4.1.5",
|
|
50
49
|
"@vue/test-utils": "^2.4.10",
|
|
51
|
-
"eslint": "^10.3.0",
|
|
52
|
-
"eslint-config-prettier": "^10.1.8",
|
|
53
|
-
"eslint-plugin-vue": "^10.9.0",
|
|
54
|
-
"globals": "^17.6.0",
|
|
55
50
|
"jsdom": "^29.1.1",
|
|
56
|
-
"
|
|
51
|
+
"oxlint": "1.67.0",
|
|
52
|
+
"oxlint-tsgolint": "0.23.0",
|
|
57
53
|
"pinia": "^3.0.4",
|
|
58
|
-
"typescript
|
|
59
|
-
"vue": "^3.5.33",
|
|
60
|
-
"vue-router": "^5.0.6",
|
|
54
|
+
"typescript": "^6.0.3",
|
|
61
55
|
"vite": "^7.3.2",
|
|
62
56
|
"vitest": "^4.1.5",
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"stonecrop
|
|
57
|
+
"vue": "^3.5.33",
|
|
58
|
+
"vue-router": "^5.0.6",
|
|
59
|
+
"@stonecrop/aform": "0.13.5",
|
|
60
|
+
"stonecrop-rig": "0.7.0",
|
|
61
|
+
"@stonecrop/atable": "0.13.5"
|
|
66
62
|
},
|
|
67
63
|
"description": "Schema-driven framework with XState workflows and HST state management",
|
|
68
64
|
"publishConfig": {
|
|
@@ -75,7 +71,8 @@
|
|
|
75
71
|
"_phase:build": "heft build && vite build && rushx docs",
|
|
76
72
|
"build": "heft build && vite build && rushx docs",
|
|
77
73
|
"docs": "bash ../common/scripts/run-docs.sh stonecrop",
|
|
78
|
-
"lint": "
|
|
74
|
+
"lint": "oxlint",
|
|
75
|
+
"lint:fix": "oxlint --fix",
|
|
79
76
|
"preview": "vite preview",
|
|
80
77
|
"test": "vitest run --coverage.enabled false",
|
|
81
78
|
"test:watch": "vitest watch",
|
|
@@ -62,7 +62,7 @@ export function useLazyLink(doctype: Doctype, recordId: string, linkFieldname: s
|
|
|
62
62
|
try {
|
|
63
63
|
// Create function from serialized string and invoke it
|
|
64
64
|
// The function receives the stonecrop instance and path as parameters
|
|
65
|
-
//
|
|
65
|
+
// oxlint-disable-next-line @typescript-eslint/no-implied-eval
|
|
66
66
|
const fn = new Function(
|
|
67
67
|
'stonecrop',
|
|
68
68
|
'path',
|
|
@@ -71,7 +71,6 @@ export function useLazyLink(doctype: Doctype, recordId: string, linkFieldname: s
|
|
|
71
71
|
return (${handler})(stonecrop, path, hst)
|
|
72
72
|
`
|
|
73
73
|
)
|
|
74
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
75
74
|
return await fn(stonecropInstance, getLinkPath(), hstStore)
|
|
76
75
|
} catch (err) {
|
|
77
76
|
throw new Error(`Custom handler failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err })
|
|
@@ -116,12 +116,12 @@ export function useStonecrop(options?: {
|
|
|
116
116
|
)
|
|
117
117
|
|
|
118
118
|
// Operation log methods
|
|
119
|
-
const undo = (
|
|
120
|
-
return stonecrop.value?.getOperationLogStore().undo(
|
|
119
|
+
const undo = (hstNode: HSTNode): boolean => {
|
|
120
|
+
return stonecrop.value?.getOperationLogStore().undo(hstNode) ?? false
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
const redo = (
|
|
124
|
-
return stonecrop.value?.getOperationLogStore().redo(
|
|
123
|
+
const redo = (hstNode: HSTNode): boolean => {
|
|
124
|
+
return stonecrop.value?.getOperationLogStore().redo(hstNode) ?? false
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
const startBatch = () => {
|
|
@@ -165,9 +165,9 @@ export function useStonecrop(options?: {
|
|
|
165
165
|
actionName: string,
|
|
166
166
|
recordIds?: string[],
|
|
167
167
|
result: 'success' | 'failure' | 'pending' = 'success',
|
|
168
|
-
|
|
168
|
+
errorMessage?: string
|
|
169
169
|
): string => {
|
|
170
|
-
return stonecrop.value?.getOperationLogStore().logAction(doctype, actionName, recordIds, result,
|
|
170
|
+
return stonecrop.value?.getOperationLogStore().logAction(doctype, actionName, recordIds, result, errorMessage) ?? ''
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
const configure = (config: Partial<OperationLogConfig>) => {
|
|
@@ -462,12 +462,12 @@ export function useStonecrop(options?: {
|
|
|
462
462
|
path: string,
|
|
463
463
|
doctype: Doctype,
|
|
464
464
|
recordId: string,
|
|
465
|
-
|
|
465
|
+
fetchOptions?: { includeNested?: boolean | string[] }
|
|
466
466
|
): Promise<void> => {
|
|
467
467
|
if (!stonecrop.value) {
|
|
468
468
|
throw new Error('Stonecrop instance not available')
|
|
469
469
|
}
|
|
470
|
-
return stonecrop.value.fetchNestedData(path, doctype, recordId,
|
|
470
|
+
return stonecrop.value.fetchNestedData(path, doctype, recordId, fetchOptions)
|
|
471
471
|
}
|
|
472
472
|
|
|
473
473
|
/**
|
|
@@ -613,7 +613,7 @@ function setupDeepReactivity(
|
|
|
613
613
|
* Update nested object with dot-notation path
|
|
614
614
|
*/
|
|
615
615
|
function updateNestedObject(obj: any, path: string[], value: any): void {
|
|
616
|
-
let current
|
|
616
|
+
let current: Record<string, any> = obj
|
|
617
617
|
|
|
618
618
|
for (let i = 0; i < path.length - 1; i++) {
|
|
619
619
|
const key = path[i]
|
|
@@ -622,7 +622,7 @@ function updateNestedObject(obj: any, path: string[], value: any): void {
|
|
|
622
622
|
current[key] = isNaN(Number(path[i + 1])) ? {} : []
|
|
623
623
|
}
|
|
624
624
|
|
|
625
|
-
current = current[key]
|
|
625
|
+
current = current[key]
|
|
626
626
|
}
|
|
627
627
|
|
|
628
628
|
const finalKey = path[path.length - 1]
|
package/src/doctype.ts
CHANGED
|
@@ -190,6 +190,7 @@ export default class Doctype {
|
|
|
190
190
|
const states = workflow.states
|
|
191
191
|
if (!states.includes(currentState)) return []
|
|
192
192
|
|
|
193
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Array.isArray(workflow.states) confirms WorkflowMeta format above
|
|
193
194
|
const actions = (workflow as WorkflowMeta).actions
|
|
194
195
|
if (!actions) return []
|
|
195
196
|
|
|
@@ -246,6 +247,7 @@ export default class Doctype {
|
|
|
246
247
|
const workflow = this.workflow
|
|
247
248
|
if (!workflow || !Array.isArray(workflow.states)) return undefined
|
|
248
249
|
|
|
250
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Array.isArray(workflow.states) confirms WorkflowMeta format above
|
|
249
251
|
const actions = (workflow as WorkflowMeta).actions
|
|
250
252
|
return actions?.[actionName]
|
|
251
253
|
}
|
package/src/field-triggers.ts
CHANGED
|
@@ -45,6 +45,7 @@ export class FieldTriggerEngine {
|
|
|
45
45
|
enableRollback: options.enableRollback ?? true,
|
|
46
46
|
errorHandler: options.errorHandler,
|
|
47
47
|
}
|
|
48
|
+
return this
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
/**
|
|
@@ -111,6 +112,7 @@ export class FieldTriggerEngine {
|
|
|
111
112
|
|
|
112
113
|
// Convert from different Map types to regular Map
|
|
113
114
|
// Check for Immutable.js Map first (has entrySeq method)
|
|
115
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Immutable.js interop: runtime duck-type check follows
|
|
114
116
|
const immutableActions = actions as ImmutableMap<string, string[]>
|
|
115
117
|
if (typeof immutableActions.entrySeq === 'function') {
|
|
116
118
|
// Immutable Map
|
|
@@ -125,6 +127,7 @@ export class FieldTriggerEngine {
|
|
|
125
127
|
} else if (actions && typeof actions === 'object') {
|
|
126
128
|
// Plain object
|
|
127
129
|
Object.entries(actions).forEach(([key, value]) => {
|
|
130
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Object.entries value type is unknown at runtime; guarded by typeof object check above
|
|
128
131
|
this.categorizeAction(key, value as string[], actionMap, transitionMap)
|
|
129
132
|
})
|
|
130
133
|
}
|
|
@@ -199,9 +202,10 @@ export class FieldTriggerEngine {
|
|
|
199
202
|
snapshot = this.captureSnapshot(context)
|
|
200
203
|
}
|
|
201
204
|
|
|
202
|
-
// Execute actions sequentially
|
|
205
|
+
// Execute actions sequentially — each action may depend on state set by the previous; stop-on-error semantics require sequential execution
|
|
203
206
|
for (const actionName of triggers) {
|
|
204
207
|
try {
|
|
208
|
+
// oxlint-disable-next-line eslint/no-await-in-loop -- intentionally sequential; actions are order-dependent and stop on first failure
|
|
205
209
|
const actionResult = await this.executeAction(actionName, context, options.timeout)
|
|
206
210
|
actionResults.push(actionResult)
|
|
207
211
|
|
|
@@ -229,7 +233,6 @@ export class FieldTriggerEngine {
|
|
|
229
233
|
this.restoreSnapshot(context, snapshot)
|
|
230
234
|
rolledBack = true
|
|
231
235
|
} catch (rollbackError) {
|
|
232
|
-
// eslint-disable-next-line no-console
|
|
233
236
|
console.error('[FieldTriggers] Rollback failed:', rollbackError)
|
|
234
237
|
}
|
|
235
238
|
}
|
|
@@ -245,7 +248,6 @@ export class FieldTriggerEngine {
|
|
|
245
248
|
this.options.errorHandler(failedResult.error, context, failedResult.action)
|
|
246
249
|
}
|
|
247
250
|
} catch (handlerError) {
|
|
248
|
-
// eslint-disable-next-line no-console
|
|
249
251
|
console.error('[FieldTriggers] Error in global error handler:', handlerError)
|
|
250
252
|
}
|
|
251
253
|
}
|
|
@@ -283,9 +285,10 @@ export class FieldTriggerEngine {
|
|
|
283
285
|
|
|
284
286
|
const results: TransitionExecutionResult[] = []
|
|
285
287
|
|
|
286
|
-
// Execute transition actions sequentially
|
|
288
|
+
// Execute transition actions sequentially — transitions are state-machine steps; order and stop-on-error are required
|
|
287
289
|
for (const actionName of transitionActions) {
|
|
288
290
|
try {
|
|
291
|
+
// oxlint-disable-next-line eslint/no-await-in-loop -- intentionally sequential; transition actions are order-dependent and stop on first failure
|
|
289
292
|
const actionResult = await this.executeTransitionAction(actionName, context, options.timeout)
|
|
290
293
|
results.push(actionResult)
|
|
291
294
|
|
|
@@ -314,10 +317,10 @@ export class FieldTriggerEngine {
|
|
|
314
317
|
try {
|
|
315
318
|
// Call with FieldChangeContext (base context type)
|
|
316
319
|
if (failedResult.error) {
|
|
320
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- action result carries string name; FieldAction cast required by errorHandler signature
|
|
317
321
|
this.options.errorHandler(failedResult.error, context, failedResult.action as unknown as FieldAction)
|
|
318
322
|
}
|
|
319
323
|
} catch (handlerError) {
|
|
320
|
-
// eslint-disable-next-line no-console
|
|
321
324
|
console.error('[FieldTriggers] Error in global error handler:', handlerError)
|
|
322
325
|
}
|
|
323
326
|
}
|
|
@@ -365,6 +368,7 @@ export class FieldTriggerEngine {
|
|
|
365
368
|
throw new Error(`Transition action "${actionName}" not found in registry`)
|
|
366
369
|
}
|
|
367
370
|
|
|
371
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- actionFn may be FieldActionFunction set from globalActions fallback; polymorphic function type
|
|
368
372
|
await this.executeWithTimeout(actionFn as FieldActionFunction, context, actionTimeout)
|
|
369
373
|
const executionTime = performance.now() - startTime
|
|
370
374
|
|
|
@@ -515,7 +519,7 @@ export class FieldTriggerEngine {
|
|
|
515
519
|
Promise.resolve(fn(context))
|
|
516
520
|
.then(result => {
|
|
517
521
|
clearTimeout(timeoutId)
|
|
518
|
-
resolve(result)
|
|
522
|
+
return resolve(result)
|
|
519
523
|
})
|
|
520
524
|
.catch(error => {
|
|
521
525
|
clearTimeout(timeoutId)
|
|
@@ -548,7 +552,6 @@ export class FieldTriggerEngine {
|
|
|
548
552
|
return JSON.parse(JSON.stringify(recordData))
|
|
549
553
|
} catch (error) {
|
|
550
554
|
if (this.options.debug) {
|
|
551
|
-
// eslint-disable-next-line no-console
|
|
552
555
|
console.warn('[FieldTriggers] Failed to capture snapshot:', error)
|
|
553
556
|
}
|
|
554
557
|
return undefined
|
|
@@ -572,11 +575,10 @@ export class FieldTriggerEngine {
|
|
|
572
575
|
context.store.set(recordPath, snapshot)
|
|
573
576
|
|
|
574
577
|
if (this.options.debug) {
|
|
575
|
-
//
|
|
578
|
+
// oxlint-disable-next-line no-console
|
|
576
579
|
console.log(`[FieldTriggers] Rolled back ${recordPath} to previous state`)
|
|
577
580
|
}
|
|
578
581
|
} catch (error) {
|
|
579
|
-
// eslint-disable-next-line no-console
|
|
580
582
|
console.error('[FieldTriggers] Failed to restore snapshot:', error)
|
|
581
583
|
throw error
|
|
582
584
|
}
|
package/src/plugins/index.ts
CHANGED
|
@@ -93,7 +93,6 @@ const plugin: Plugin = {
|
|
|
93
93
|
}
|
|
94
94
|
} catch (error) {
|
|
95
95
|
// Pinia not available - operation log won't work, but app should still function
|
|
96
|
-
// eslint-disable-next-line no-console
|
|
97
96
|
console.warn('Pinia not available - operation log features will be disabled:', error)
|
|
98
97
|
}
|
|
99
98
|
|
package/src/registry.ts
CHANGED
|
@@ -67,6 +67,7 @@ export default class Registry {
|
|
|
67
67
|
Registry._root = this
|
|
68
68
|
this.router = router
|
|
69
69
|
this.getMeta = getMeta
|
|
70
|
+
return this
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
/**
|
|
@@ -161,11 +162,12 @@ export default class Registry {
|
|
|
161
162
|
if ('fieldtype' in field && field.fieldtype === 'Link') {
|
|
162
163
|
const link = linksByFieldname.get(field.fieldname)
|
|
163
164
|
if (!link) {
|
|
164
|
-
|
|
165
|
+
// oxlint-disable typescript/no-unsafe-type-assertion -- SchemaTypes union narrowed to FieldMeta by fieldtype === 'Link' check; options may not exist on all members
|
|
166
|
+
const linkDoctype =
|
|
165
167
|
typeof (field as FieldMeta).options === 'string' ? ((field as FieldMeta).options as string) : undefined
|
|
168
|
+
// oxlint-enable typescript/no-unsafe-type-assertion
|
|
166
169
|
|
|
167
|
-
if (
|
|
168
|
-
// eslint-disable-next-line no-console
|
|
170
|
+
if (linkDoctype === undefined) {
|
|
169
171
|
console.warn(
|
|
170
172
|
`[Stonecrop] Link field "${field.fieldname}" has no \`options\` or corresponding \`links\` declaration. ` +
|
|
171
173
|
`AFormLink will be created without a \`doctype\` prop, so navigation will not work. ` +
|
|
@@ -182,7 +184,7 @@ export default class Registry {
|
|
|
182
184
|
resolvedFields.push({
|
|
183
185
|
...fieldRest,
|
|
184
186
|
component: fieldRest.component || 'AFormLink',
|
|
185
|
-
...(
|
|
187
|
+
...(linkDoctype !== undefined ? { doctype: linkDoctype } : {}),
|
|
186
188
|
})
|
|
187
189
|
|
|
188
190
|
continue
|
|
@@ -425,10 +427,11 @@ export default class Registry {
|
|
|
425
427
|
const doctype = this.registry[doctypeSlug]
|
|
426
428
|
if (!doctype?.links) return []
|
|
427
429
|
|
|
428
|
-
return Object.entries(doctype.links).map(([fieldname, link]) =>
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
430
|
+
return Object.entries(doctype.links).map(([fieldname, link]) =>
|
|
431
|
+
Object.assign({}, link, {
|
|
432
|
+
fieldname,
|
|
433
|
+
})
|
|
434
|
+
)
|
|
432
435
|
}
|
|
433
436
|
|
|
434
437
|
/**
|
package/src/schema-validator.ts
CHANGED
|
@@ -80,6 +80,7 @@ export class SchemaValidator {
|
|
|
80
80
|
// Validate action registration
|
|
81
81
|
if (this.options.validateActions && actions) {
|
|
82
82
|
const actionsMap = actions instanceof Map ? actions : actions.toObject()
|
|
83
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- toObject() returns plain object; runtime-safe cast for Map<string, string[]>
|
|
83
84
|
issues.push(...this.validateActionRegistration(doctype, actionsMap as Record<string, string[]>))
|
|
84
85
|
}
|
|
85
86
|
|
|
@@ -130,10 +131,13 @@ export class SchemaValidator {
|
|
|
130
131
|
|
|
131
132
|
// Validate nested schemas (recursively)
|
|
132
133
|
if ('schema' in field) {
|
|
134
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- 'schema' in field guard confirms property exists; SchemaTypes union narrowed at runtime
|
|
133
135
|
const nestedSchema = (field as { schema: unknown }).schema
|
|
136
|
+
// oxlint-disable typescript/no-unsafe-type-assertion -- Immutable List or plain array; toArray() returns SchemaTypes elements
|
|
134
137
|
const nestedArray = (
|
|
135
138
|
Array.isArray(nestedSchema) ? nestedSchema : (nestedSchema as { toArray?: () => unknown[] }).toArray?.() || []
|
|
136
139
|
) as SchemaTypes[]
|
|
140
|
+
// oxlint-enable typescript/no-unsafe-type-assertion
|
|
137
141
|
issues.push(...this.validateRequiredProperties(doctype, nestedArray))
|
|
138
142
|
}
|
|
139
143
|
}
|
|
@@ -149,6 +153,7 @@ export class SchemaValidator {
|
|
|
149
153
|
const issues: ValidationIssue[] = []
|
|
150
154
|
|
|
151
155
|
for (const field of schema) {
|
|
156
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- 'fieldtype' in field guard confirms property exists; accessing unknown-typed fieldtype safely
|
|
152
157
|
const fieldtype = 'fieldtype' in field ? (field as { fieldtype: unknown }).fieldtype : undefined
|
|
153
158
|
|
|
154
159
|
// Check Link fields
|
|
@@ -194,10 +199,13 @@ export class SchemaValidator {
|
|
|
194
199
|
|
|
195
200
|
// Recursively check nested schemas
|
|
196
201
|
if ('schema' in field) {
|
|
202
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- 'schema' in field guard confirms property exists; SchemaTypes union narrowed at runtime
|
|
197
203
|
const nestedSchema = (field as { schema: unknown }).schema
|
|
204
|
+
// oxlint-disable typescript/no-unsafe-type-assertion -- Immutable List or plain array; toArray() returns SchemaTypes elements
|
|
198
205
|
const nestedArray = (
|
|
199
206
|
Array.isArray(nestedSchema) ? nestedSchema : (nestedSchema as { toArray?: () => unknown[] }).toArray?.() || []
|
|
200
207
|
) as SchemaTypes[]
|
|
208
|
+
// oxlint-enable typescript/no-unsafe-type-assertion
|
|
201
209
|
issues.push(...this.validateLinkFields(doctype, nestedArray, registry))
|
|
202
210
|
}
|
|
203
211
|
}
|
|
@@ -371,6 +379,7 @@ export class SchemaValidator {
|
|
|
371
379
|
if (typeof transition === 'string') {
|
|
372
380
|
reachableStates.add(transition)
|
|
373
381
|
} else if (transition && typeof transition === 'object') {
|
|
382
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- 'target' in transition guard confirms property; accessing unknown-typed target safely
|
|
374
383
|
const target = 'target' in transition ? (transition as { target: unknown }).target : undefined
|
|
375
384
|
if (typeof target === 'string') {
|
|
376
385
|
reachableStates.add(target)
|
|
@@ -425,6 +434,7 @@ export class SchemaValidator {
|
|
|
425
434
|
// Check each action name
|
|
426
435
|
for (const actionName of actionNames) {
|
|
427
436
|
// Check if action is registered globally
|
|
437
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- introspecting FieldTriggerEngine private fields for validation; no public API for this check
|
|
428
438
|
const engine = triggerEngine as unknown as {
|
|
429
439
|
globalActions?: Map<string, unknown>
|
|
430
440
|
globalTransitionActions?: Map<string, unknown>
|
package/src/stonecrop.ts
CHANGED
|
@@ -56,6 +56,7 @@ export class Stonecrop {
|
|
|
56
56
|
// Initialize HST store with auto-sync to Registry
|
|
57
57
|
this.initializeHSTStore()
|
|
58
58
|
this.setupRegistrySync()
|
|
59
|
+
return this
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
/**
|
|
@@ -207,7 +208,7 @@ export class Stonecrop {
|
|
|
207
208
|
const slug = typeof doctype === 'string' ? doctype : doctype.slug
|
|
208
209
|
this.ensureDoctypeExists(slug)
|
|
209
210
|
|
|
210
|
-
const doctypeNode = this.hstStore.get(slug)
|
|
211
|
+
const doctypeNode = this.hstStore.get(slug)
|
|
211
212
|
if (!doctypeNode || typeof doctypeNode !== 'object') {
|
|
212
213
|
return []
|
|
213
214
|
}
|
|
@@ -370,8 +371,10 @@ export class Stonecrop {
|
|
|
370
371
|
|
|
371
372
|
// Store each record in HST
|
|
372
373
|
records.forEach(record => {
|
|
373
|
-
if (record.id) {
|
|
374
|
-
this.addRecord(doctype, record.id
|
|
374
|
+
if (typeof record.id === 'string' && record.id) {
|
|
375
|
+
this.addRecord(doctype, record.id, record)
|
|
376
|
+
} else if (typeof record.id === 'number') {
|
|
377
|
+
this.addRecord(doctype, String(record.id), record)
|
|
375
378
|
}
|
|
376
379
|
})
|
|
377
380
|
}
|
|
@@ -477,6 +480,7 @@ export class Stonecrop {
|
|
|
477
480
|
if (!meta?.workflow) return ''
|
|
478
481
|
|
|
479
482
|
const record = this.getRecordById(slug, recordId)
|
|
483
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- HSTNode.get returns any; status is always string in workflow records
|
|
480
484
|
const status = record?.get('status') as string | undefined
|
|
481
485
|
|
|
482
486
|
// Handle both XState format and WorkflowMeta format
|
|
@@ -488,10 +492,12 @@ export class Stonecrop {
|
|
|
488
492
|
initialState = workflow.states[0] ?? ''
|
|
489
493
|
} else {
|
|
490
494
|
// XState format: states is an object, use initial or first key
|
|
495
|
+
// oxlint-disable typescript/no-unsafe-type-assertion -- XState MachineConfig has `initial` but type union doesn't expose it; runtime-guarded by typeof check
|
|
491
496
|
initialState =
|
|
492
497
|
typeof (workflow as { initial?: unknown }).initial === 'string'
|
|
493
498
|
? (workflow as { initial: string }).initial
|
|
494
499
|
: (Object.keys(workflow.states ?? {})[0] ?? '')
|
|
500
|
+
// oxlint-enable typescript/no-unsafe-type-assertion
|
|
495
501
|
}
|
|
496
502
|
|
|
497
503
|
return status || initialState
|
|
@@ -674,6 +680,7 @@ interface CodedError extends Error {
|
|
|
674
680
|
}
|
|
675
681
|
|
|
676
682
|
function createCodedError(message: string, code: string): CodedError {
|
|
683
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- extending Error with code property; CodedError cast is the standard pattern for typed error objects
|
|
677
684
|
const error = new Error(message) as CodedError
|
|
678
685
|
error.code = code
|
|
679
686
|
return error
|
package/src/stores/hst.ts
CHANGED
|
@@ -54,14 +54,8 @@ interface PiniaStore {
|
|
|
54
54
|
[key: string]: any
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// Interface for objects with property access
|
|
58
|
-
interface PropertyAccessible {
|
|
59
|
-
[key: string]: any
|
|
60
|
-
}
|
|
61
|
-
|
|
62
57
|
// Extend global interfaces
|
|
63
58
|
declare global {
|
|
64
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
65
59
|
interface Window extends RegistryGlobal {}
|
|
66
60
|
const global: RegistryGlobal | undefined
|
|
67
61
|
}
|
|
@@ -94,7 +88,8 @@ class HST {
|
|
|
94
88
|
// In test environment, try different ways to access Registry
|
|
95
89
|
// First, try the global Registry if it exists
|
|
96
90
|
if (typeof globalThis !== 'undefined') {
|
|
97
|
-
|
|
91
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Window interface extended with RegistryGlobal in declare global above; no other way to access globalThis properties
|
|
92
|
+
const globalRegistry = (globalThis as unknown as RegistryGlobal).Registry?._root
|
|
98
93
|
if (globalRegistry) {
|
|
99
94
|
return globalRegistry
|
|
100
95
|
}
|
|
@@ -129,7 +124,7 @@ class HST {
|
|
|
129
124
|
getDoctypeMeta(doctype: string) {
|
|
130
125
|
const registry = this.getRegistry()
|
|
131
126
|
if (registry && typeof registry === 'object' && 'registry' in registry) {
|
|
132
|
-
return
|
|
127
|
+
return registry.registry[doctype]
|
|
133
128
|
}
|
|
134
129
|
return undefined
|
|
135
130
|
}
|
|
@@ -199,7 +194,6 @@ class HSTProxy implements HSTNode {
|
|
|
199
194
|
// Get current value for change context
|
|
200
195
|
const fullPath = this.resolvePath(path)
|
|
201
196
|
if (fullPath === undefined) {
|
|
202
|
-
// eslint-disable-next-line no-console
|
|
203
197
|
console.warn('HST.set: resolved path is undefined, skipping operation')
|
|
204
198
|
return
|
|
205
199
|
}
|
|
@@ -445,7 +439,7 @@ class HSTProxy implements HSTNode {
|
|
|
445
439
|
}
|
|
446
440
|
|
|
447
441
|
// Plain object
|
|
448
|
-
return
|
|
442
|
+
return obj[key]
|
|
449
443
|
}
|
|
450
444
|
|
|
451
445
|
private setProperty(obj: any, key: string, value: any): void {
|
|
@@ -459,13 +453,13 @@ class HSTProxy implements HSTNode {
|
|
|
459
453
|
if (obj.$patch) {
|
|
460
454
|
obj.$patch({ [key]: value })
|
|
461
455
|
} else {
|
|
462
|
-
|
|
456
|
+
obj[key] = value
|
|
463
457
|
}
|
|
464
458
|
return
|
|
465
459
|
}
|
|
466
460
|
|
|
467
461
|
// Vue reactive or plain object
|
|
468
|
-
|
|
462
|
+
obj[key] = value
|
|
469
463
|
}
|
|
470
464
|
|
|
471
465
|
private async triggerFieldActions(fullPath: string, beforeValue: any, afterValue: any): Promise<void> {
|
|
@@ -524,19 +518,13 @@ class HSTProxy implements HSTNode {
|
|
|
524
518
|
// Silently handle trigger errors to not break the main flow
|
|
525
519
|
// In production, you might want to log this error
|
|
526
520
|
if (error instanceof Error) {
|
|
527
|
-
// eslint-disable-next-line no-console
|
|
528
521
|
console.warn('Field trigger error:', error.message)
|
|
529
522
|
// Optional: emit an event or call error handler
|
|
530
523
|
}
|
|
531
524
|
}
|
|
532
525
|
}
|
|
533
526
|
private isVueReactive(obj: any): obj is VueReactive {
|
|
534
|
-
return
|
|
535
|
-
obj &&
|
|
536
|
-
typeof obj === 'object' &&
|
|
537
|
-
'__v_isReactive' in obj &&
|
|
538
|
-
(obj as { __v_isReactive: boolean }).__v_isReactive === true
|
|
539
|
-
)
|
|
527
|
+
return obj && typeof obj === 'object' && '__v_isReactive' in obj && obj.__v_isReactive === true
|
|
540
528
|
}
|
|
541
529
|
|
|
542
530
|
private isPiniaStore(obj: any): obj is PiniaStore {
|
|
@@ -548,9 +536,9 @@ class HSTProxy implements HSTNode {
|
|
|
548
536
|
return false
|
|
549
537
|
}
|
|
550
538
|
|
|
551
|
-
const hasGetMethod = 'get' in obj && typeof
|
|
552
|
-
const hasSetMethod = 'set' in obj && typeof
|
|
553
|
-
const hasHasMethod = 'has' in obj && typeof
|
|
539
|
+
const hasGetMethod = 'get' in obj && typeof obj.get === 'function'
|
|
540
|
+
const hasSetMethod = 'set' in obj && typeof obj.set === 'function'
|
|
541
|
+
const hasHasMethod = 'has' in obj && typeof obj.has === 'function'
|
|
554
542
|
|
|
555
543
|
const hasImmutableMarkers =
|
|
556
544
|
'__ownerID' in obj ||
|
|
@@ -565,14 +553,9 @@ class HSTProxy implements HSTNode {
|
|
|
565
553
|
|
|
566
554
|
let constructorName: string | undefined
|
|
567
555
|
try {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
objWithConstructor.constructor &&
|
|
572
|
-
typeof objWithConstructor.constructor === 'object' &&
|
|
573
|
-
'name' in objWithConstructor.constructor
|
|
574
|
-
) {
|
|
575
|
-
const nameValue = (objWithConstructor.constructor as { name: unknown }).name
|
|
556
|
+
if ('constructor' in obj && obj.constructor && typeof obj.constructor === 'object' && 'name' in obj.constructor) {
|
|
557
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- obj.constructor.name accessed after 'name' in check; obj is any throughout this method
|
|
558
|
+
const nameValue = (obj.constructor as { name: unknown }).name
|
|
576
559
|
constructorName = typeof nameValue === 'string' ? nameValue : undefined
|
|
577
560
|
}
|
|
578
561
|
} catch {
|