@stonecrop/stonecrop 0.10.11 → 0.10.13

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 (58) hide show
  1. package/dist/composables/stonecrop.js +26 -110
  2. package/dist/field-triggers.js +5 -6
  3. package/dist/index.js +9 -6
  4. package/dist/plugins/index.js +4 -1
  5. package/dist/registry.js +62 -61
  6. package/dist/schema-validator.js +1 -13
  7. package/dist/src/composables/stonecrop.d.ts +2 -74
  8. package/dist/src/composables/stonecrop.d.ts.map +1 -1
  9. package/dist/src/doctype.d.ts +1 -27
  10. package/dist/src/doctype.d.ts.map +1 -1
  11. package/dist/src/field-triggers.d.ts.map +1 -1
  12. package/dist/src/index.d.ts +6 -9
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/plugins/index.d.ts.map +1 -1
  15. package/dist/src/registry.d.ts +13 -10
  16. package/dist/src/registry.d.ts.map +1 -1
  17. package/dist/src/schema-validator.d.ts +1 -62
  18. package/dist/src/schema-validator.d.ts.map +1 -1
  19. package/dist/src/stonecrop.d.ts +38 -17
  20. package/dist/src/stonecrop.d.ts.map +1 -1
  21. package/dist/src/stores/operation-log.d.ts +1 -1
  22. package/dist/src/types/composable.d.ts +230 -0
  23. package/dist/src/types/composable.d.ts.map +1 -0
  24. package/dist/src/types/doctype.d.ts +57 -0
  25. package/dist/src/types/doctype.d.ts.map +1 -0
  26. package/dist/src/types/index.d.ts +6 -67
  27. package/dist/src/types/index.d.ts.map +1 -1
  28. package/dist/src/types/plugin.d.ts +37 -0
  29. package/dist/src/types/plugin.d.ts.map +1 -0
  30. package/dist/src/types/schema-validator.d.ts +64 -0
  31. package/dist/src/types/schema-validator.d.ts.map +1 -0
  32. package/dist/src/types/stonecrop.d.ts +17 -0
  33. package/dist/src/types/stonecrop.d.ts.map +1 -0
  34. package/dist/stonecrop.css +1 -0
  35. package/dist/stonecrop.d.ts +206 -14
  36. package/dist/stonecrop.js +1751 -1631
  37. package/dist/stonecrop.js.map +1 -1
  38. package/dist/types/composable.js +0 -0
  39. package/dist/types/doctype.js +0 -0
  40. package/dist/types/index.js +7 -2
  41. package/dist/types/plugin.js +0 -0
  42. package/dist/types/schema-validator.js +13 -0
  43. package/dist/types/stonecrop.js +0 -0
  44. package/package.json +4 -4
  45. package/src/composables/stonecrop.ts +34 -218
  46. package/src/doctype.ts +2 -29
  47. package/src/field-triggers.ts +5 -6
  48. package/src/index.ts +12 -19
  49. package/src/plugins/index.ts +4 -1
  50. package/src/registry.ts +61 -66
  51. package/src/schema-validator.ts +3 -66
  52. package/src/stonecrop.ts +148 -17
  53. package/src/types/composable.ts +245 -0
  54. package/src/types/doctype.ts +60 -0
  55. package/src/types/index.ts +7 -74
  56. package/src/types/plugin.ts +38 -0
  57. package/src/types/schema-validator.ts +67 -0
  58. package/src/types/stonecrop.ts +17 -0
package/src/registry.ts CHANGED
@@ -85,17 +85,18 @@ export default class Registry {
85
85
  }
86
86
 
