@stonecrop/stonecrop 0.4.37 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -3
- package/dist/src/composable.d.ts +74 -8
- package/dist/src/composable.d.ts.map +1 -1
- package/dist/src/composable.js +348 -0
- package/dist/src/composables/operation-log.d.ts +136 -0
- package/dist/src/composables/operation-log.d.ts.map +1 -0
- package/dist/src/composables/operation-log.js +221 -0
- package/dist/src/doctype.d.ts +9 -1
- package/dist/src/doctype.d.ts.map +1 -1
- package/dist/{doctype.js → src/doctype.js} +9 -3
- package/dist/src/field-triggers.d.ts +178 -0
- package/dist/src/field-triggers.d.ts.map +1 -0
- package/dist/src/field-triggers.js +564 -0
- package/dist/src/index.d.ts +12 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +18 -0
- package/dist/src/plugins/index.d.ts +11 -13
- package/dist/src/plugins/index.d.ts.map +1 -1
- package/dist/src/plugins/index.js +90 -0
- package/dist/src/registry.d.ts +9 -3
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/{registry.js → src/registry.js} +14 -1
- package/dist/src/stonecrop.d.ts +350 -114
- package/dist/src/stonecrop.d.ts.map +1 -1
- package/dist/src/stonecrop.js +251 -0
- package/dist/src/stores/hst.d.ts +157 -0
- package/dist/src/stores/hst.d.ts.map +1 -0
- package/dist/src/stores/hst.js +483 -0
- package/dist/src/stores/index.d.ts +5 -1
- package/dist/src/stores/index.d.ts.map +1 -1
- package/dist/{stores → src/stores}/index.js +4 -1
- package/dist/src/stores/operation-log.d.ts +268 -0
- package/dist/src/stores/operation-log.d.ts.map +1 -0
- package/dist/src/stores/operation-log.js +571 -0
- package/dist/src/types/field-triggers.d.ts +186 -0
- package/dist/src/types/field-triggers.d.ts.map +1 -0
- package/dist/src/types/field-triggers.js +4 -0
- package/dist/src/types/index.d.ts +13 -2
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/types/index.js +4 -0
- package/dist/src/types/operation-log.d.ts +165 -0
- package/dist/src/types/operation-log.d.ts.map +1 -0
- package/dist/src/types/registry.d.ts +11 -0
- package/dist/src/types/registry.d.ts.map +1 -0
- package/dist/src/types/registry.js +0 -0
- package/dist/stonecrop.d.ts +1555 -159
- package/dist/stonecrop.js +1974 -7028
- package/dist/stonecrop.js.map +1 -1
- package/dist/stonecrop.umd.cjs +4 -8
- package/dist/stonecrop.umd.cjs.map +1 -1
- package/dist/tests/setup.d.ts +5 -0
- package/dist/tests/setup.d.ts.map +1 -0
- package/dist/tests/setup.js +15 -0
- package/package.json +6 -5
- package/src/composable.ts +481 -31
- package/src/composables/operation-log.ts +254 -0
- package/src/doctype.ts +9 -3
- package/src/field-triggers.ts +671 -0
- package/src/index.ts +50 -4
- package/src/plugins/index.ts +70 -22
- package/src/registry.ts +18 -3
- package/src/stonecrop.ts +246 -155
- package/src/stores/hst.ts +703 -0
- package/src/stores/index.ts +6 -1
- package/src/stores/operation-log.ts +671 -0
- package/src/types/field-triggers.ts +201 -0
- package/src/types/index.ts +17 -6
- package/src/types/operation-log.ts +205 -0
- package/src/types/registry.ts +10 -0
- package/dist/composable.js +0 -50
- package/dist/index.js +0 -6
- package/dist/plugins/index.js +0 -49
- package/dist/src/stores/data.d.ts +0 -11
- package/dist/src/stores/data.d.ts.map +0 -1
- package/dist/stores/data.js +0 -7
- package/src/stores/data.ts +0 -8
- /package/dist/{exceptions.js → src/exceptions.js} +0 -0
- /package/dist/{types/index.js → src/types/operation-log.js} +0 -0
package/src/index.ts
CHANGED
|
@@ -1,11 +1,57 @@
|
|
|
1
1
|
export type * from '@stonecrop/aform/types'
|
|
2
2
|
export type * from '@stonecrop/atable/types'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { useStonecrop } from './composable'
|
|
5
|
+
import { useOperationLog, useUndoRedoShortcuts, withBatch } from './composables/operation-log'
|
|
5
6
|
import DoctypeMeta from './doctype'
|
|
7
|
+
import {
|
|
8
|
+
getGlobalTriggerEngine,
|
|
9
|
+
markOperationIrreversible,
|
|
10
|
+
registerGlobalAction,
|
|
11
|
+
registerTransitionAction,
|
|
12
|
+
setFieldRollback,
|
|
13
|
+
triggerTransition,
|
|
14
|
+
} from './field-triggers'
|
|
15
|
+
import plugin from './plugins'
|
|
6
16
|
import Registry from './registry'
|
|
7
|
-
import Stonecrop from './
|
|
8
|
-
import {
|
|
17
|
+
import { Stonecrop } from './stonecrop'
|
|
18
|
+
import { HST, createHST, type HSTNode } from './stores/hst'
|
|
19
|
+
import { useOperationLogStore } from './stores/operation-log'
|
|
9
20
|
export type * from './types'
|
|
21
|
+
export type { BaseStonecropReturn, HSTChangeData, HSTStonecropReturn, OperationLogAPI } from './composable'
|
|
22
|
+
export type { FieldTriggerEngine } from './field-triggers'
|
|
23
|
+
export type {
|
|
24
|
+
FieldChangeContext,
|
|
25
|
+
TransitionChangeContext,
|
|
26
|
+
FieldTriggerExecutionResult,
|
|
27
|
+
ActionExecutionResult,
|
|
28
|
+
TransitionExecutionResult,
|
|
29
|
+
FieldActionFunction,
|
|
30
|
+
TransitionActionFunction,
|
|
31
|
+
} from './types/field-triggers'
|
|
10
32
|
|
|
11
|
-
export {
|
|
33
|
+
export {
|
|
34
|
+
DoctypeMeta,
|
|
35
|
+
Registry,
|
|
36
|
+
Stonecrop,
|
|
37
|
+
useStonecrop,
|
|
38
|
+
// HST exports for advanced usage
|
|
39
|
+
HST,
|
|
40
|
+
createHST,
|
|
41
|
+
HSTNode,
|
|
42
|
+
// Field trigger system exports
|
|
43
|
+
getGlobalTriggerEngine,
|
|
44
|
+
registerGlobalAction,
|
|
45
|
+
registerTransitionAction,
|
|
46
|
+
setFieldRollback,
|
|
47
|
+
triggerTransition,
|
|
48
|
+
markOperationIrreversible,
|
|
49
|
+
// Operation log exports
|
|
50
|
+
useOperationLog,
|
|
51
|
+
useOperationLogStore,
|
|
52
|
+
useUndoRedoShortcuts,
|
|
53
|
+
withBatch,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Default export is the Vue plugin
|
|
57
|
+
export default plugin
|
package/src/plugins/index.ts
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
|
-
import { App, type Plugin } from 'vue'
|
|
1
|
+
import { App, type Plugin, nextTick } from 'vue'
|
|
2
|
+
import type { Pinia } from 'pinia'
|
|
2
3
|
|
|
3
4
|
import Registry from '../registry'
|
|
4
|
-
import {
|
|
5
|
+
import { Stonecrop } from '../stonecrop'
|
|
5
6
|
import type { InstallOptions } from '../types'
|
|
7
|
+
import { useOperationLogStore } from '../stores/operation-log'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Setup auto-initialization for user-defined initialization logic
|
|
11
|
+
* This function handles the post-mount initialization automatically
|
|
12
|
+
*/
|
|
13
|
+
async function setupAutoInitialization(
|
|
14
|
+
registry: Registry,
|
|
15
|
+
stonecrop: Stonecrop,
|
|
16
|
+
onRouterInitialized: (registry: Registry, stonecrop: Stonecrop) => void | Promise<void>
|
|
17
|
+
) {
|
|
18
|
+
// Wait for the next tick to ensure the app is mounted
|
|
19
|
+
await nextTick()
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await onRouterInitialized(registry, stonecrop)
|
|
23
|
+
} catch {
|
|
24
|
+
// Silent error handling - application should handle initialization errors
|
|
25
|
+
}
|
|
26
|
+
}
|
|
6
27
|
|
|
7
28
|
/**
|
|
8
29
|
* Stonecrop Vue plugin
|
|
@@ -10,47 +31,74 @@ import type { InstallOptions } from '../types'
|
|
|
10
31
|
* @param options - The plugin options
|
|
11
32
|
* @example
|
|
12
33
|
* ```ts
|
|
13
|
-
*
|
|
14
34
|
* import { createApp } from 'vue'
|
|
15
|
-
* import Stonecrop from 'stonecrop'
|
|
16
|
-
*
|
|
17
|
-
* import App from './App.vue'
|
|
35
|
+
* import Stonecrop from '@stonecrop/stonecrop'
|
|
36
|
+
* import router from './router'
|
|
18
37
|
*
|
|
19
38
|
* const app = createApp(App)
|
|
20
39
|
* app.use(Stonecrop, {
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
40
|
+
* router,
|
|
41
|
+
* getMeta: async (routeContext) => {
|
|
42
|
+
* // routeContext contains: { path, segments }
|
|
43
|
+
* // fetch doctype meta from your API using the route context
|
|
44
|
+
* },
|
|
45
|
+
* autoInitializeRouter: true,
|
|
46
|
+
* onRouterInitialized: async (registry, stonecrop) => {
|
|
47
|
+
* // your custom initialization logic here
|
|
48
|
+
* }
|
|
28
49
|
* })
|
|
29
|
-
*
|
|
30
50
|
* app.mount('#app')
|
|
31
51
|
* ```
|
|
32
|
-
*
|
|
33
52
|
* @public
|
|
34
53
|
*/
|
|
35
54
|
const plugin: Plugin = {
|
|
36
55
|
install: (app: App, options?: InstallOptions) => {
|
|
37
|
-
//
|
|
56
|
+
// Check for existing router installation
|
|
38
57
|
const existingRouter = app.config.globalProperties.$router
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
app.use(appRouter)
|
|
58
|
+
const providedRouter = options?.router
|
|
59
|
+
const router = existingRouter || providedRouter
|
|
60
|
+
if (!existingRouter && providedRouter) {
|
|
61
|
+
app.use(providedRouter)
|
|
44
62
|
}
|
|
45
63
|
|
|
46
|
-
|
|
64
|
+
// Create registry with available router
|
|
65
|
+
const registry = new Registry(router, options?.getMeta)
|
|
47
66
|
app.provide('$registry', registry)
|
|
67
|
+
app.config.globalProperties.$registry = registry
|
|
68
|
+
|
|
69
|
+
// Create and provide a global Stonecrop instance
|
|
70
|
+
const stonecrop = new Stonecrop(registry)
|
|
71
|
+
app.provide('$stonecrop', stonecrop)
|
|
72
|
+
app.config.globalProperties.$stonecrop = stonecrop
|
|
73
|
+
|
|
74
|
+
// Initialize operation log store if Pinia is available
|
|
75
|
+
// This ensures the store is created with the app's Pinia instance
|
|
76
|
+
try {
|
|
77
|
+
const pinia = app.config.globalProperties.$pinia as Pinia | undefined
|
|
78
|
+
if (pinia) {
|
|
79
|
+
// Initialize the operation log store with the app's Pinia instance
|
|
80
|
+
const operationLogStore = useOperationLogStore(pinia)
|
|
81
|
+
|
|
82
|
+
// Provide the store so components can access it
|
|
83
|
+
app.provide('$operationLogStore', operationLogStore)
|
|
84
|
+
app.config.globalProperties.$operationLogStore = operationLogStore
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// Pinia not available - operation log won't work, but app should still function
|
|
88
|
+
console.warn('Pinia not available - operation log features will be disabled:', error)
|
|
89
|
+
}
|
|
48
90
|
|
|
91
|
+
// Register custom components
|
|
49
92
|
if (options?.components) {
|
|
50
93
|
for (const [tag, component] of Object.entries(options.components)) {
|
|
51
94
|
app.component(tag, component)
|
|
52
95
|
}
|
|
53
96
|
}
|
|
97
|
+
|
|
98
|
+
// Setup auto-initialization if requested
|
|
99
|
+
if (options?.autoInitializeRouter && options.onRouterInitialized) {
|
|
100
|
+
void setupAutoInitialization(registry, stonecrop, options.onRouterInitialized)
|
|
101
|
+
}
|
|
54
102
|
},
|
|
55
103
|
}
|
|
56
104
|
|
package/src/registry.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Router } from 'vue-router'
|
|
2
2
|
|
|
3
3
|
import DoctypeMeta from './doctype'
|
|
4
|
+
import { getGlobalTriggerEngine } from './field-triggers'
|
|
5
|
+
import { RouteContext } from './types/registry'
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Stonecrop Registry class
|
|
@@ -31,7 +33,12 @@ export default class Registry {
|
|
|
31
33
|
*/
|
|
32
34
|
readonly router?: Router
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Creates a new Registry instance (singleton pattern)
|
|
38
|
+
* @param router - Optional Vue router instance for route management
|
|
39
|
+
* @param getMeta - Optional function to fetch doctype metadata from an API
|
|
40
|
+
*/
|
|
41
|
+
constructor(router?: Router, getMeta?: (routeContext: RouteContext) => DoctypeMeta | Promise<DoctypeMeta>) {
|
|
35
42
|
if (Registry._root) {
|
|
36
43
|
return Registry._root
|
|
37
44
|
}
|
|
@@ -43,10 +50,10 @@ export default class Registry {
|
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
/**
|
|
46
|
-
* The getMeta function fetches doctype metadata from an API
|
|
53
|
+
* The getMeta function fetches doctype metadata from an API based on route context
|
|
47
54
|
* @see {@link DoctypeMeta}
|
|
48
55
|
*/
|
|
49
|
-
getMeta?: (
|
|
56
|
+
getMeta?: (routeContext: RouteContext) => DoctypeMeta | Promise<DoctypeMeta>
|
|
50
57
|
|
|
51
58
|
/**
|
|
52
59
|
* Get doctype metadata
|
|
@@ -59,6 +66,14 @@ export default class Registry {
|
|
|
59
66
|
this.registry[doctype.slug] = doctype
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
// Register actions (including field triggers) with the field trigger engine
|
|
70
|
+
const triggerEngine = getGlobalTriggerEngine()
|
|
71
|
+
// Register under both doctype name and slug to handle different lookup patterns
|
|
72
|
+
triggerEngine.registerDoctypeActions(doctype.doctype, doctype.actions)
|
|
73
|
+
if (doctype.slug !== doctype.doctype) {
|
|
74
|
+
triggerEngine.registerDoctypeActions(doctype.slug, doctype.actions)
|
|
75
|
+
}
|
|
76
|
+
|
|
62
77
|
if (doctype.component && this.router && !this.router.hasRoute(doctype.doctype)) {
|
|
63
78
|
this.router.addRoute({
|
|
64
79
|
path: `/${doctype.slug}`,
|
package/src/stonecrop.ts
CHANGED
|
@@ -1,199 +1,290 @@
|
|
|
1
|
-
import { createActor, createMachine } from 'xstate'
|
|
2
|
-
|
|
3
1
|
import DoctypeMeta from './doctype'
|
|
4
|
-
import { NotImplementedError } from './exceptions'
|
|
5
2
|
import Registry from './registry'
|
|
6
|
-
import {
|
|
3
|
+
import { createHST, type HSTNode } from './stores/hst'
|
|
4
|
+
import { useOperationLogStore } from './stores/operation-log'
|
|
5
|
+
import type { OperationLogConfig } from './types/operation-log'
|
|
6
|
+
import type { RouteContext } from './types/registry'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Stonecrop class
|
|
9
|
+
* Main Stonecrop class with HST integration and built-in Operation Log
|
|
10
10
|
* @public
|
|
11
11
|
*/
|
|
12
12
|
export class Stonecrop {
|
|
13
|
+
private hstStore: HSTNode
|
|
14
|
+
private _operationLogStore?: ReturnType<typeof useOperationLogStore>
|
|
15
|
+
private _operationLogConfig?: Partial<OperationLogConfig>
|
|
16
|
+
|
|
17
|
+
/** The registry instance containing all doctype definitions */
|
|
18
|
+
readonly registry: Registry
|
|
19
|
+
|
|
13
20
|
/**
|
|
14
|
-
*
|
|
21
|
+
* Creates a new Stonecrop instance with HST integration
|
|
22
|
+
* @param registry - The Registry instance containing doctype definitions
|
|
23
|
+
* @param operationLogConfig - Optional configuration for the operation log
|
|
15
24
|
*/
|
|
16
|
-
|
|
25
|
+
constructor(registry: Registry, operationLogConfig?: Partial<OperationLogConfig>) {
|
|
26
|
+
this.registry = registry
|
|
27
|
+
|
|
28
|
+
// Store config for lazy initialization
|
|
29
|
+
this._operationLogConfig = operationLogConfig
|
|
30
|
+
|
|
31
|
+
// Initialize HST store with auto-sync to Registry
|
|
32
|
+
this.initializeHSTStore()
|
|
33
|
+
this.setupRegistrySync()
|
|
34
|
+
}
|
|
17
35
|
|
|
18
36
|
/**
|
|
19
|
-
*
|
|
20
|
-
* @
|
|
21
|
-
*
|
|
22
|
-
* @defaultValue 'Stonecrop'
|
|
37
|
+
* Get the operation log store (lazy initialization)
|
|
38
|
+
* @internal
|
|
23
39
|
*/
|
|
24
|
-
|
|
40
|
+
getOperationLogStore() {
|
|
41
|
+
if (!this._operationLogStore) {
|
|
42
|
+
this._operationLogStore = useOperationLogStore()
|
|
43
|
+
if (this._operationLogConfig) {
|
|
44
|
+
this._operationLogStore.configure(this._operationLogConfig)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return this._operationLogStore
|
|
48
|
+
}
|
|
25
49
|
|
|
26
50
|
/**
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```ts
|
|
30
|
-
* {
|
|
31
|
-
* 'task': {
|
|
32
|
-
* doctype: 'Task',
|
|
33
|
-
* schema: {
|
|
34
|
-
* title: 'string',
|
|
35
|
-
* description: 'string',
|
|
36
|
-
* ...
|
|
37
|
-
* }
|
|
38
|
-
* },
|
|
39
|
-
* ...
|
|
40
|
-
* }
|
|
41
|
-
* ```
|
|
42
|
-
* @see {@link Registry}
|
|
43
|
-
* @see {@link DoctypeMeta}
|
|
51
|
+
* Initialize the HST store structure
|
|
44
52
|
*/
|
|
45
|
-
|
|
53
|
+
private initializeHSTStore(): void {
|
|
54
|
+
const initialStoreStructure: Record<string, any> = {}
|
|
55
|
+
|
|
56
|
+
// Auto-populate from existing Registry doctypes
|
|
57
|
+
Object.keys(this.registry.registry).forEach(doctypeSlug => {
|
|
58
|
+
initialStoreStructure[doctypeSlug] = {}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
this.hstStore = createHST(initialStoreStructure, 'StonecropStore')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Setup automatic sync with Registry when doctypes are added
|
|
66
|
+
*/
|
|
67
|
+
private setupRegistrySync(): void {
|
|
68
|
+
// Extend Registry.addDoctype to auto-create HST store sections
|
|
69
|
+
const originalAddDoctype = this.registry.addDoctype.bind(this.registry)
|
|
70
|
+
|
|
71
|
+
this.registry.addDoctype = (doctype: DoctypeMeta) => {
|
|
72
|
+
// Call original method
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
74
|
+
originalAddDoctype(doctype)
|
|
75
|
+
|
|
76
|
+
// Auto-create HST store section for new doctype
|
|
77
|
+
if (!this.hstStore.has(doctype.slug)) {
|
|
78
|
+
this.hstStore.set(doctype.slug, {})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
46
82
|
|
|
47
83
|
/**
|
|
48
|
-
*
|
|
84
|
+
* Get records hash for a doctype
|
|
85
|
+
* @param doctype - The doctype to get records for
|
|
86
|
+
* @returns HST node containing records hash
|
|
49
87
|
*/
|
|
50
|
-
|
|
88
|
+
records(doctype: string | DoctypeMeta): HSTNode {
|
|
89
|
+
const slug = typeof doctype === 'string' ? doctype : doctype.slug
|
|
90
|
+
this.ensureDoctypeExists(slug)
|
|
91
|
+
return this.hstStore.getNode(slug)
|
|
92
|
+
}
|
|
51
93
|
|
|
52
94
|
/**
|
|
53
|
-
*
|
|
54
|
-
* @param
|
|
55
|
-
* @
|
|
56
|
-
* @
|
|
57
|
-
* ```ts
|
|
58
|
-
* const registry = new Registry()
|
|
59
|
-
* const store = useDataStore()
|
|
60
|
-
* const stonecrop = new Stonecrop(registry, store)
|
|
61
|
-
* ```
|
|
95
|
+
* Add a record to the store
|
|
96
|
+
* @param doctype - The doctype
|
|
97
|
+
* @param recordId - The record ID
|
|
98
|
+
* @param recordData - The record data
|
|
62
99
|
*/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
100
|
+
addRecord(doctype: string | DoctypeMeta, recordId: string, recordData: any): void {
|
|
101
|
+
const slug = typeof doctype === 'string' ? doctype : doctype.slug
|
|
102
|
+
|
|
103
|
+
this.ensureDoctypeExists(slug)
|
|
104
|
+
|
|
105
|
+
// Store raw record data - let HST handle wrapping with proper hierarchy
|
|
106
|
+
this.hstStore.set(`${slug}.${recordId}`, recordData)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get a specific record
|
|
111
|
+
* @param doctype - The doctype
|
|
112
|
+
* @param recordId - The record ID
|
|
113
|
+
* @returns HST node for the record or undefined
|
|
114
|
+
*/
|
|
115
|
+
getRecordById(doctype: string | DoctypeMeta, recordId: string): HSTNode | undefined {
|
|
116
|
+
const slug = typeof doctype === 'string' ? doctype : doctype.slug
|
|
117
|
+
this.ensureDoctypeExists(slug)
|
|
118
|
+
|
|
119
|
+
// First check if the record exists
|
|
120
|
+
const recordExists = this.hstStore.has(`${slug}.${recordId}`)
|
|
121
|
+
if (!recordExists) {
|
|
122
|
+
return undefined
|
|
66
123
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
124
|
+
|
|
125
|
+
// Check if the actual value is undefined (i.e., record was removed)
|
|
126
|
+
const recordValue = this.hstStore.get(`${slug}.${recordId}`)
|
|
127
|
+
if (recordValue === undefined) {
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Use getNode to get the properly wrapped HST node with correct parent relationships
|
|
132
|
+
return this.hstStore.getNode(`${slug}.${recordId}`)
|
|
70
133
|
}
|
|
71
134
|
|
|
72
135
|
/**
|
|
73
|
-
*
|
|
74
|
-
* @param doctype - The doctype
|
|
75
|
-
* @
|
|
76
|
-
* ```ts
|
|
77
|
-
* const doctype = await registry.getMeta('Task')
|
|
78
|
-
* stonecrop.setup(doctype)
|
|
79
|
-
* ```
|
|
136
|
+
* Remove a record from the store
|
|
137
|
+
* @param doctype - The doctype
|
|
138
|
+
* @param recordId - The record ID
|
|
80
139
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
140
|
+
removeRecord(doctype: string | DoctypeMeta, recordId: string): void {
|
|
141
|
+
const slug = typeof doctype === 'string' ? doctype : doctype.slug
|
|
142
|
+
this.ensureDoctypeExists(slug)
|
|
143
|
+
|
|
144
|
+
// Remove the specific record directly by setting to undefined
|
|
145
|
+
if (this.hstStore.has(`${slug}.${recordId}`)) {
|
|
146
|
+
this.hstStore.set(`${slug}.${recordId}`, undefined)
|
|
147
|
+
}
|
|
83
148
|
}
|
|
84
149
|
|
|
85
150
|
/**
|
|
86
|
-
*
|
|
87
|
-
* @param doctype - The doctype
|
|
88
|
-
* @returns
|
|
89
|
-
* @throws `NotImplementedError` if the `getMeta` function is not implemented for the doctype in the registry
|
|
90
|
-
* @example
|
|
91
|
-
* ```ts
|
|
92
|
-
* const doctype = await registry.getMeta('Task')
|
|
93
|
-
* const meta = stonecrop.getMeta(doctype)
|
|
94
|
-
* ```
|
|
95
|
-
* @see {@link DoctypeMeta}
|
|
151
|
+
* Get all record IDs for a doctype
|
|
152
|
+
* @param doctype - The doctype
|
|
153
|
+
* @returns Array of record IDs
|
|
96
154
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
155
|
+
getRecordIds(doctype: string | DoctypeMeta): string[] {
|
|
156
|
+
const slug = typeof doctype === 'string' ? doctype : doctype.slug
|
|
157
|
+
this.ensureDoctypeExists(slug)
|
|
158
|
+
|
|
159
|
+
const doctypeNode = this.hstStore.get(slug) as Record<string, any>
|
|
160
|
+
if (!doctypeNode || typeof doctypeNode !== 'object') {
|
|
161
|
+
return []
|
|
100
162
|
}
|
|
101
|
-
|
|
163
|
+
|
|
164
|
+
return Object.keys(doctypeNode).filter(key => doctypeNode[key] !== undefined)
|
|
102
165
|
}
|
|
103
166
|
|
|
104
167
|
/**
|
|
105
|
-
*
|
|
106
|
-
* @param doctype - The doctype
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* @
|
|
132
|
-
* ```ts
|
|
133
|
-
* const doctype = await registry.getMeta('Task')
|
|
134
|
-
* await stonecrop.getRecord(doctype, 'TASK-00001')
|
|
135
|
-
* ```
|
|
136
|
-
*/
|
|
137
|
-
async getRecord(doctype: DoctypeMeta, id: string): Promise<void> {
|
|
138
|
-
this.store.$patch({ record: {} })
|
|
139
|
-
const record = await fetch(`/${doctype.slug}/${id}`)
|
|
140
|
-
const data: Record<string, any> = await record.json()
|
|
141
|
-
this.store.$patch({ record: data })
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Runs the action for the given doctype and id
|
|
146
|
-
* @param doctype - The doctype to run action for
|
|
168
|
+
* Clear all records for a doctype
|
|
169
|
+
* @param doctype - The doctype
|
|
170
|
+
*/
|
|
171
|
+
clearRecords(doctype: string | DoctypeMeta): void {
|
|
172
|
+
const slug = typeof doctype === 'string' ? doctype : doctype.slug
|
|
173
|
+
this.ensureDoctypeExists(slug)
|
|
174
|
+
|
|
175
|
+
// Get all record IDs and remove them
|
|
176
|
+
const recordIds = this.getRecordIds(slug)
|
|
177
|
+
recordIds.forEach(recordId => {
|
|
178
|
+
this.hstStore.set(`${slug}.${recordId}`, undefined)
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Setup method for doctype initialization
|
|
184
|
+
* @param doctype - The doctype to setup
|
|
185
|
+
*/
|
|
186
|
+
setup(doctype: DoctypeMeta): void {
|
|
187
|
+
// Ensure doctype exists in store
|
|
188
|
+
this.ensureDoctypeExists(doctype.slug)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Run action on doctype
|
|
193
|
+
* Executes the action and logs it to the operation log for audit tracking
|
|
194
|
+
* @param doctype - The doctype
|
|
147
195
|
* @param action - The action to run
|
|
148
|
-
* @param
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
* const doctype = await registry.getMeta('Task')
|
|
152
|
-
* stonecrop.runAction(doctype, 'create')
|
|
153
|
-
* ```
|
|
154
|
-
* @example
|
|
155
|
-
* ```ts
|
|
156
|
-
* const doctype = await registry.getMeta('Task')
|
|
157
|
-
* stonecrop.runAction(doctype, 'update', ['TASK-00001'])
|
|
158
|
-
* ```
|
|
159
|
-
* @example
|
|
160
|
-
* ```ts
|
|
161
|
-
* const doctype = await registry.getMeta('Task')
|
|
162
|
-
* stonecrop.runAction(doctype, 'delete', ['TASK-00001'])
|
|
163
|
-
* ```
|
|
164
|
-
* @example
|
|
165
|
-
* ```ts
|
|
166
|
-
* const doctype = await registry.getMeta('Task')
|
|
167
|
-
* stonecrop.runAction(doctype, 'merge', ['TASK-00001', 'TASK-00002'])
|
|
168
|
-
* ```
|
|
169
|
-
*/
|
|
170
|
-
runAction(doctype: DoctypeMeta, action: string, id?: string[]): void {
|
|
196
|
+
* @param args - Action arguments (typically record IDs)
|
|
197
|
+
*/
|
|
198
|
+
runAction(doctype: DoctypeMeta, action: string, args?: any[]): void {
|
|
171
199
|
const registry = this.registry.registry[doctype.slug]
|
|
172
|
-
const actions = registry
|
|
173
|
-
const
|
|
200
|
+
const actions = registry?.actions?.get(action)
|
|
201
|
+
const recordIds = Array.isArray(args) ? args.filter((arg): arg is string => typeof arg === 'string') : undefined
|
|
174
202
|
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
203
|
+
// Log action execution start
|
|
204
|
+
const opLogStore = this.getOperationLogStore()
|
|
205
|
+
let actionResult: 'success' | 'failure' | 'pending' = 'success'
|
|
206
|
+
let actionError: string | undefined
|
|
179
207
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
actor.send({ type: action, id })
|
|
183
|
-
|
|
184
|
-
// run actions after state machine transition
|
|
185
|
-
// TODO: should this happen with or without the workflow?
|
|
208
|
+
try {
|
|
209
|
+
// Execute action functions
|
|
186
210
|
if (actions && actions.length > 0) {
|
|
187
|
-
actions.forEach(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
211
|
+
actions.forEach(actionStr => {
|
|
212
|
+
try {
|
|
213
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
214
|
+
const actionFn = new Function('args', actionStr)
|
|
215
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
216
|
+
actionFn(args)
|
|
217
|
+
} catch (error) {
|
|
218
|
+
actionResult = 'failure'
|
|
219
|
+
actionError = error instanceof Error ? error.message : 'Unknown error'
|
|
220
|
+
throw error
|
|
221
|
+
}
|
|
195
222
|
})
|
|
196
223
|
}
|
|
224
|
+
} catch {
|
|
225
|
+
// Error already set in inner catch
|
|
226
|
+
} finally {
|
|
227
|
+
// Log the action execution to operation log
|
|
228
|
+
opLogStore.logAction(doctype.doctype, action, recordIds, actionResult, actionError)
|
|
197
229
|
}
|
|
198
230
|
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get records from server (maintains compatibility)
|
|
234
|
+
* @param doctype - The doctype
|
|
235
|
+
*/
|
|
236
|
+
async getRecords(doctype: DoctypeMeta): Promise<void> {
|
|
237
|
+
const response = await fetch(`/${doctype.slug}`)
|
|
238
|
+
const records = await response.json()
|
|
239
|
+
|
|
240
|
+
// Store each record in HST
|
|
241
|
+
records.forEach((record: any) => {
|
|
242
|
+
if (record.id) {
|
|
243
|
+
this.addRecord(doctype, record.id, record)
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get single record from server (maintains compatibility)
|
|
250
|
+
* @param doctype - The doctype
|
|
251
|
+
* @param recordId - The record ID
|
|
252
|
+
*/
|
|
253
|
+
async getRecord(doctype: DoctypeMeta, recordId: string): Promise<void> {
|
|
254
|
+
const response = await fetch(`/${doctype.slug}/${recordId}`)
|
|
255
|
+
const record = await response.json()
|
|
256
|
+
|
|
257
|
+
// Store record
|
|
258
|
+
this.addRecord(doctype, recordId, record)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Ensure doctype section exists in HST store
|
|
263
|
+
* @param slug - The doctype slug
|
|
264
|
+
*/
|
|
265
|
+
private ensureDoctypeExists(slug: string): void {
|
|
266
|
+
if (!this.hstStore.has(slug)) {
|
|
267
|
+
this.hstStore.set(slug, {})
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get doctype metadata from the registry
|
|
273
|
+
* @param context - The route context
|
|
274
|
+
* @returns The doctype metadata
|
|
275
|
+
*/
|
|
276
|
+
async getMeta(context: RouteContext): Promise<any> {
|
|
277
|
+
if (!this.registry.getMeta) {
|
|
278
|
+
throw new Error('No getMeta function provided to Registry')
|
|
279
|
+
}
|
|
280
|
+
return await this.registry.getMeta(context)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get the root HST store node for advanced usage
|
|
285
|
+
* @returns Root HST node
|
|
286
|
+
*/
|
|
287
|
+
getStore(): HSTNode {
|
|
288
|
+
return this.hstStore
|
|
289
|
+
}
|
|
199
290
|
}
|