@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.
- package/README.md +72 -29
- package/dist/composable.js +1 -0
- package/dist/composables/lazy-link.js +125 -0
- package/dist/composables/stonecrop.js +123 -68
- package/dist/composables/use-lazy-link-state.js +125 -0
- package/dist/composables/use-stonecrop.js +476 -0
- package/dist/doctype.js +10 -2
- package/dist/field-triggers.js +15 -3
- package/dist/index.js +4 -3
- package/dist/operation-log-DB-dGNT9.js +593 -0
- package/dist/operation-log-DB-dGNT9.js.map +1 -0
- package/dist/registry.js +261 -101
- package/dist/schema-validator.js +105 -1
- package/dist/src/composable.d.ts +11 -0
- package/dist/src/composable.d.ts.map +1 -0
- package/dist/src/composable.js +477 -0
- package/dist/src/composables/lazy-link.d.ts +25 -0
- package/dist/src/composables/lazy-link.d.ts.map +1 -0
- package/dist/src/composables/operation-log.d.ts +5 -5
- package/dist/src/composables/operation-log.d.ts.map +1 -1
- package/dist/src/composables/operation-log.js +224 -0
- package/dist/src/composables/stonecrop.d.ts +11 -1
- package/dist/src/composables/stonecrop.d.ts.map +1 -1
- package/dist/src/composables/stonecrop.js +574 -0
- package/dist/src/composables/use-lazy-link-state.d.ts +25 -0
- package/dist/src/composables/use-lazy-link-state.d.ts.map +1 -0
- package/dist/src/composables/use-stonecrop.d.ts +93 -0
- package/dist/src/composables/use-stonecrop.d.ts.map +1 -0
- package/dist/src/composables/useNestedSchema.d.ts +110 -0
- package/dist/src/composables/useNestedSchema.d.ts.map +1 -0
- package/dist/src/composables/useNestedSchema.js +155 -0
- package/dist/src/doctype.d.ts +9 -1
- package/dist/src/doctype.d.ts.map +1 -1
- package/dist/src/doctype.js +234 -0
- package/dist/src/exceptions.js +16 -0
- package/dist/src/field-triggers.d.ts +6 -0
- package/dist/src/field-triggers.d.ts.map +1 -1
- package/dist/src/field-triggers.js +567 -0
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +23 -0
- package/dist/src/plugins/index.js +96 -0
- package/dist/src/registry.d.ts +102 -23
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +246 -0
- package/dist/src/schema-validator.d.ts +8 -1
- package/dist/src/schema-validator.d.ts.map +1 -1
- package/dist/src/schema-validator.js +315 -0
- package/dist/src/stonecrop.d.ts +73 -28
- package/dist/src/stonecrop.d.ts.map +1 -1
- package/dist/src/stonecrop.js +339 -0
- package/dist/src/stores/data.d.ts +11 -0
- package/dist/src/stores/data.d.ts.map +1 -0
- package/dist/src/stores/hst.d.ts +5 -75
- package/dist/src/stores/hst.d.ts.map +1 -1
- package/dist/src/stores/hst.js +495 -0
- package/dist/src/stores/index.js +12 -0
- package/dist/src/stores/operation-log.d.ts +14 -14
- package/dist/src/stores/operation-log.d.ts.map +1 -1
- package/dist/src/stores/operation-log.js +568 -0
- package/dist/src/stores/xstate.d.ts +31 -0
- package/dist/src/stores/xstate.d.ts.map +1 -0
- package/dist/src/tsdoc-metadata.json +11 -0
- package/dist/src/types/composable.d.ts +50 -12
- package/dist/src/types/composable.d.ts.map +1 -1
- package/dist/src/types/doctype.d.ts +6 -7
- package/dist/src/types/doctype.d.ts.map +1 -1
- package/dist/src/types/field-triggers.d.ts +1 -1
- package/dist/src/types/field-triggers.d.ts.map +1 -1
- package/dist/src/types/field-triggers.js +4 -0
- package/dist/src/types/hst.d.ts +70 -0
- package/dist/src/types/hst.d.ts.map +1 -0
- package/dist/src/types/index.d.ts +1 -0
- 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 +4 -4
- package/dist/src/types/operation-log.d.ts.map +1 -1
- package/dist/src/types/operation-log.js +0 -0
- package/dist/src/types/registry.js +0 -0
- package/dist/src/types/schema-validator.d.ts +2 -0
- package/dist/src/types/schema-validator.d.ts.map +1 -1
- package/dist/src/utils.d.ts +24 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/stonecrop.d.ts +317 -99
- package/dist/stonecrop.js +2191 -1897
- package/dist/stonecrop.js.map +1 -1
- package/dist/stonecrop.umd.cjs +6 -0
- package/dist/stonecrop.umd.cjs.map +1 -0
- package/dist/stores/data.js +7 -0
- package/dist/stores/hst.js +27 -25
- package/dist/stores/operation-log.js +59 -47
- package/dist/stores/xstate.js +29 -0
- 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/dist/types/hst.js +0 -0
- package/dist/types/index.js +1 -0
- package/dist/utils.js +46 -0
- package/package.json +5 -5
- package/src/composables/lazy-link.ts +146 -0
- package/src/composables/operation-log.ts +1 -1
- package/src/composables/stonecrop.ts +142 -73
- package/src/doctype.ts +13 -4
- package/src/field-triggers.ts +18 -4
- package/src/index.ts +4 -2
- package/src/registry.ts +289 -111
- package/src/schema-validator.ts +120 -1
- package/src/stonecrop.ts +230 -106
- package/src/stores/hst.ts +29 -104
- package/src/stores/operation-log.ts +64 -50
- package/src/types/composable.ts +55 -12
- package/src/types/doctype.ts +6 -7
- package/src/types/field-triggers.ts +1 -1
- package/src/types/hst.ts +77 -0
- package/src/types/index.ts +1 -0
- package/src/types/operation-log.ts +4 -4
- package/src/types/schema-validator.ts +2 -0
package/src/stonecrop.ts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type DoctypeManySchema,
|
|
3
|
-
type DoctypeOneSchema,
|
|
4
|
-
type DoctypeSchema,
|
|
5
|
-
type SchemaTypes,
|
|
6
|
-
isDoctypeMany,
|
|
7
|
-
} from '@stonecrop/aform'
|
|
8
1
|
import type { DataClient } from '@stonecrop/schema'
|
|
9
2
|
import { reactive } from 'vue'
|
|
10
3
|
|
|
11
4
|
import Doctype from './doctype'
|
|
5
|
+
import { getGlobalTriggerEngine } from './field-triggers'
|
|
12
6
|
import Registry from './registry'
|
|
13
7
|
import { createHST, type HSTNode } from './stores/hst'
|
|
14
8
|
import { useOperationLogStore } from './stores/operation-log'
|
|
9
|
+
import type { FieldChangeContext } from './types/field-triggers'
|
|
15
10
|
import type { OperationLogConfig } from './types/operation-log'
|
|
16
11
|
import type { RouteContext } from './types/registry'
|
|
17
12
|
import type { StonecropOptions } from './types/stonecrop'
|
|
@@ -29,13 +24,14 @@ export class Stonecrop {
|
|
|
29
24
|
*/
|
|
30
25
|
static _root: Stonecrop
|
|
31
26
|
|
|
32
|
-
|
|
27
|
+
/** The HST store instance for reactive state management */
|
|
28
|
+
private hstStore!: HSTNode
|
|
33
29
|
private _operationLogStore?: ReturnType<typeof useOperationLogStore>
|
|
34
30
|
private _operationLogConfig?: Partial<OperationLogConfig>
|
|
35
31
|
private _client?: DataClient
|
|
36
32
|
|
|
37
33
|
/** The registry instance containing all doctype definitions */
|
|
38
|
-
readonly registry
|
|
34
|
+
readonly registry!: Registry
|
|
39
35
|
|
|
40
36
|
/**
|
|
41
37
|
* Creates a new Stonecrop instance with HST integration (singleton pattern)
|
|
@@ -184,7 +180,7 @@ export class Stonecrop {
|
|
|
184
180
|
return undefined
|
|
185
181
|
}
|
|
186
182
|
|
|
187
|
-
// Use getNode to get the properly wrapped HST node with correct
|
|
183
|
+
// Use getNode to get the properly wrapped HST node with correct ancestor relationships
|
|
188
184
|
return this.hstStore.getNode(`${slug}.${recordId}`)
|
|
189
185
|
}
|
|
190
186
|
|
|
@@ -251,10 +247,25 @@ export class Stonecrop {
|
|
|
251
247
|
* @param action - The action to run
|
|
252
248
|
* @param args - Action arguments (typically record IDs)
|
|
253
249
|
*/
|
|
254
|
-
runAction(doctype: Doctype, action: string, args?:
|
|
250
|
+
runAction(doctype: Doctype, action: string, args?: string[]): void {
|
|
255
251
|
const registry = this.registry.registry[doctype.slug]
|
|
256
252
|
const actions = registry?.actions?.get(action)
|
|
257
253
|
const recordIds = Array.isArray(args) ? args.filter((arg): arg is string => typeof arg === 'string') : undefined
|
|
254
|
+
const recordId = recordIds?.[0]
|
|
255
|
+
|
|
256
|
+
// Check if workflow is ready (all blocked links have data)
|
|
257
|
+
const workflowStatus = recordId ? this.isWorkflowReady(doctype, recordId) : { ready: true }
|
|
258
|
+
if (!workflowStatus.ready) {
|
|
259
|
+
const opLogStore = this.getOperationLogStore()
|
|
260
|
+
opLogStore.logAction(
|
|
261
|
+
doctype.doctype,
|
|
262
|
+
action,
|
|
263
|
+
recordIds,
|
|
264
|
+
'failure',
|
|
265
|
+
`BLOCKED: missing data for links: ${workflowStatus.blockedLinks?.join(', ')}`
|
|
266
|
+
)
|
|
267
|
+
throw new Error(`Workflow blocked: missing data for links: ${workflowStatus.blockedLinks?.join(', ')}`)
|
|
268
|
+
}
|
|
258
269
|
|
|
259
270
|
// Log action execution start
|
|
260
271
|
const opLogStore = this.getOperationLogStore()
|
|
@@ -264,12 +275,22 @@ export class Stonecrop {
|
|
|
264
275
|
try {
|
|
265
276
|
// Execute action functions
|
|
266
277
|
if (actions && actions.length > 0) {
|
|
278
|
+
const engine = getGlobalTriggerEngine()
|
|
267
279
|
actions.forEach(actionStr => {
|
|
268
280
|
try {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
281
|
+
const actionFn = engine.getAction(actionStr)
|
|
282
|
+
if (!actionFn) throw new Error(`Action "${actionStr}" is not registered in FieldTriggerEngine`)
|
|
283
|
+
const context = {
|
|
284
|
+
path: `${doctype.slug}.${recordIds?.[0] ?? ''}`,
|
|
285
|
+
fieldname: action,
|
|
286
|
+
beforeValue: undefined,
|
|
287
|
+
afterValue: args,
|
|
288
|
+
operation: 'set',
|
|
289
|
+
doctype: doctype.doctype,
|
|
290
|
+
recordId: recordId,
|
|
291
|
+
timestamp: new Date(),
|
|
292
|
+
} as FieldChangeContext
|
|
293
|
+
void actionFn(context)
|
|
273
294
|
} catch (error) {
|
|
274
295
|
actionResult = 'failure'
|
|
275
296
|
actionError = error instanceof Error ? error.message : 'Unknown error'
|
|
@@ -285,6 +306,54 @@ export class Stonecrop {
|
|
|
285
306
|
}
|
|
286
307
|
}
|
|
287
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Get the effective blockWorkflows value for a link.
|
|
311
|
+
* Returns true if blockWorkflows is explicitly true, or if it's absent and fetch method is 'sync'.
|
|
312
|
+
* @param link - The link declaration
|
|
313
|
+
* @returns Whether workflows should be blocked until this link is loaded
|
|
314
|
+
*/
|
|
315
|
+
private getEffectiveBlockWorkflows(link: { blockWorkflows?: boolean; fetch?: { method?: string } }): boolean {
|
|
316
|
+
if (link.blockWorkflows !== undefined) {
|
|
317
|
+
return link.blockWorkflows
|
|
318
|
+
}
|
|
319
|
+
// TODO: For custom fetch handlers, this returns false (not blocking), but the custom handler
|
|
320
|
+
// may still be invoked by useLazyLink. Future: custom handlers should be able to declare they
|
|
321
|
+
// satisfy blockWorkflows, or validation should reject custom + blockWorkflows: true.
|
|
322
|
+
// See: relationships.md Phase 6 "Open Question: blockWorkflows + custom fetch"
|
|
323
|
+
return link.fetch?.method === 'sync'
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Check if workflow actions are ready to run (all required link data is loaded).
|
|
328
|
+
* A link's data is considered loaded if it exists in HST at `slug.recordId.linkname`.
|
|
329
|
+
* @param doctype - The doctype to check
|
|
330
|
+
* @param recordId - The record ID
|
|
331
|
+
* @returns Object with `ready: true` if all blocked links are loaded, or `ready: false` with `blockedLinks` array
|
|
332
|
+
*/
|
|
333
|
+
isWorkflowReady(doctype: Doctype, recordId: string): { ready: boolean; blockedLinks?: string[] } {
|
|
334
|
+
// New records don't block workflows - they haven't been saved yet
|
|
335
|
+
if (recordId === 'new') {
|
|
336
|
+
return { ready: true }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const links = this.registry.getDescendantLinks(doctype.slug)
|
|
340
|
+
const blockedLinks: string[] = []
|
|
341
|
+
|
|
342
|
+
for (const link of links) {
|
|
343
|
+
if (this.getEffectiveBlockWorkflows(link)) {
|
|
344
|
+
const linkPath = `${doctype.slug}.${recordId}.${link.fieldname}`
|
|
345
|
+
if (!this.hstStore.has(linkPath)) {
|
|
346
|
+
blockedLinks.push(link.fieldname)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (blockedLinks.length > 0) {
|
|
352
|
+
return { ready: false, blockedLinks }
|
|
353
|
+
}
|
|
354
|
+
return { ready: true }
|
|
355
|
+
}
|
|
356
|
+
|
|
288
357
|
/**
|
|
289
358
|
* Get records from server using the configured data client.
|
|
290
359
|
* @param doctype - The doctype
|
|
@@ -436,39 +505,25 @@ export class Stonecrop {
|
|
|
436
505
|
|
|
437
506
|
const payload: Record<string, any> = { ...recordData }
|
|
438
507
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const resolved = this.registry.resolveSchema(schemaArray)
|
|
445
|
-
|
|
446
|
-
const doctypeFields = resolved.filter(
|
|
447
|
-
field =>
|
|
448
|
-
'fieldtype' in field &&
|
|
449
|
-
field.fieldtype === 'Doctype' &&
|
|
450
|
-
!isDoctypeMany(field as DoctypeSchema) &&
|
|
451
|
-
'schema' in field &&
|
|
452
|
-
Array.isArray(field.schema)
|
|
453
|
-
)
|
|
454
|
-
|
|
455
|
-
for (const field of doctypeFields) {
|
|
456
|
-
const doctypeField = field as DoctypeOneSchema
|
|
457
|
-
const fieldPath = `${recordPath}.${doctypeField.fieldname}`
|
|
458
|
-
const nestedData = collectNestedData(doctypeField.schema!, fieldPath, this.hstStore)
|
|
459
|
-
payload[doctypeField.fieldname] = nestedData
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const doctypeManyFields = resolved.filter(
|
|
463
|
-
field => 'fieldtype' in field && field.fieldtype === 'Doctype' && isDoctypeMany(field as DoctypeSchema)
|
|
464
|
-
)
|
|
508
|
+
// Collect nested data from links
|
|
509
|
+
if (doctype.links) {
|
|
510
|
+
for (const [fieldname, link] of Object.entries(doctype.links)) {
|
|
511
|
+
const fieldPath = `${recordPath}.${fieldname}`
|
|
512
|
+
const isMany = link.cardinality === 'noneOrMany' || link.cardinality === 'atLeastOne'
|
|
465
513
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
514
|
+
if (isMany) {
|
|
515
|
+
const arrayData = this.hstStore.get(fieldPath)
|
|
516
|
+
if (Array.isArray(arrayData)) {
|
|
517
|
+
payload[fieldname] = arrayData
|
|
518
|
+
}
|
|
519
|
+
} else {
|
|
520
|
+
const targetDoctype = this.registry.getDoctype(link.target)
|
|
521
|
+
if (targetDoctype?.links) {
|
|
522
|
+
payload[fieldname] = this.collectNestedData(fieldPath, targetDoctype)
|
|
523
|
+
} else {
|
|
524
|
+
payload[fieldname] = this.hstStore.get(fieldPath) || {}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
472
527
|
}
|
|
473
528
|
}
|
|
474
529
|
|
|
@@ -476,76 +531,145 @@ export class Stonecrop {
|
|
|
476
531
|
}
|
|
477
532
|
|
|
478
533
|
/**
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
534
|
+
* Scaffold empty descendant records from defaults for all descendant links.
|
|
535
|
+
*
|
|
536
|
+
* Initializes all scalar and link fields at their HST paths with default values.
|
|
537
|
+
* For new records, call this after setting up the doctype to ensure all paths exist.
|
|
538
|
+
*
|
|
539
|
+
* @param path - HST path (e.g., "customer.new")
|
|
540
|
+
* @param doctype - The doctype to initialize
|
|
484
541
|
* @public
|
|
485
542
|
*/
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
543
|
+
initializeNestedData(path: string, doctype: Doctype): void {
|
|
544
|
+
const slug = doctype.slug
|
|
545
|
+
this.ensureDoctypeExists(slug)
|
|
546
|
+
|
|
547
|
+
// Resolve schema and initialize with defaults
|
|
548
|
+
const resolvedSchema = this.registry.resolveSchema(doctype)
|
|
549
|
+
const record = this.registry.initializeRecord(resolvedSchema)
|
|
550
|
+
|
|
551
|
+
// Ensure the ancestor path exists in HST before setting descendant fields
|
|
552
|
+
const existingData = this.hstStore.get(path)
|
|
553
|
+
if (!existingData) {
|
|
554
|
+
this.hstStore.set(path, {}, 'system')
|
|
491
555
|
}
|
|
492
556
|
|
|
493
|
-
//
|
|
494
|
-
|
|
557
|
+
// Store each field at its own HST path
|
|
558
|
+
for (const [key, value] of Object.entries(record)) {
|
|
559
|
+
this.hstStore.set(`${path}.${key}`, value, 'system')
|
|
560
|
+
}
|
|
561
|
+
}
|
|
495
562
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
563
|
+
/**
|
|
564
|
+
* Fetch a record and its nested data from the server.
|
|
565
|
+
*
|
|
566
|
+
* Calls `_client.getRecord()` with nested sub-selections and stores each scalar field at its own HST path
|
|
567
|
+
* (`slug.recordId.fieldname`), descendants at the link-level path (`slug.recordId.linkname`).
|
|
568
|
+
*
|
|
569
|
+
* @param path - HST path (e.g., "recipe.r1")
|
|
570
|
+
* @param doctype - The doctype to fetch
|
|
571
|
+
* @param recordId - Record ID to fetch
|
|
572
|
+
* @param options - Query options (includeNested to control which links are fetched)
|
|
573
|
+
* @throws Error with code `"CLIENT_REQUIRED"` if no data client is configured
|
|
574
|
+
* @throws Error with code `"RECORD_NOT_FOUND"` if the server returns null
|
|
575
|
+
* @public
|
|
576
|
+
*/
|
|
577
|
+
async fetchNestedData(
|
|
578
|
+
path: string,
|
|
579
|
+
doctype: Doctype,
|
|
580
|
+
recordId: string,
|
|
581
|
+
options?: { includeNested?: boolean | string[] }
|
|
582
|
+
): Promise<void> {
|
|
583
|
+
if (!this._client) {
|
|
584
|
+
throw createCodedError(
|
|
585
|
+
'No data client configured. Call setClient() with a DataClient implementation ' +
|
|
586
|
+
'(e.g., StonecropClient from @stonecrop/graphql-client) before fetching records.',
|
|
587
|
+
'CLIENT_REQUIRED'
|
|
588
|
+
)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const record = await this._client.getRecord({ name: doctype.doctype }, recordId, {
|
|
592
|
+
includeNested: options?.includeNested ?? true,
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
if (!record) {
|
|
596
|
+
throw createCodedError(`Record not found: ${doctype.doctype} ${recordId}`, 'RECORD_NOT_FOUND')
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Store each scalar field at its own HST path, descendants at link-level path
|
|
600
|
+
const slug = doctype.slug
|
|
601
|
+
this.ensureDoctypeExists(slug)
|
|
602
|
+
|
|
603
|
+
// Ensure the ancestor path exists in HST before setting descendant fields
|
|
604
|
+
const existingData = this.hstStore.get(`${slug}.${recordId}`)
|
|
605
|
+
if (!existingData) {
|
|
606
|
+
this.hstStore.set(`${slug}.${recordId}`, {}, 'system')
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
for (const [key, value] of Object.entries(record)) {
|
|
610
|
+
this.hstStore.set(`${slug}.${recordId}.${key}`, value, 'system')
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Recursively collect nested data from HST
|
|
616
|
+
* @param basePath - The base path in HST (e.g., "customer.123.address")
|
|
617
|
+
* @param doctype - The doctype whose links drive the recursive traversal
|
|
618
|
+
* @returns The collected data object
|
|
619
|
+
*/
|
|
620
|
+
private collectNestedData(basePath: string, doctype: Doctype): Record<string, any> {
|
|
621
|
+
const data = this.hstStore.get(basePath) || {}
|
|
622
|
+
const payload: Record<string, any> = { ...data }
|
|
623
|
+
|
|
624
|
+
if (!doctype.links) return payload
|
|
625
|
+
|
|
626
|
+
for (const [fieldname, link] of Object.entries(doctype.links)) {
|
|
627
|
+
const fieldPath = `${basePath}.${fieldname}`
|
|
628
|
+
const isMany = link.cardinality === 'noneOrMany' || link.cardinality === 'atLeastOne'
|
|
629
|
+
|
|
630
|
+
if (isMany) {
|
|
631
|
+
const arrayData = this.hstStore.get(fieldPath)
|
|
632
|
+
if (Array.isArray(arrayData)) {
|
|
633
|
+
payload[fieldname] = arrayData
|
|
634
|
+
}
|
|
635
|
+
} else {
|
|
636
|
+
const targetDoctype = this.registry.getDoctype(link.target)
|
|
637
|
+
if (targetDoctype?.links) {
|
|
638
|
+
payload[fieldname] = this.collectNestedData(fieldPath, targetDoctype)
|
|
639
|
+
} else {
|
|
640
|
+
payload[fieldname] = this.hstStore.get(fieldPath) || {}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return payload
|
|
504
646
|
}
|
|
505
647
|
}
|
|
506
648
|
|
|
507
649
|
/**
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
650
|
+
* Returns the global Stonecrop singleton instance, or `undefined` if no
|
|
651
|
+
* instance has been created yet.
|
|
652
|
+
*
|
|
653
|
+
* Use this when you need the Stonecrop instance outside a Vue component
|
|
654
|
+
* context (e.g., in workflow action handlers, plugin setup code, or
|
|
655
|
+
* non-component utilities). Inside a component, prefer `useStonecrop()`.
|
|
656
|
+
*
|
|
513
657
|
* @public
|
|
514
658
|
*/
|
|
515
|
-
function
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const doctypeFields = resolvedSchema.filter(
|
|
520
|
-
field =>
|
|
521
|
-
'fieldtype' in field &&
|
|
522
|
-
field.fieldtype === 'Doctype' &&
|
|
523
|
-
!isDoctypeMany(field as DoctypeSchema) &&
|
|
524
|
-
'schema' in field &&
|
|
525
|
-
Array.isArray(field.schema)
|
|
526
|
-
)
|
|
527
|
-
|
|
528
|
-
for (const field of doctypeFields) {
|
|
529
|
-
const doctypeField = field as DoctypeOneSchema
|
|
530
|
-
const fieldPath = `${basePath}.${doctypeField.fieldname}`
|
|
531
|
-
const nestedData = collectNestedData(doctypeField.schema!, fieldPath, hstStore)
|
|
532
|
-
payload[doctypeField.fieldname] = nestedData
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const doctypeManyFields = resolvedSchema.filter(
|
|
536
|
-
field => 'fieldtype' in field && field.fieldtype === 'Doctype' && isDoctypeMany(field as DoctypeSchema)
|
|
537
|
-
)
|
|
538
|
-
|
|
539
|
-
for (const field of doctypeManyFields) {
|
|
540
|
-
const doctypeField = field as DoctypeManySchema
|
|
541
|
-
const fieldPath = `${basePath}.${doctypeField.fieldname}`
|
|
542
|
-
const arrayData = hstStore.get(fieldPath)
|
|
543
|
-
if (Array.isArray(arrayData)) {
|
|
544
|
-
payload[doctypeField.fieldname] = arrayData
|
|
545
|
-
}
|
|
546
|
-
}
|
|
659
|
+
export function getStonecrop(): Stonecrop | undefined {
|
|
660
|
+
return Stonecrop._root
|
|
661
|
+
}
|
|
547
662
|
|
|
548
|
-
|
|
663
|
+
/**
|
|
664
|
+
* Create an Error with a `code` property for programmatic error handling.
|
|
665
|
+
* @internal
|
|
666
|
+
*/
|
|
667
|
+
interface CodedError extends Error {
|
|
668
|
+
code: string
|
|
549
669
|
}
|
|
550
670
|
|
|
551
|
-
|
|
671
|
+
function createCodedError(message: string, code: string): CodedError {
|
|
672
|
+
const error = new Error(message) as CodedError
|
|
673
|
+
error.code = code
|
|
674
|
+
return error
|
|
675
|
+
}
|
package/src/stores/hst.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getGlobalTriggerEngine } from '../field-triggers'
|
|
2
|
-
import type { FieldChangeContext, TransitionChangeContext } from '../types/field-triggers'
|
|
3
2
|
import { useOperationLogStore } from './operation-log'
|
|
3
|
+
import type { FieldChangeContext, TransitionChangeContext } from '../types/field-triggers'
|
|
4
|
+
import type { HSTNode } from '../types/hst'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Get the operation log store if available
|
|
@@ -14,84 +15,6 @@ function getOperationLogStore() {
|
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
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
18
|
// Type definitions for global Registry
|
|
96
19
|
interface RegistryGlobal {
|
|
97
20
|
Registry?: {
|
|
@@ -215,18 +138,16 @@ class HST {
|
|
|
215
138
|
// Enhanced HST Proxy with tree navigation
|
|
216
139
|
class HSTProxy implements HSTNode {
|
|
217
140
|
private target: any
|
|
218
|
-
private
|
|
141
|
+
private ancestorPath: string
|
|
219
142
|
private rootNode: HSTNode | null
|
|
220
143
|
private doctype: string
|
|
221
|
-
private parentDoctype?: string
|
|
222
144
|
private hst: HST
|
|
223
145
|
|
|
224
|
-
constructor(target: any, doctype: string,
|
|
146
|
+
constructor(target: any, doctype: string, ancestorPath = '', rootNode: HSTNode | null = null) {
|
|
225
147
|
this.target = target
|
|
226
|
-
this.
|
|
148
|
+
this.ancestorPath = ancestorPath
|
|
227
149
|
this.rootNode = rootNode || this
|
|
228
150
|
this.doctype = doctype
|
|
229
|
-
this.parentDoctype = parentDoctype
|
|
230
151
|
this.hst = HST.getInstance()
|
|
231
152
|
|
|
232
153
|
return new Proxy(this, {
|
|
@@ -267,16 +188,21 @@ class HSTProxy implements HSTNode {
|
|
|
267
188
|
|
|
268
189
|
// Always wrap in HSTProxy for tree navigation
|
|
269
190
|
if (typeof value === 'object' && value !== null && !this.isPrimitive(value)) {
|
|
270
|
-
return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode
|
|
191
|
+
return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode)
|
|
271
192
|
}
|
|
272
193
|
|
|
273
194
|
// For primitives, return a minimal wrapper that throws on tree operations
|
|
274
|
-
return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode
|
|
195
|
+
return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode)
|
|
275
196
|
}
|
|
276
197
|
|
|
277
198
|
set(path: string, value: any, source: 'user' | 'system' | 'sync' | 'undo' | 'redo' = 'user'): void {
|
|
278
199
|
// Get current value for change context
|
|
279
200
|
const fullPath = this.resolvePath(path)
|
|
201
|
+
if (fullPath === undefined) {
|
|
202
|
+
// eslint-disable-next-line no-console
|
|
203
|
+
console.warn('HST.set: resolved path is undefined, skipping operation')
|
|
204
|
+
return
|
|
205
|
+
}
|
|
280
206
|
const beforeValue = this.has(path) ? this.get(path) : undefined
|
|
281
207
|
|
|
282
208
|
// Log operation if not from undo/redo and store is available
|
|
@@ -355,18 +281,18 @@ class HSTProxy implements HSTNode {
|
|
|
355
281
|
}
|
|
356
282
|
|
|
357
283
|
// Tree navigation methods
|
|
358
|
-
|
|
359
|
-
if (!this.
|
|
284
|
+
getAncestor(): HSTNode | null {
|
|
285
|
+
if (!this.ancestorPath) return null
|
|
360
286
|
|
|
361
|
-
const
|
|
362
|
-
const
|
|
287
|
+
const ancestorSegments = this.ancestorPath.split('.').slice(0, -1)
|
|
288
|
+
const ancestorPath = ancestorSegments.join('.')
|
|
363
289
|
|
|
364
|
-
if (
|
|
290
|
+
if (ancestorPath === '') {
|
|
365
291
|
return this.rootNode
|
|
366
292
|
}
|
|
367
293
|
|
|
368
294
|
// Return a wrapped node, not raw data
|
|
369
|
-
return this.rootNode!.getNode(
|
|
295
|
+
return this.rootNode!.getNode(ancestorPath)
|
|
370
296
|
}
|
|
371
297
|
|
|
372
298
|
getRoot(): HSTNode {
|
|
@@ -374,15 +300,15 @@ class HSTProxy implements HSTNode {
|
|
|
374
300
|
}
|
|
375
301
|
|
|
376
302
|
getPath(): string {
|
|
377
|
-
return this.
|
|
303
|
+
return this.ancestorPath
|
|
378
304
|
}
|
|
379
305
|
|
|
380
306
|
getDepth(): number {
|
|
381
|
-
return this.
|
|
307
|
+
return this.ancestorPath ? this.ancestorPath.split('.').length : 0
|
|
382
308
|
}
|
|
383
309
|
|
|
384
310
|
getBreadcrumbs(): string[] {
|
|
385
|
-
return this.
|
|
311
|
+
return this.ancestorPath ? this.ancestorPath.split('.') : []
|
|
386
312
|
}
|
|
387
313
|
|
|
388
314
|
/**
|
|
@@ -395,7 +321,7 @@ class HSTProxy implements HSTNode {
|
|
|
395
321
|
const triggerEngine = getGlobalTriggerEngine()
|
|
396
322
|
|
|
397
323
|
// Determine doctype and recordId from the current path
|
|
398
|
-
const pathSegments = this.
|
|
324
|
+
const pathSegments = this.ancestorPath.split('.')
|
|
399
325
|
let doctype = this.doctype
|
|
400
326
|
let recordId: string | undefined
|
|
401
327
|
|
|
@@ -411,7 +337,7 @@ class HSTProxy implements HSTNode {
|
|
|
411
337
|
|
|
412
338
|
// Build transition context
|
|
413
339
|
const transitionContext: TransitionChangeContext = {
|
|
414
|
-
path: this.
|
|
340
|
+
path: this.ancestorPath,
|
|
415
341
|
fieldname: '', // No specific field for transitions
|
|
416
342
|
beforeValue: undefined,
|
|
417
343
|
afterValue: undefined,
|
|
@@ -432,7 +358,7 @@ class HSTProxy implements HSTNode {
|
|
|
432
358
|
logStore.addOperation(
|
|
433
359
|
{
|
|
434
360
|
type: 'transition' as const,
|
|
435
|
-
path: this.
|
|
361
|
+
path: this.ancestorPath,
|
|
436
362
|
fieldname: transition,
|
|
437
363
|
beforeValue: context?.currentState,
|
|
438
364
|
afterValue: context?.targetState,
|
|
@@ -456,8 +382,8 @@ class HSTProxy implements HSTNode {
|
|
|
456
382
|
|
|
457
383
|
// Private helper methods
|
|
458
384
|
private resolvePath(path: string): string {
|
|
459
|
-
if (path === '') return this.
|
|
460
|
-
return this.
|
|
385
|
+
if (path === '') return this.ancestorPath ?? ''
|
|
386
|
+
return this.ancestorPath ? `${this.ancestorPath}.${path}` : path
|
|
461
387
|
}
|
|
462
388
|
|
|
463
389
|
private resolveValue(path: string): any {
|
|
@@ -490,7 +416,7 @@ class HSTProxy implements HSTNode {
|
|
|
490
416
|
const lastSegment = segments.pop()!
|
|
491
417
|
let current = this.target
|
|
492
418
|
|
|
493
|
-
// Navigate to
|
|
419
|
+
// Navigate to ancestor object
|
|
494
420
|
for (const segment of segments) {
|
|
495
421
|
current = this.getProperty(current, segment)
|
|
496
422
|
if (current === null || current === undefined) {
|
|
@@ -705,13 +631,12 @@ class HSTProxy implements HSTNode {
|
|
|
705
631
|
*
|
|
706
632
|
* @param target - The target object to wrap with HST functionality
|
|
707
633
|
* @param doctype - The document type identifier
|
|
708
|
-
* @param parentDoctype - Optional parent document type identifier
|
|
709
634
|
* @returns A new HSTNode proxy instance
|
|
710
635
|
*
|
|
711
636
|
* @public
|
|
712
637
|
*/
|
|
713
|
-
function createHST(target: any, doctype: string
|
|
714
|
-
return new HSTProxy(target, doctype, '', null
|
|
638
|
+
function createHST(target: any, doctype: string): HSTNode {
|
|
639
|
+
return new HSTProxy(target, doctype, '', null)
|
|
715
640
|
}
|
|
716
641
|
|
|
717
642
|
// Export everything
|