87
87
  /**
88
- * Resolve nested Doctype and Table fields in a schema by embedding child schemas inline.
88
+ * Resolve nested Doctype fields in a schema by embedding child schemas inline.
89
89
  *
90
90
  * @remarks
91
91
  * Walks the schema array and for each field with `fieldtype: 'Doctype'` and a string
92
- * `options` value, looks up the referenced doctype in the registry and embeds its schema
93
- * as the field's `schema` property. Recurses for deeply nested doctypes.
92
+ * `options` value, looks up the referenced doctype in the registry and:
94
93
  *
95
- * For fields with `fieldtype: 'Table'`, looks up the referenced child doctype and
96
- * auto-derives `columns` from its schema fields (unless columns are already provided).
97
- * Also sets sensible defaults for `component` (`'ATable'`) and `config` (`{ view: 'list' }`).
98
- * Row data is expected to come from the parent form's data model at `data[fieldname]`.
94
+ * - If `cardinality: 'many'`: auto-derives `columns` from the child doctype's schema,
95
+ * sets `component: 'ATable'`, `config: { view: 'list' }`, and initializes `rows: []`.
96
+ * - Otherwise (default/`cardinality: 'one'`): embeds the child schema as the field's
97
+ * `schema` property for 1:1 nested forms.
98
+ *
99
+ * Recurses for deeply nested doctypes. Circular references are protected against.
99
100
  *
100
101
  * Returns a new array — does not mutate the original schema.
101
102
  *
@@ -137,64 +138,52 @@ export default class Registry {
137
138
  // Convert Immutable.List to plain array if needed
138
139
  const childSchema: SchemaTypes[] = Array.isArray(doctype.schema) ? doctype.schema : Array.from(doctype.schema)
139
140
 
140
- // Recurse into child schema to resolve deeply nested doctypes
141
- seen.add(doctypeSlug)
142
- const resolvedChild = this.resolveSchema(childSchema, seen)
143
- seen.delete(doctypeSlug)
144
-
145
- return { ...field, schema: resolvedChild }
146
- }
147
- }
141
+ // Check cardinality to determine handling
142
+ const cardinality = 'cardinality' in field ? field.cardinality : undefined
148
143
 
149
- // Resolve Table fieldtype — 1:many child doctype rendered as ATable
150
- if (
151
- 'fieldtype' in field &&
152
- field.fieldtype === 'Table' &&
153
- 'options' in field &&
154
- typeof field.options === 'string'
155
- ) {
156
- const doctypeSlug = field.options
144
+ if (cardinality === 'many') {
145
+ // 1:many child table - derive columns, set component, config, rows
146
+ const resolved: Record<string, any> = { ...field }
157
147
 
158
- // Circular reference protection
159
- if (seen.has(doctypeSlug)) {
160
- return { ...field }
161
- }
148
+ // Auto-derive columns from child schema fields if not already provided
149
+ if (!('columns' in field) || !field.columns) {
150
+ resolved.columns = childSchema.map(childField => ({
151
+ name: childField.fieldname,
152
+ fieldname: childField.fieldname,
153
+ label: ('label' in childField && childField.label) || childField.fieldname,
154
+ fieldtype: 'fieldtype' in childField ? childField.fieldtype : 'Data',
155
+ align: ('align' in childField && childField.align) || 'left',
156
+ edit: 'edit' in childField ? childField.edit : true,
157
+ width: ('width' in childField && childField.width) || '20ch',
158
+ }))
159
+ }
162
160
 
163
- const doctype = this.registry[doctypeSlug]
164
- if (doctype && doctype.schema) {
165
- const childSchema: SchemaTypes[] = Array.isArray(doctype.schema) ? doctype.schema : Array.from(doctype.schema)
166
- const resolved: Record<string, any> = { ...field }
161
+ // Set default component if not already specified
162
+ if (!resolved.component) {
163
+ resolved.component = 'ATable'
164
+ }
167
165
 
168
- // Auto-derive columns from child schema fields if not already provided
169
- if (!('columns' in field) || !field.columns) {
170
- resolved.columns = childSchema.map(childField => ({
171
- name: childField.fieldname,
172
- fieldname: childField.fieldname,
173
- label: ('label' in childField && childField.label) || childField.fieldname,
174
- fieldtype: 'fieldtype' in childField ? childField.fieldtype : 'Data',
175
- align: ('align' in childField && childField.align) || 'left',
176
- edit: 'edit' in childField ? childField.edit : true,
177
- width: ('width' in childField && childField.width) || '20ch',
178
- }))
179
- }
166
+ // Set default config if not already specified
167
+ if (!('config' in field) || !field.config) {
168
+ resolved.config = { view: 'list' }
169
+ }
180
170
 
181
- // Set default component if not already specified
182
- if (!resolved.component) {
183
- resolved.component = 'ATable'
184
- }
171
+ // Initialize rows to empty array so componentProps fallback
172
+ // routes data from the form's dataModel[fieldname]
173
+ if (!('rows' in field) || !field.rows) {
174
+ resolved.rows = []
175
+ }
185
176
 
186
- // Set default config if not already specified
187
- if (!('config' in field) || !field.config) {
188
- resolved.config = { view: 'list' }
189
- }
177
+ return resolved as SchemaTypes
178
+ } else {
179
+ // 1:1 nested form (default cardinality: 'one')
180
+ // Recurse into child schema to resolve deeply nested doctypes
181
+ seen.add(doctypeSlug)
182
+ const resolvedChild = this.resolveSchema(childSchema, seen)
183
+ seen.delete(doctypeSlug)
190
184
 
191
- // Initialize rows to empty array so componentProps fallback
192
- // routes data from the form's dataModel[fieldname]
193
- if (!('rows' in field) || !field.rows) {
194
- resolved.rows = []
185
+ return { ...field, schema: resolvedChild }
195
186
  }
196
-
197
- return resolved as SchemaTypes
198
187
  }
199
188
  }
200
189
 
@@ -211,11 +200,13 @@ export default class Registry {
211
200
  * - Data, Text → `''`
212
201
  * - Check → `false`
213
202
  * - Int, Float, Decimal, Currency, Quantity → `0`
214
- * - Table → `[]`
215
- * - JSON, Doctype → `{}`
203
+ * - JSON → `{}`
204
+ * - Doctype with `cardinality: 'many'` → `[]`
205
+ * - Doctype without `cardinality` or `cardinality: 'one'` → recursively initializes nested record
216
206
  * - All others → `null`
217
207
  *
218
- * For Doctype fields with a resolved `schema` array, recursively initializes the nested record.
208
+ * For Doctype fields with a resolved `schema` array (cardinality: 'one'), recursively
209
+ * initializes the nested record.
219
210
  *
220
211
  * @param schema - The schema array to derive defaults from
221
212
  * @returns A plain object with default values for each field
@@ -250,20 +241,24 @@ export default class Registry {
250
241
  case 'Quantity':
251
242
  record[field.fieldname] = 0
252
243
  break
253
- case 'Table':
254
- record[field.fieldname] = []
255
- break
256
244
  case 'JSON':
257
245
  record[field.fieldname] = {}
258
246
  break
259
- case 'Doctype':
260
- // If nested schema is resolved, recursively initialize
261
- if ('schema' in field && Array.isArray(field.schema)) {
247
+ case 'Doctype': {
248
+ // Check cardinality to determine initial value
249
+ const cardinality = 'cardinality' in field ? field.cardinality : undefined
250
+ if (cardinality === 'many') {
251
+ // 1:many child table - initialize as empty array
252
+ record[field.fieldname] = []
253
+ } else if ('schema' in field && Array.isArray(field.schema)) {
254
+ // 1:1 nested form with resolved schema - recursively initialize
262
255
  record[field.fieldname] = this.initializeRecord(field.schema)
263
256
  } else {
257
+ // 1:1 without resolved schema - empty object
264
258
  record[field.fieldname] = {}
265
259
  }
266
260
  break
261
+ }
267
262
  default:
268
263
  record[field.fieldname] = null
269
264
  }
@@ -7,74 +7,11 @@
7
7
  import type { SchemaTypes } from '@stonecrop/aform'
8
8
  import type { List, Map as ImmutableMap } from 'immutable'
9
9
  import type { AnyStateNodeConfig } from 'xstate'
10
+
10
11
  import { getGlobalTriggerEngine } from './field-triggers'
11
12
  import type Registry from './registry'
12
-
13
- /**
14
- * Validation severity levels
15
- * @public
16
- */
17
- export enum ValidationSeverity {
18
- /** Blocking error that prevents save */
19
- ERROR = 'error',
20
- /** Advisory warning that allows save */
21
- WARNING = 'warning',
22
- /** Informational message */
23
- INFO = 'info',
24
- }
25
-
26
- /**
27
- * Validation issue
28
- * @public
29
- */
30
- export interface ValidationIssue {
31
- /** Severity level */
32
- severity: ValidationSeverity
33
- /** Validation rule that failed */
34
- rule: string
35
- /** Human-readable message */
36
- message: string
37
- /** Doctype name */
38
- doctype?: string
39
- /** Field name if applicable */
40
- fieldname?: string
41
- /** Additional context */
42
- context?: Record<string, unknown>
43
- }
44
-
45
- /**
46
- * Validation result
47
- * @public
48
- */
49
- export interface ValidationResult {
50
- /** Whether validation passed (no blocking errors) */
51
- valid: boolean
52
- /** List of validation issues */
53
- issues: ValidationIssue[]
54
- /** Count of errors */
55
- errorCount: number
56
- /** Count of warnings */
57
- warningCount: number
58
- /** Count of info messages */
59
- infoCount: number
60
- }
61
-
62
- /**
63
- * Schema validator options
64
- * @public
65
- */
66
- export interface ValidatorOptions {
67
- /** Registry instance for doctype lookups */
68
- registry?: Registry
69
- /** Whether to validate Link field targets */
70
- validateLinkTargets?: boolean
71
- /** Whether to validate workflow reachability */
72
- validateWorkflows?: boolean
73
- /** Whether to validate action registration */
74
- validateActions?: boolean
75
- /** Whether to validate required schema properties */
76
- validateRequiredProperties?: boolean
77
- }
13
+ import { ValidationSeverity } from './types/schema-validator'
14
+ import type { ValidationIssue, ValidationResult, ValidatorOptions } from './types/schema-validator'
78
15
 
