@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.
Files changed (78) hide show
  1. package/README.md +92 -3
  2. package/dist/src/composable.d.ts +74 -8
  3. package/dist/src/composable.d.ts.map +1 -1
  4. package/dist/src/composable.js +348 -0
  5. package/dist/src/composables/operation-log.d.ts +136 -0
  6. package/dist/src/composables/operation-log.d.ts.map +1 -0
  7. package/dist/src/composables/operation-log.js +221 -0
  8. package/dist/src/doctype.d.ts +9 -1
  9. package/dist/src/doctype.d.ts.map +1 -1
  10. package/dist/{doctype.js → src/doctype.js} +9 -3
  11. package/dist/src/field-triggers.d.ts +178 -0
  12. package/dist/src/field-triggers.d.ts.map +1 -0
  13. package/dist/src/field-triggers.js +564 -0
  14. package/dist/src/index.d.ts +12 -4
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +18 -0
  17. package/dist/src/plugins/index.d.ts +11 -13
  18. package/dist/src/plugins/index.d.ts.map +1 -1
  19. package/dist/src/plugins/index.js +90 -0
  20. package/dist/src/registry.d.ts +9 -3
  21. package/dist/src/registry.d.ts.map +1 -1
  22. package/dist/{registry.js → src/registry.js} +14 -1
  23. package/dist/src/stonecrop.d.ts +350 -114
  24. package/dist/src/stonecrop.d.ts.map +1 -1
  25. package/dist/src/stonecrop.js +251 -0
  26. package/dist/src/stores/hst.d.ts +157 -0
  27. package/dist/src/stores/hst.d.ts.map +1 -0
  28. package/dist/src/stores/hst.js +483 -0
  29. package/dist/src/stores/index.d.ts +5 -1
  30. package/dist/src/stores/index.d.ts.map +1 -1
  31. package/dist/{stores → src/stores}/index.js +4 -1
  32. package/dist/src/stores/operation-log.d.ts +268 -0
  33. package/dist/src/stores/operation-log.d.ts.map +1 -0
  34. package/dist/src/stores/operation-log.js +571 -0
  35. package/dist/src/types/field-triggers.d.ts +186 -0
  36. package/dist/src/types/field-triggers.d.ts.map +1 -0
  37. package/dist/src/types/field-triggers.js +4 -0
  38. package/dist/src/types/index.d.ts +13 -2
  39. package/dist/src/types/index.d.ts.map +1 -1
  40. package/dist/src/types/index.js +4 -0
  41. package/dist/src/types/operation-log.d.ts +165 -0
  42. package/dist/src/types/operation-log.d.ts.map +1 -0
  43. package/dist/src/types/registry.d.ts +11 -0
  44. package/dist/src/types/registry.d.ts.map +1 -0
  45. package/dist/src/types/registry.js +0 -0
  46. package/dist/stonecrop.d.ts +1555 -159
  47. package/dist/stonecrop.js +1974 -7028
  48. package/dist/stonecrop.js.map +1 -1
  49. package/dist/stonecrop.umd.cjs +4 -8
  50. package/dist/stonecrop.umd.cjs.map +1 -1
  51. package/dist/tests/setup.d.ts +5 -0
  52. package/dist/tests/setup.d.ts.map +1 -0
  53. package/dist/tests/setup.js +15 -0
  54. package/package.json +6 -5
  55. package/src/composable.ts +481 -31
  56. package/src/composables/operation-log.ts +254 -0
  57. package/src/doctype.ts +9 -3
  58. package/src/field-triggers.ts +671 -0
  59. package/src/index.ts +50 -4
  60. package/src/plugins/index.ts +70 -22
  61. package/src/registry.ts +18 -3
  62. package/src/stonecrop.ts +246 -155
  63. package/src/stores/hst.ts +703 -0
  64. package/src/stores/index.ts +6 -1
  65. package/src/stores/operation-log.ts +671 -0
  66. package/src/types/field-triggers.ts +201 -0
  67. package/src/types/index.ts +17 -6
  68. package/src/types/operation-log.ts +205 -0
  69. package/src/types/registry.ts +10 -0
  70. package/dist/composable.js +0 -50
  71. package/dist/index.js +0 -6
  72. package/dist/plugins/index.js +0 -49
  73. package/dist/src/stores/data.d.ts +0 -11
  74. package/dist/src/stores/data.d.ts.map +0 -1
  75. package/dist/stores/data.js +0 -7
  76. package/src/stores/data.ts +0 -8
  77. /package/dist/{exceptions.js → src/exceptions.js} +0 -0
  78. /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 { type StonecropReturn, useStonecrop } from './composable'
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 './plugins'
8
- import { Stonecrop as StonecropClass } from './stonecrop'
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 { DoctypeMeta, Registry, Stonecrop, StonecropClass, StonecropReturn, useStonecrop }
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
@@ -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 { pinia } from '../stores'
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
- * router,
22
- * components: {
23
- * // register custom components
24
- * },
25
- * getMeta: async (doctype: string) => {
26
- * // fetch doctype meta from API
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
- // check if the router is already installed via another plugin
56
+ // Check for existing router installation
38
57
  const existingRouter = app.config.globalProperties.$router
39
- const appRouter = existingRouter || options?.router
40
- const registry = new Registry(appRouter, options?.getMeta)
41
-
42
- if (!existingRouter && appRouter) {
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
- app.use(pinia)
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
- constructor(router?: Router, getMeta?: (doctype: string) => DoctypeMeta | Promise<DoctypeMeta>) {
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?: (doctype: string) => DoctypeMeta | Promise<DoctypeMeta>
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 { useDataStore } from './stores/data'
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
- * The root Stonecrop instance
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
- static _root: Stonecrop
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
- * The name of the Stonecrop instance
20
- * @readonly
21
- *
22
- * @defaultValue 'Stonecrop'
37
+ * Get the operation log store (lazy initialization)
38
+ * @internal
23
39
  */
24
- readonly name = 'Stonecrop'
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
- * The registry is an immutable collection of doctypes
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
- readonly registry: Registry
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
- * The Pinia store that manages the mutable records
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
- store: ReturnType<typeof useDataStore>
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
- * @param registry - The immutable registry
54
- * @param store - The mutable Pinia store
55
- * @returns The Stonecrop instance with the given registry and store. If a Stonecrop instance has already been created, it returns the existing instance instead of creating a new one.
56
- * @example
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
- constructor(registry: Registry, store: ReturnType<typeof useDataStore>) {
64
- if (Stonecrop._root) {
65
- return Stonecrop._root
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
- Stonecrop._root = this
68
- this.registry = registry
69
- this.store = store
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
- * Sets up the Stonecrop instance with the given doctype
74
- * @param doctype - The doctype to setup
75
- * @example
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
- setup(doctype: DoctypeMeta): void {
82
- void this.getMeta(doctype.doctype)
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
- * Gets the meta for the given doctype
87
- * @param doctype - The doctype to get meta for
88
- * @returns The meta for the given doctype
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
- async getMeta(doctype: string): Promise<DoctypeMeta> | never {
98
- if (!this.registry.getMeta) {
99
- throw new NotImplementedError(`getMeta function is not implemented for ${doctype} in the registry`)
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
- return await this.registry.getMeta(doctype)
163
+
164
+ return Object.keys(doctypeNode).filter(key => doctypeNode[key] !== undefined)
102
165
  }
103
166
 
104
167
  /**
105
- * Gets the records for the given doctype
106
- * @param doctype - The doctype to get records for
107
- * @param filters - The filters to apply to the records
108
- * @example
109
- * ```ts
110
- * const doctype = await registry.getMeta('Task')
111
- * await stonecrop.getRecords(doctype)
112
- * ```
113
- * @example
114
- * ```ts
115
- * const doctype = await registry.getMeta('Task')
116
- * const filters = JSON.stringify({ status: 'Open' })
117
- * await stonecrop.getRecords(doctype, { body: filters })
118
- * ```
119
- */
120
- async getRecords(doctype: DoctypeMeta, filters?: RequestInit): Promise<void> {
121
- this.store.$patch({ records: [] })
122
- const records = await fetch(`/${doctype.slug}`, filters)
123
- const data: Record<string, any>[] = await records.json()
124
- this.store.$patch({ records: data })
125
- }
126
-
127
- /**
128
- * Gets the record for the given doctype and id
129
- * @param doctype - The doctype to get record for
130
- * @param id - The id of the record to get
131
- * @example
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 id - The id(s) of the record(s) to run action on
149
- * @example
150
- * ```ts
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.actions?.get(action)
173
- const workflow = registry.workflow
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
- // trigger the action on the state machine
176
- if (workflow) {
177
- const machine = createMachine(workflow)
178
- const actor = createActor(machine)
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
- // TODO: this shouldn't spawn an actor at the initial state always; look into persistence
181
- actor.start()
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(action => {
188
- // TODO: Replace Function constructor with a safer action execution mechanism
189
- // This is currently flagged as a security risk (implied eval)
190
- // Consider using a registry of pre-defined action functions instead
191
- // eslint-disable-next-line @typescript-eslint/no-implied-eval
192
- const actionFn = new Function(action)
193
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
194
- actionFn(id)
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
  }