@stonecrop/stonecrop 0.10.4 → 0.10.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonecrop/stonecrop",
3
- "version": "0.10.4",
3
+ "version": "0.10.6",
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.10.4"
37
+ "@stonecrop/schema": "0.10.6"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "pinia": "^3.0.4",
@@ -60,9 +60,9 @@
60
60
  "vue-router": "^5.0.2",
61
61
  "vite": "^7.3.1",
62
62
  "vitest": "^4.0.18",
63
- "@stonecrop/aform": "0.10.4",
64
- "stonecrop-rig": "0.7.0",
65
- "@stonecrop/atable": "0.10.4"
63
+ "@stonecrop/aform": "0.10.6",
64
+ "@stonecrop/atable": "0.10.6",
65
+ "stonecrop-rig": "0.7.0"
66
66
  },
67
67
  "description": "Schema-driven framework with XState workflows and HST state management",
68
68
  "publishConfig": {
@@ -2,7 +2,7 @@ import { inject, onMounted, Ref, ref, watch, provide, computed, ComputedRef } fr
2
2
 
3
3
  import Registry from '../registry'
4
4
  import { Stonecrop } from '../stonecrop'
5
- import DoctypeMeta from '../doctype'
5
+ import Doctype from '../doctype'
6
6
  import type { HSTNode } from '../stores/hst'
7
7
  import { RouteContext } from '../types/registry'
8
8
  import { storeToRefs } from 'pinia'
@@ -65,11 +65,11 @@ export type HSTStonecropReturn = BaseStonecropReturn & {
65
65
  hstStore: Ref<HSTNode | undefined>
66
66
  formData: Ref<Record<string, any>>
67
67
  resolvedSchema: Ref<SchemaTypes[]>
68
- loadNestedData: (parentPath: string, childDoctype: DoctypeMeta, recordId?: string) => Record<string, any>
69
- saveRecursive: (doctype: DoctypeMeta, recordId: string) => Promise<Record<string, any>>
68
+ loadNestedData: (parentPath: string, childDoctype: Doctype, recordId?: string) => Record<string, any>
69
+ saveRecursive: (doctype: Doctype, recordId: string) => Promise<Record<string, any>>
70
70
  createNestedContext: (
71
71
  basePath: string,
72
- childDoctype: DoctypeMeta
72
+ childDoctype: Doctype
73
73
  ) => {
74
74
  provideHSTPath: (fieldname: string) => string
75
75
  handleHSTChange: (changeData: HSTChangeData) => void
@@ -102,17 +102,13 @@ export function useStonecrop(): BaseStonecropReturn | HSTStonecropReturn
102
102
  * @returns Stonecrop instance with full HST integration utilities
103
103
  * @public
104
104
  */
105
- export function useStonecrop(options: {
106
- registry?: Registry
107
- doctype: DoctypeMeta
108
- recordId?: string
109
- }): HSTStonecropReturn
105
+ export function useStonecrop(options: { registry?: Registry; doctype: Doctype; recordId?: string }): HSTStonecropReturn
110
106
  /**
111
107
  * @public
112
108
  */
113
109
  export function useStonecrop(options?: {
114
110
  registry?: Registry
115
- doctype?: DoctypeMeta
111
+ doctype?: Doctype
116
112
  recordId?: string
117
113
  }): BaseStonecropReturn | HSTStonecropReturn {
118
114
  if (!options) options = {}
@@ -124,7 +120,7 @@ export function useStonecrop(options?: {
124
120
  const formData = ref<Record<string, any>>({})
125
121
 
126
122
  // Use refs for router-loaded doctype to maintain reactivity
127
- const routerDoctype = ref<DoctypeMeta | undefined>()
123
+ const routerDoctype = ref<Doctype | undefined>()
128
124
  const routerRecordId = ref<string | undefined>()
129
125
 
130
126
  // Resolved schema with nested Doctype fields expanded
@@ -418,7 +414,7 @@ export function useStonecrop(options?: {
418
414
  * @param recordId - Optional record ID to load
419
415
  * @returns Promise resolving to the loaded or initialized data
420
416
  */
421
- const loadNestedData = (parentPath: string, childDoctype: DoctypeMeta, recordId?: string): Record<string, any> => {
417
+ const loadNestedData = (parentPath: string, childDoctype: Doctype, recordId?: string): Record<string, any> => {
422
418
  if (!stonecrop.value) {
423
419
  return initializeNewRecord(childDoctype)
424
420
  }
@@ -450,7 +446,7 @@ export function useStonecrop(options?: {
450
446
  * @param recordId - The record ID to save
451
447
  * @returns The complete save payload
452
448
  */
453
- const saveRecursive = (doctype: DoctypeMeta, recordId: string): Record<string, any> => {
449
+ const saveRecursive = (doctype: Doctype, recordId: string): Record<string, any> => {
454
450
  if (!hstStore.value || !stonecrop.value) {
455
451
  throw new Error('HST store not initialized')
456
452
  }
@@ -487,7 +483,7 @@ export function useStonecrop(options?: {
487
483
  * @param _childDoctype - The child doctype metadata (unused but kept for API consistency)
488
484
  * @returns Object with scoped provideHSTPath and handleHSTChange
489
485
  */
490
- const createNestedContext = (basePath: string, _childDoctype: DoctypeMeta) => {
486
+ const createNestedContext = (basePath: string, _childDoctype: Doctype) => {
491
487
  const nestedProvideHSTPath = (fieldname: string): string => {
492
488
  return `${basePath}.${fieldname}`
493
489
  }
@@ -570,7 +566,7 @@ export function useStonecrop(options?: {
570
566
  /**
571
567
  * Initialize new record structure based on doctype schema
572
568
  */
573
- function initializeNewRecord(doctype: DoctypeMeta): Record<string, any> {
569
+ function initializeNewRecord(doctype: Doctype): Record<string, any> {
574
570
  const initialData: Record<string, any> = {}
575
571
 
576
572
  if (!doctype.schema) {
@@ -610,7 +606,7 @@ function initializeNewRecord(doctype: DoctypeMeta): Record<string, any> {
610
606
  * Setup deep reactivity between form data and HST store
611
607
  */
612
608
  function setupDeepReactivity(
613
- doctype: DoctypeMeta,
609
+ doctype: Doctype,
614
610
  recordId: string,
615
611
  formData: Ref<Record<string, any>>,
616
612
  hstStore: HSTNode
package/src/doctype.ts CHANGED
@@ -1,12 +1,42 @@
1
+ import { List, Map } from 'immutable'
1
2
  import { Component } from 'vue'
2
3
 
4
+ import type { SchemaTypes } from '@stonecrop/aform'
5
+ import type { UnknownMachineConfig } from 'xstate'
6
+
3
7
  import type { ImmutableDoctype } from './types'
4
8
 
5
9
  /**
6
- * Doctype Meta class
10
+ * Plain object representation of doctype configuration for serialization/API responses.
11
+ * Compatible with the DoctypeMeta type from \@stonecrop/schema.
12
+ * @public
13
+ */
14
+ export type DoctypeConfig = {
15
+ /** Display name of the doctype */
16
+ name: string
17
+ /** URL-friendly slug (kebab-case) */
18
+ slug?: string
19
+ /** Database table name */
20
+ tableName?: string
21
+ /** Field definitions */
22
+ fields?: SchemaTypes[]
23
+ /** Workflow configuration */
24
+ workflow?: UnknownMachineConfig
25
+ /** Actions and their field triggers */
26
+ actions?: Record<string, string[]>
27
+ /** Parent doctype for inheritance */
28
+ inherits?: string
29
+ /** Doctype to use for list views */
30
+ listDoctype?: string
31
+ /** Parent doctype for child tables */
32
+ parentDoctype?: string
33
+ }
34
+
35
+ /**
36
+ * Doctype runtime class with Immutable.js collections for HST change tracking.
7
37
  * @public
8
38
  */
9
- export default class DoctypeMeta {
39
+ export default class Doctype {
10
40
  /**
11
41
  * The doctype name
12
42
  * @public
@@ -52,7 +82,7 @@ export default class DoctypeMeta {
52
82
  readonly component?: Component
53
83
 
54
84
  /**
55
- * Creates a new DoctypeMeta instance
85
+ * Creates a new Doctype instance
56
86
  * @param doctype - The doctype name
57
87
  * @param schema - The doctype schema definition
58
88
  * @param workflow - The doctype workflow configuration (XState machine)
@@ -73,6 +103,84 @@ export default class DoctypeMeta {
73
103
  this.component = component
74
104
  }
75
105
 
106
+ /**
107
+ * Creates a Doctype instance from a plain configuration object.
108
+ * Handles conversion of arrays to Immutable.js collections internally.
109
+ *
110
+ * This is the recommended way to create a Doctype from API responses
111
+ * or configuration files, as it encapsulates the Immutable.js construction
112
+ * that the framework uses internally.
113
+ *
114
+ * @param config - Plain object with doctype configuration (typically from API response)
115
+ * @returns A new Doctype instance with Immutable.js collections
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * // From an API response
120
+ * const response = await client.getMeta({ doctype: 'plan' })
121
+ * const doctype = Doctype.fromObject(response)
122
+ * registry.addDoctype(doctype)
123
+ * ```
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * // From a configuration object
128
+ * const planDoctype = Doctype.fromObject({
129
+ * name: 'Plan',
130
+ * fields: [
131
+ * { fieldname: 'title', label: 'Title', fieldtype: 'Data' },
132
+ * { fieldname: 'status', label: 'Status', fieldtype: 'Data' },
133
+ * ],
134
+ * workflow: {
135
+ * id: 'plan',
136
+ * initial: 'draft',
137
+ * states: { draft: {}, submitted: {} }
138
+ * }
139
+ * })
140
+ * ```
141
+ *
142
+ * @public
143
+ */
144
+ static fromObject(config: DoctypeConfig): Doctype {
145
+ const schema = config.fields ? List(config.fields) : List<SchemaTypes>()
146
+ const actions = config.actions ? Map(config.actions) : Map<string, string[]>()
147
+
148
+ return new Doctype(config.name, schema, config.workflow, actions)
149
+ }
150
+
151
+ /**
152
+ * Returns the schema as a plain array for use with components that expect
153
+ * plain JavaScript arrays (e.g., AForm, ATable).
154
+ *
155
+ * @returns Array of schema fields
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * const schemaArray = doctype.getSchemaArray()
160
+ * // Use with AForm
161
+ * <AForm :schema="schemaArray" v-model:data="formData" />
162
+ * ```
163
+ *
164
+ * @public
165
+ */
166
+ getSchemaArray(): SchemaTypes[] {
167
+ if (!this.schema) return []
168
+ return this.schema.toArray()
169
+ }
170
+
171
+ /**
172
+ * Returns the actions as a plain object for use with components that expect
173
+ * plain JavaScript objects.
174
+ *
175
+ * @returns Object mapping action names to field trigger arrays
176
+ *
177
+ * @public
178
+ */
179
+ getActionsObject(): Record<string, string[]> {
180
+ if (!this.actions) return {}
181
+ return this.actions.toObject()
182
+ }
183
+
76
184
  /**
77
185
  * Returns the transitions available from a given workflow state, derived from the
78
186
  * doctype's XState workflow configuration.
@@ -109,7 +217,7 @@ export default class DoctypeMeta {
109
217
  *
110
218
  * @example
111
219
  * ```ts
112
- * const doctype = new DoctypeMeta('TaskItem', schema, workflow, actions
220
+ * const doctype = new Doctype('TaskItem', schema, workflow, actions)
113
221
  * console.log(doctype.slug) // 'task-item'
114
222
  * ```
115
223
  *
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ export type * from '@stonecrop/atable/types'
3
3
 
4
4
  import { useStonecrop } from './composables/stonecrop'
5
5
  import { useOperationLog, useUndoRedoShortcuts, withBatch } from './composables/operation-log'
6
- import DoctypeMeta from './doctype'
6
+ import Doctype, { type DoctypeConfig } from './doctype'
7
7
  import {
8
8
  getGlobalTriggerEngine,
9
9
  markOperationIrreversible,
@@ -36,7 +36,8 @@ export type { ValidationIssue, ValidationResult, ValidatorOptions } from './sche
36
36
  export { ValidationSeverity } from './schema-validator'
37
37
 
38
38
  export {
39
- DoctypeMeta,
39
+ Doctype,
40
+ DoctypeConfig,
40
41
  Registry,
41
42
  Stonecrop,
42
43
  StonecropOptions,
package/src/registry.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { SchemaTypes } from '@stonecrop/aform'
2
2
  import { Router } from 'vue-router'
3
3
 
4
- import DoctypeMeta from './doctype'
4
+ import Doctype from './doctype'
5
5
  import { getGlobalTriggerEngine } from './field-triggers'
6
6
  import { RouteContext } from './types/registry'
7
7
 
@@ -24,9 +24,9 @@ export default class Registry {
24
24
 
25
25
  /**
26
26
  * The registry property contains a collection of doctypes
27
- * @see {@link DoctypeMeta}
27
+ * @see {@link Doctype}
28
28
  */
29
- readonly registry: Record<string, DoctypeMeta>
29
+ readonly registry: Record<string, Doctype>
30
30
 
31
31
  /**
32
32
  * The Vue router instance
@@ -39,7 +39,7 @@ export default class Registry {
39
39
  * @param router - Optional Vue router instance for route management
40
40
  * @param getMeta - Optional function to fetch doctype metadata from an API
41
41
  */
42
- constructor(router?: Router, getMeta?: (routeContext: RouteContext) => DoctypeMeta | Promise<DoctypeMeta>) {
42
+ constructor(router?: Router, getMeta?: (routeContext: RouteContext) => Doctype | Promise<Doctype>) {
43
43
  if (Registry._root) {
44
44
  return Registry._root
45
45
  }
@@ -52,17 +52,17 @@ export default class Registry {
52
52
 
53
53
  /**
54
54
  * The getMeta function fetches doctype metadata from an API based on route context
55
- * @see {@link DoctypeMeta}
55
+ * @see {@link Doctype}
56
56
  */
57
- getMeta?: (routeContext: RouteContext) => DoctypeMeta | Promise<DoctypeMeta>
57
+ getMeta?: (routeContext: RouteContext) => Doctype | Promise<Doctype>
58
58
 
59
59
  /**
60
60
  * Get doctype metadata
61
61
  * @param doctype - The doctype to fetch metadata for
62
62
  * @returns The doctype metadata
63
- * @see {@link DoctypeMeta}
63
+ * @see {@link Doctype}
64
64
  */
65
- addDoctype(doctype: DoctypeMeta) {
65
+ addDoctype(doctype: Doctype) {
66
66
  if (!(doctype.slug in this.registry)) {
67
67
  this.registry[doctype.slug] = doctype
68
68
  }
@@ -153,7 +153,7 @@ export default class Registry {
153
153
  'options' in field &&
154
154
  typeof field.options === 'string'
155
155
  ) {
156
- const doctypeSlug = field.options as string
156
+ const doctypeSlug = field.options
157
157
 
158
158
  // Circular reference protection
159
159
  if (seen.has(doctypeSlug)) {
@@ -275,10 +275,10 @@ export default class Registry {
275
275
  /**
276
276
  * Get a registered doctype by slug
277
277
  * @param slug - The doctype slug to look up
278
- * @returns The DoctypeMeta instance if found, or undefined
278
+ * @returns The Doctype instance if found, or undefined
279
279
  * @public
280
280
  */
281
- getDoctype(slug: string): DoctypeMeta | undefined {
281
+ getDoctype(slug: string): Doctype | undefined {
282
282
  return this.registry[slug]
283
283
  }
284
284
 
package/src/stonecrop.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { DataClient } from '@stonecrop/schema'
2
2
  import { reactive } from 'vue'
3
3
 
4
- import DoctypeMeta from './doctype'
4
+ import Doctype from './doctype'
5
5
  import Registry from './registry'
6
6
  import { createHST, type HSTNode } from './stores/hst'
7
7
  import { useOperationLogStore } from './stores/operation-log'
@@ -119,7 +119,7 @@ export class Stonecrop {
119
119
  // Extend Registry.addDoctype to auto-create HST store sections
120
120
  const originalAddDoctype = this.registry.addDoctype.bind(this.registry)
121
121
 
122
- this.registry.addDoctype = (doctype: DoctypeMeta) => {
122
+ this.registry.addDoctype = (doctype: Doctype) => {
123
123
  // Call original method
124
124
  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
125
125
  originalAddDoctype(doctype)
@@ -136,7 +136,7 @@ export class Stonecrop {
136
136
  * @param doctype - The doctype to get records for
137
137
  * @returns HST node containing records hash
138
138
  */
139
- records(doctype: string | DoctypeMeta): HSTNode {
139
+ records(doctype: string | Doctype): HSTNode {
140
140
  const slug = typeof doctype === 'string' ? doctype : doctype.slug
141
141
  this.ensureDoctypeExists(slug)
142
142
  return this.hstStore.getNode(slug)
@@ -148,7 +148,7 @@ export class Stonecrop {
148
148
  * @param recordId - The record ID
149
149
  * @param recordData - The record data
150
150
  */
151
- addRecord(doctype: string | DoctypeMeta, recordId: string, recordData: any): void {
151
+ addRecord(doctype: string | Doctype, recordId: string, recordData: any): void {
152
152
  const slug = typeof doctype === 'string' ? doctype : doctype.slug
153
153
 
154
154
  this.ensureDoctypeExists(slug)
@@ -163,7 +163,7 @@ export class Stonecrop {
163
163
  * @param recordId - The record ID
164
164
  * @returns HST node for the record or undefined
165
165
  */
166
- getRecordById(doctype: string | DoctypeMeta, recordId: string): HSTNode | undefined {
166
+ getRecordById(doctype: string | Doctype, recordId: string): HSTNode | undefined {
167
167
  const slug = typeof doctype === 'string' ? doctype : doctype.slug
168
168
  this.ensureDoctypeExists(slug)
169
169
 
@@ -188,7 +188,7 @@ export class Stonecrop {
188
188
  * @param doctype - The doctype
189
189
  * @param recordId - The record ID
190
190
  */
191
- removeRecord(doctype: string | DoctypeMeta, recordId: string): void {
191
+ removeRecord(doctype: string | Doctype, recordId: string): void {
192
192
  const slug = typeof doctype === 'string' ? doctype : doctype.slug
193
193
  this.ensureDoctypeExists(slug)
194
194
 
@@ -203,7 +203,7 @@ export class Stonecrop {
203
203
  * @param doctype - The doctype
204
204
  * @returns Array of record IDs
205
205
  */
206
- getRecordIds(doctype: string | DoctypeMeta): string[] {
206
+ getRecordIds(doctype: string | Doctype): string[] {
207
207
  const slug = typeof doctype === 'string' ? doctype : doctype.slug
208
208
  this.ensureDoctypeExists(slug)
209
209
 
@@ -219,7 +219,7 @@ export class Stonecrop {
219
219
  * Clear all records for a doctype
220
220
  * @param doctype - The doctype
221
221
  */
222
- clearRecords(doctype: string | DoctypeMeta): void {
222
+ clearRecords(doctype: string | Doctype): void {
223
223
  const slug = typeof doctype === 'string' ? doctype : doctype.slug
224
224
  this.ensureDoctypeExists(slug)
225
225
 
@@ -234,7 +234,7 @@ export class Stonecrop {
234
234
  * Setup method for doctype initialization
235
235
  * @param doctype - The doctype to setup
236
236
  */
237
- setup(doctype: DoctypeMeta): void {
237
+ setup(doctype: Doctype): void {
238
238
  // Ensure doctype exists in store
239
239
  this.ensureDoctypeExists(doctype.slug)
240
240
  }
@@ -246,7 +246,7 @@ export class Stonecrop {
246
246
  * @param action - The action to run
247
247
  * @param args - Action arguments (typically record IDs)
248
248
  */
249
- runAction(doctype: DoctypeMeta, action: string, args?: any[]): void {
249
+ runAction(doctype: Doctype, action: string, args?: any[]): void {
250
250
  const registry = this.registry.registry[doctype.slug]
251
251
  const actions = registry?.actions?.get(action)
252
252
  const recordIds = Array.isArray(args) ? args.filter((arg): arg is string => typeof arg === 'string') : undefined
@@ -285,7 +285,7 @@ export class Stonecrop {
285
285
  * @param doctype - The doctype
286
286
  * @throws Error if no data client has been configured
287
287
  */
288
- async getRecords(doctype: DoctypeMeta): Promise<void> {
288
+ async getRecords(doctype: Doctype): Promise<void> {
289
289
  if (!this._client) {
290
290
  throw new Error(
291
291
  'No data client configured. Call setClient() with a DataClient implementation ' +
@@ -309,7 +309,7 @@ export class Stonecrop {
309
309
  * @param recordId - The record ID
310
310
  * @throws Error if no data client has been configured
311
311
  */
312
- async getRecord(doctype: DoctypeMeta, recordId: string): Promise<void> {
312
+ async getRecord(doctype: Doctype, recordId: string): Promise<void> {
313
313
  if (!this._client) {
314
314
  throw new Error(
315
315
  'No data client configured. Call setClient() with a DataClient implementation ' +
@@ -335,7 +335,7 @@ export class Stonecrop {
335
335
  * @throws Error if no data client has been configured
336
336
  */
337
337
  async dispatchAction(
338
- doctype: DoctypeMeta,
338
+ doctype: Doctype,
339
339
  action: string,
340
340
  args?: unknown[]
341
341
  ): Promise<{ success: boolean; data: unknown; error: string | null }> {
@@ -386,13 +386,13 @@ export class Stonecrop {
386
386
  * empty the doctype's declared `workflow.initial` state is used as the fallback,
387
387
  * giving callers a reliable state name without having to duplicate that logic.
388
388
  *
389
- * @param doctype - The doctype slug or DoctypeMeta instance
389
+ * @param doctype - The doctype slug or Doctype instance
390
390
  * @param recordId - The record identifier
391
391
  * @returns The current state name, or an empty string if the doctype has no workflow
392
392
  *
393
393
  * @public
394
394
  */
395
- getRecordState(doctype: string | DoctypeMeta, recordId: string): string {
395
+ getRecordState(doctype: string | Doctype, recordId: string): string {
396
396
  const slug = typeof doctype === 'string' ? doctype : doctype.slug
397
397
  const meta = this.registry.getDoctype(slug)
398
398
  if (!meta?.workflow) return ''
@@ -5,7 +5,7 @@ import type { Component } from 'vue'
5
5
  import type { Router } from 'vue-router'
6
6
  import type { AnyStateNodeConfig, UnknownMachineConfig } from 'xstate'
7
7
 
8
- import type DoctypeMeta from '../doctype'
8
+ import type Doctype from '../doctype'
9
9
  import Registry from '../registry'
10
10
  import { Stonecrop } from '../stonecrop'
11
11
  import type { RouteContext } from './registry'
@@ -47,7 +47,7 @@ export type Schema = {
47
47
  export type InstallOptions = {
48
48
  router?: Router
49
49
  components?: Record<string, Component>
50
- getMeta?: (routeContext: RouteContext) => DoctypeMeta | Promise<DoctypeMeta>
50
+ getMeta?: (routeContext: RouteContext) => Doctype | Promise<Doctype>
51
51
  /**
52
52
  * Data client for fetching doctype metadata and records.
53
53
  * Use \@stonecrop/graphql-client's StonecropClient for GraphQL backends,