79
16
  /**
80
17
  * Schema validator class
package/src/stonecrop.ts CHANGED
@@ -1,4 +1,11 @@
1
- import type { DataClient, WorkflowMeta } from '@stonecrop/schema'
1
+ import {
2
+ type DoctypeManySchema,
3
+ type DoctypeOneSchema,
4
+ type DoctypeSchema,
5
+ type SchemaTypes,
6
+ isDoctypeMany,
7
+ } from '@stonecrop/aform'
8
+ import type { DataClient } from '@stonecrop/schema'
2
9
  import { reactive } from 'vue'
3
10
 
4
11
  import Doctype from './doctype'
@@ -7,28 +14,21 @@ import { createHST, type HSTNode } from './stores/hst'
7
14
  import { useOperationLogStore } from './stores/operation-log'
8
15
  import type { OperationLogConfig } from './types/operation-log'
9
16
  import type { RouteContext } from './types/registry'
17
+ import type { StonecropOptions } from './types/stonecrop'
10
18
 
11
19
  /**
12
- * Options for constructing a Stonecrop instance directly.
13
- * When using the Vue plugin, pass these via `InstallOptions` instead.
20
+ * Main Stonecrop class with HST integration and built-in Operation Log
14
21
  * @public
15
22
  */
16
- export interface StonecropOptions {
23
+ export class Stonecrop {
17
24
  /**
18
- * Data client for fetching doctype metadata and records.
19
- * Use \@stonecrop/graphql-client's StonecropClient for GraphQL backends,
20
- * or implement DataClient for custom data sources.
21
- *
22
- * Can be set later via `setClient()` for deferred configuration.
25
+ * Singleton instance of Stonecrop. Only one Stonecrop instance can exist
26
+ * per application, ensuring consistent HST state and registry access.
27
+ * Subsequent constructor calls return this instance instead of creating new ones.
28
+ * @internal
23
29
  */
24
- client?: DataClient
25
- }
30
+ static _root: Stonecrop
26
31
 
27
- /**
28
- * Main Stonecrop class with HST integration and built-in Operation Log
29
- * @public
30
- */
31
- export class Stonecrop {
32
32
  private hstStore: HSTNode
33
33
  private _operationLogStore?: ReturnType<typeof useOperationLogStore>
34
34
  private _operationLogConfig?: Partial<OperationLogConfig>
@@ -38,12 +38,17 @@ export class Stonecrop {
38
38
  readonly registry: Registry
39
39
 
40
40
  /**
41
- * Creates a new Stonecrop instance with HST integration
41
+ * Creates a new Stonecrop instance with HST integration (singleton pattern)
42
42
  * @param registry - The Registry instance containing doctype definitions
43
43
  * @param operationLogConfig - Optional configuration for the operation log
44
44
  * @param options - Options including the data client (can be set later via setClient)
45
45
  */
46
46
  constructor(registry: Registry, operationLogConfig?: Partial<OperationLogConfig>, options?: StonecropOptions) {
47
+ if (Stonecrop._root) {
48
+ return Stonecrop._root
49
+ }
50
+ Stonecrop._root = this
51
+
47
52
  this.registry = registry
48
53
 
49
54
  // Store config for lazy initialization
@@ -417,4 +422,130 @@ export class Stonecrop {
417
422
 
418
423
  return status || initialState
419
424
  }
425
+
426
+ /**
427
+ * Collect a record payload with all nested doctype fields from HST
428
+ * @param doctype - The doctype metadata
429
+ * @param recordId - The record ID to collect
430
+ * @returns The complete record payload ready for API submission
431
+ * @public
432
+ */
433
+ collectRecordPayload(doctype: Doctype, recordId: string): Record<string, any> {
434
+ const recordPath = `${doctype.slug}.${recordId}`
435
+ const recordData = this.hstStore.get(recordPath) || {}
436
+
437
+ const payload: Record<string, any> = { ...recordData }
438
+
439
+ const schemaArray = doctype.schema
440
+ ? Array.isArray(doctype.schema)
441
+ ? doctype.schema
442
+ : Array.from(doctype.schema)
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
+ )
465
+
466
+ for (const field of doctypeManyFields) {
467
+ const doctypeField = field as DoctypeManySchema
468
+ const fieldPath = `${recordPath}.${doctypeField.fieldname}`
469
+ const arrayData = this.hstStore.get(fieldPath)
470
+ if (Array.isArray(arrayData)) {
471
+ payload[doctypeField.fieldname] = arrayData
472
+ }
473
+ }
474
+
475
+ return payload
476
+ }
477
+
478
+ /**
479
+ * Load nested data from HST or initialize with defaults
480
+ * @param parentPath - The HST path to check for existing data
481
+ * @param childDoctype - The child doctype metadata
482
+ * @param _recordId - Optional record ID to load
483
+ * @returns The loaded or initialized data
484
+ * @public
485
+ */
486
+ loadNestedData(parentPath: string, childDoctype: Doctype, _recordId?: string): Record<string, any> {
487
+ // Check if data already exists in HST
488
+ const existingData = this.hstStore.get(parentPath)
489
+ if (existingData && typeof existingData === 'object') {
490
+ return existingData as Record<string, any>
491
+ }
492
+
493
+ // TODO: If recordId provided and no HST data, fetch from API using this._client
494
+ // For now, always fall through to initialize new record
495
+
496
+ // Resolve schema and initialize with defaults
497
+ const schemaArray = childDoctype.schema
498
+ ? Array.isArray(childDoctype.schema)
499
+ ? childDoctype.schema
500
+ : Array.from(childDoctype.schema)
501
+ : []
502
+ const resolvedSchema = this.registry.resolveSchema(schemaArray)
503
+ return this.registry.initializeRecord(resolvedSchema)
504
+ }
420
505
  }
506
+
507
+ /**
508
+ * Recursively collect nested data from HST using pre-resolved schemas
509
+ * @param resolvedSchema - The already-resolved schema (with nested schemas embedded)
510
+ * @param basePath - The base path in HST (e.g., "customer.123.address")
511
+ * @param hstStore - The HST store instance
512
+ * @returns The collected data object
513
+ * @public
514
+ */
515
+ function collectNestedData(resolvedSchema: SchemaTypes[], basePath: string, hstStore: HSTNode): Record<string, any> {
516
+ const data = hstStore.get(basePath) || {}
517
+ const payload: Record<string, any> = { ...data }
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
+ }
547
+
548
+ return payload
549
+ }
550
+
551
+ export { collectNestedData }