@stonecrop/stonecrop 0.11.0 → 0.11.1
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 +4 -4
- package/dist/composable.js +0 -1
- package/dist/composables/use-lazy-link-state.js +0 -125
- package/dist/composables/use-stonecrop.js +0 -476
- package/dist/operation-log-DB-dGNT9.js +0 -593
- package/dist/operation-log-DB-dGNT9.js.map +0 -1
- package/dist/src/composable.d.ts +0 -11
- package/dist/src/composable.d.ts.map +0 -1
- package/dist/src/composable.js +0 -477
- package/dist/src/composables/operation-log.js +0 -224
- package/dist/src/composables/stonecrop.js +0 -574
- package/dist/src/composables/use-lazy-link-state.d.ts +0 -25
- package/dist/src/composables/use-lazy-link-state.d.ts.map +0 -1
- package/dist/src/composables/use-stonecrop.d.ts +0 -93
- package/dist/src/composables/use-stonecrop.d.ts.map +0 -1
- package/dist/src/composables/useNestedSchema.d.ts +0 -110
- package/dist/src/composables/useNestedSchema.d.ts.map +0 -1
- package/dist/src/composables/useNestedSchema.js +0 -155
- package/dist/src/doctype.js +0 -234
- package/dist/src/exceptions.js +0 -16
- package/dist/src/field-triggers.js +0 -567
- package/dist/src/index.js +0 -23
- package/dist/src/plugins/index.js +0 -96
- package/dist/src/registry.js +0 -246
- package/dist/src/schema-validator.js +0 -315
- package/dist/src/stonecrop.js +0 -339
- package/dist/src/stores/data.d.ts +0 -11
- package/dist/src/stores/data.d.ts.map +0 -1
- package/dist/src/stores/hst.js +0 -495
- package/dist/src/stores/index.js +0 -12
- package/dist/src/stores/operation-log.js +0 -568
- package/dist/src/stores/xstate.d.ts +0 -31
- package/dist/src/stores/xstate.d.ts.map +0 -1
- package/dist/src/tsdoc-metadata.json +0 -11
- package/dist/src/types/field-triggers.js +0 -4
- package/dist/src/types/index.js +0 -4
- package/dist/src/types/operation-log.js +0 -0
- package/dist/src/types/registry.js +0 -0
- package/dist/src/utils.d.ts +0 -24
- package/dist/src/utils.d.ts.map +0 -1
- package/dist/stonecrop.css +0 -1
- package/dist/stonecrop.umd.cjs +0 -6
- package/dist/stonecrop.umd.cjs.map +0 -1
- package/dist/stores/data.js +0 -7
- package/dist/stores/xstate.js +0 -29
- package/dist/tests/setup.d.ts +0 -5
- package/dist/tests/setup.d.ts.map +0 -1
- package/dist/tests/setup.js +0 -15
- package/dist/utils.js +0 -46
package/dist/src/registry.js
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { getGlobalTriggerEngine } from './field-triggers';
|
|
2
|
-
/**
|
|
3
|
-
* Stonecrop Registry class
|
|
4
|
-
* @public
|
|
5
|
-
*/
|
|
6
|
-
export default class Registry {
|
|
7
|
-
/**
|
|
8
|
-
* The root Registry instance
|
|
9
|
-
*/
|
|
10
|
-
static _root;
|
|
11
|
-
/**
|
|
12
|
-
* The name of the Registry instance
|
|
13
|
-
*
|
|
14
|
-
* @defaultValue 'Registry'
|
|
15
|
-
*/
|
|
16
|
-
name = 'Registry';
|
|
17
|
-
/**
|
|
18
|
-
* The registry property contains a collection of doctypes
|
|
19
|
-
* @see {@link Doctype}
|
|
20
|
-
*/
|
|
21
|
-
registry = {};
|
|
22
|
-
/**
|
|
23
|
-
* The Vue router instance
|
|
24
|
-
* @see {@link https://router.vuejs.org/}
|
|
25
|
-
*/
|
|
26
|
-
router;
|
|
27
|
-
/**
|
|
28
|
-
* Creates a new Registry instance (singleton pattern)
|
|
29
|
-
* @param router - Optional Vue router instance for route management
|
|
30
|
-
* @param getMeta - Optional function to fetch doctype metadata from an API
|
|
31
|
-
*/
|
|
32
|
-
constructor(router, getMeta) {
|
|
33
|
-
if (Registry._root) {
|
|
34
|
-
return Registry._root;
|
|
35
|
-
}
|
|
36
|
-
Registry._root = this;
|
|
37
|
-
this.router = router;
|
|
38
|
-
this.getMeta = getMeta;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* The getMeta function fetches doctype metadata from an API based on route context
|
|
42
|
-
* @see {@link Doctype}
|
|
43
|
-
*/
|
|
44
|
-
getMeta;
|
|
45
|
-
/**
|
|
46
|
-
* Get doctype metadata
|
|
47
|
-
* @param doctype - The doctype to fetch metadata for
|
|
48
|
-
* @returns The doctype metadata
|
|
49
|
-
* @see {@link Doctype}
|
|
50
|
-
*/
|
|
51
|
-
addDoctype(doctype) {
|
|
52
|
-
if (!(doctype.slug in this.registry)) {
|
|
53
|
-
this.registry[doctype.slug] = doctype;
|
|
54
|
-
}
|
|
55
|
-
// Register actions (including field triggers) with the field trigger engine
|
|
56
|
-
const triggerEngine = getGlobalTriggerEngine();
|
|
57
|
-
// Register under both doctype name and slug to handle different lookup patterns
|
|
58
|
-
triggerEngine.registerDoctypeActions(doctype.doctype, doctype.actions);
|
|
59
|
-
if (doctype.slug !== doctype.doctype) {
|
|
60
|
-
triggerEngine.registerDoctypeActions(doctype.slug, doctype.actions);
|
|
61
|
-
}
|
|
62
|
-
if (doctype.component && this.router && !this.router.hasRoute(doctype.doctype)) {
|
|
63
|
-
this.router.addRoute({
|
|
64
|
-
path: `/${doctype.slug}`,
|
|
65
|
-
name: doctype.slug,
|
|
66
|
-
component: doctype.component,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Resolve nested Doctype fields in a schema by embedding child schemas inline.
|
|
72
|
-
*
|
|
73
|
-
* @remarks
|
|
74
|
-
* Walks the schema array and for each field with `fieldtype: 'Doctype'` and a string
|
|
75
|
-
* `options` value, looks up the referenced doctype in the registry and:
|
|
76
|
-
*
|
|
77
|
-
* - If `cardinality: 'many'`: auto-derives `columns` from the child doctype's schema,
|
|
78
|
-
* sets `component: 'ATable'`, `config: { view: 'list' }`, and initializes `rows: []`.
|
|
79
|
-
* - Otherwise (default/`cardinality: 'one'`): embeds the child schema as the field's
|
|
80
|
-
* `schema` property for 1:1 nested forms.
|
|
81
|
-
*
|
|
82
|
-
* Recurses for deeply nested doctypes. Circular references are protected against.
|
|
83
|
-
*
|
|
84
|
-
* Returns a new array — does not mutate the original schema.
|
|
85
|
-
*
|
|
86
|
-
* @param schema - The schema array to resolve
|
|
87
|
-
* @returns A new schema array with nested Doctype fields resolved
|
|
88
|
-
*
|
|
89
|
-
* @example
|
|
90
|
-
* ```ts
|
|
91
|
-
* registry.addDoctype(addressDoctype)
|
|
92
|
-
* registry.addDoctype(customerDoctype)
|
|
93
|
-
*
|
|
94
|
-
* // Before: customer schema has { fieldname: 'address', fieldtype: 'Doctype', options: 'address' }
|
|
95
|
-
* const resolved = registry.resolveSchema(customerSchema)
|
|
96
|
-
* // After: address field now has schema: [...address fields...]
|
|
97
|
-
* ```
|
|
98
|
-
*
|
|
99
|
-
* @public
|
|
100
|
-
*/
|
|
101
|
-
resolveSchema(schema, visited) {
|
|
102
|
-
const seen = visited || new Set();
|
|
103
|
-
return schema.map(field => {
|
|
104
|
-
// Check for Doctype fieldtype with a string options (slug reference)
|
|
105
|
-
if ('fieldtype' in field &&
|
|
106
|
-
field.fieldtype === 'Doctype' &&
|
|
107
|
-
'options' in field &&
|
|
108
|
-
typeof field.options === 'string') {
|
|
109
|
-
const doctypeSlug = field.options;
|
|
110
|
-
// Circular reference protection
|
|
111
|
-
if (seen.has(doctypeSlug)) {
|
|
112
|
-
return { ...field };
|
|
113
|
-
}
|
|
114
|
-
const doctype = this.registry[doctypeSlug];
|
|
115
|
-
if (doctype && doctype.schema) {
|
|
116
|
-
// Convert Immutable.List to plain array if needed
|
|
117
|
-
const childSchema = Array.isArray(doctype.schema) ? doctype.schema : Array.from(doctype.schema);
|
|
118
|
-
// Check cardinality to determine handling
|
|
119
|
-
const cardinality = 'cardinality' in field ? field.cardinality : undefined;
|
|
120
|
-
if (cardinality === 'many') {
|
|
121
|
-
// 1:many child table - derive columns, set component, config, rows
|
|
122
|
-
const resolved = { ...field };
|
|
123
|
-
// Auto-derive columns from child schema fields if not already provided
|
|
124
|
-
if (!('columns' in field) || !field.columns) {
|
|
125
|
-
resolved.columns = childSchema.map(childField => ({
|
|
126
|
-
name: childField.fieldname,
|
|
127
|
-
fieldname: childField.fieldname,
|
|
128
|
-
label: ('label' in childField && childField.label) || childField.fieldname,
|
|
129
|
-
fieldtype: 'fieldtype' in childField ? childField.fieldtype : 'Data',
|
|
130
|
-
align: ('align' in childField && childField.align) || 'left',
|
|
131
|
-
edit: 'edit' in childField ? childField.edit : true,
|
|
132
|
-
width: ('width' in childField && childField.width) || '20ch',
|
|
133
|
-
}));
|
|
134
|
-
}
|
|
135
|
-
// Set default component if not already specified
|
|
136
|
-
if (!resolved.component) {
|
|
137
|
-
resolved.component = 'ATable';
|
|
138
|
-
}
|
|
139
|
-
// Set default config if not already specified
|
|
140
|
-
if (!('config' in field) || !field.config) {
|
|
141
|
-
resolved.config = { view: 'list' };
|
|
142
|
-
}
|
|
143
|
-
// Initialize rows to empty array so componentProps fallback
|
|
144
|
-
// routes data from the form's dataModel[fieldname]
|
|
145
|
-
if (!('rows' in field) || !field.rows) {
|
|
146
|
-
resolved.rows = [];
|
|
147
|
-
}
|
|
148
|
-
return resolved;
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
// 1:1 nested form (default cardinality: 'one')
|
|
152
|
-
// Recurse into child schema to resolve deeply nested doctypes
|
|
153
|
-
seen.add(doctypeSlug);
|
|
154
|
-
const resolvedChild = this.resolveSchema(childSchema, seen);
|
|
155
|
-
seen.delete(doctypeSlug);
|
|
156
|
-
return { ...field, schema: resolvedChild };
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return { ...field };
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Initialize a new record with default values based on a schema.
|
|
165
|
-
*
|
|
166
|
-
* @remarks
|
|
167
|
-
* Creates a plain object with keys from the schema's fieldnames and default values
|
|
168
|
-
* derived from each field's `fieldtype`:
|
|
169
|
-
* - Data, Text → `''`
|
|
170
|
-
* - Check → `false`
|
|
171
|
-
* - Int, Float, Decimal, Currency, Quantity → `0`
|
|
172
|
-
* - JSON → `{}`
|
|
173
|
-
* - Doctype with `cardinality: 'many'` → `[]`
|
|
174
|
-
* - Doctype without `cardinality` or `cardinality: 'one'` → recursively initializes nested record
|
|
175
|
-
* - All others → `null`
|
|
176
|
-
*
|
|
177
|
-
* For Doctype fields with a resolved `schema` array (cardinality: 'one'), recursively
|
|
178
|
-
* initializes the nested record.
|
|
179
|
-
*
|
|
180
|
-
* @param schema - The schema array to derive defaults from
|
|
181
|
-
* @returns A plain object with default values for each field
|
|
182
|
-
*
|
|
183
|
-
* @example
|
|
184
|
-
* ```ts
|
|
185
|
-
* const defaults = registry.initializeRecord(addressSchema)
|
|
186
|
-
* // { street: '', city: '', state: '', zip_code: '' }
|
|
187
|
-
* ```
|
|
188
|
-
*
|
|
189
|
-
* @public
|
|
190
|
-
*/
|
|
191
|
-
initializeRecord(schema) {
|
|
192
|
-
const record = {};
|
|
193
|
-
schema.forEach(field => {
|
|
194
|
-
const fieldtype = 'fieldtype' in field ? field.fieldtype : 'Data';
|
|
195
|
-
switch (fieldtype) {
|
|
196
|
-
case 'Data':
|
|
197
|
-
case 'Text':
|
|
198
|
-
case 'Code':
|
|
199
|
-
record[field.fieldname] = '';
|
|
200
|
-
break;
|
|
201
|
-
case 'Check':
|
|
202
|
-
record[field.fieldname] = false;
|
|
203
|
-
break;
|
|
204
|
-
case 'Int':
|
|
205
|
-
case 'Float':
|
|
206
|
-
case 'Decimal':
|
|
207
|
-
case 'Currency':
|
|
208
|
-
case 'Quantity':
|
|
209
|
-
record[field.fieldname] = 0;
|
|
210
|
-
break;
|
|
211
|
-
case 'JSON':
|
|
212
|
-
record[field.fieldname] = {};
|
|
213
|
-
break;
|
|
214
|
-
case 'Doctype': {
|
|
215
|
-
// Check cardinality to determine initial value
|
|
216
|
-
const cardinality = 'cardinality' in field ? field.cardinality : undefined;
|
|
217
|
-
if (cardinality === 'many') {
|
|
218
|
-
// 1:many child table - initialize as empty array
|
|
219
|
-
record[field.fieldname] = [];
|
|
220
|
-
}
|
|
221
|
-
else if ('schema' in field && Array.isArray(field.schema)) {
|
|
222
|
-
// 1:1 nested form with resolved schema - recursively initialize
|
|
223
|
-
record[field.fieldname] = this.initializeRecord(field.schema);
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
// 1:1 without resolved schema - empty object
|
|
227
|
-
record[field.fieldname] = {};
|
|
228
|
-
}
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
default:
|
|
232
|
-
record[field.fieldname] = null;
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
return record;
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Get a registered doctype by slug
|
|
239
|
-
* @param slug - The doctype slug to look up
|
|
240
|
-
* @returns The Doctype instance if found, or undefined
|
|
241
|
-
* @public
|
|
242
|
-
*/
|
|
243
|
-
getDoctype(slug) {
|
|
244
|
-
return this.registry[slug];
|
|
245
|
-
}
|
|
246
|
-
}
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Schema Validation Utilities
|
|
3
|
-
* Validates Stonecrop schemas for integrity and consistency
|
|
4
|
-
* @packageDocumentation
|
|
5
|
-
*/
|
|
6
|
-
import { getGlobalTriggerEngine } from './field-triggers';
|
|
7
|
-
/**
|
|
8
|
-
* Validation severity levels
|
|
9
|
-
* @public
|
|
10
|
-
*/
|
|
11
|
-
export var ValidationSeverity;
|
|
12
|
-
(function (ValidationSeverity) {
|
|
13
|
-
/** Blocking error that prevents save */
|
|
14
|
-
ValidationSeverity["ERROR"] = "error";
|
|
15
|
-
/** Advisory warning that allows save */
|
|
16
|
-
ValidationSeverity["WARNING"] = "warning";
|
|
17
|
-
/** Informational message */
|
|
18
|
-
ValidationSeverity["INFO"] = "info";
|
|
19
|
-
})(ValidationSeverity || (ValidationSeverity = {}));
|
|
20
|
-
/**
|
|
21
|
-
* Schema validator class
|
|
22
|
-
* @public
|
|
23
|
-
*/
|
|
24
|
-
export class SchemaValidator {
|
|
25
|
-
options;
|
|
26
|
-
/**
|
|
27
|
-
* Creates a new SchemaValidator instance
|
|
28
|
-
* @param options - Validator configuration options
|
|
29
|
-
*/
|
|
30
|
-
constructor(options = {}) {
|
|
31
|
-
this.options = {
|
|
32
|
-
registry: options.registry || null,
|
|
33
|
-
validateLinkTargets: options.validateLinkTargets ?? true,
|
|
34
|
-
validateActions: options.validateActions ?? true,
|
|
35
|
-
validateWorkflows: options.validateWorkflows ?? true,
|
|
36
|
-
validateRequiredProperties: options.validateRequiredProperties ?? true,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Validates a complete doctype schema
|
|
41
|
-
* @param doctype - Doctype name
|
|
42
|
-
* @param schema - Schema fields (List or Array)
|
|
43
|
-
* @param workflow - Optional workflow configuration
|
|
44
|
-
* @param actions - Optional actions map
|
|
45
|
-
* @returns Validation result
|
|
46
|
-
*/
|
|
47
|
-
validate(doctype, schema, workflow, actions) {
|
|
48
|
-
const issues = [];
|
|
49
|
-
// Convert schema to array for easier iteration
|
|
50
|
-
const schemaArray = schema ? (Array.isArray(schema) ? schema : schema.toArray()) : [];
|
|
51
|
-
// Validate required properties
|
|
52
|
-
if (this.options.validateRequiredProperties) {
|
|
53
|
-
issues.push(...this.validateRequiredProperties(doctype, schemaArray));
|
|
54
|
-
}
|
|
55
|
-
// Validate Link field targets
|
|
56
|
-
if (this.options.validateLinkTargets && this.options.registry) {
|
|
57
|
-
issues.push(...this.validateLinkFields(doctype, schemaArray, this.options.registry));
|
|
58
|
-
}
|
|
59
|
-
// Validate workflow configuration
|
|
60
|
-
if (this.options.validateWorkflows && workflow) {
|
|
61
|
-
issues.push(...this.validateWorkflow(doctype, workflow));
|
|
62
|
-
}
|
|
63
|
-
// Validate action registration
|
|
64
|
-
if (this.options.validateActions && actions) {
|
|
65
|
-
const actionsMap = actions instanceof Map ? actions : actions.toObject();
|
|
66
|
-
issues.push(...this.validateActionRegistration(doctype, actionsMap));
|
|
67
|
-
}
|
|
68
|
-
// Calculate counts
|
|
69
|
-
const errorCount = issues.filter(i => i.severity === ValidationSeverity.ERROR).length;
|
|
70
|
-
const warningCount = issues.filter(i => i.severity === ValidationSeverity.WARNING).length;
|
|
71
|
-
const infoCount = issues.filter(i => i.severity === ValidationSeverity.INFO).length;
|
|
72
|
-
return {
|
|
73
|
-
valid: errorCount === 0,
|
|
74
|
-
issues,
|
|
75
|
-
errorCount,
|
|
76
|
-
warningCount,
|
|
77
|
-
infoCount,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Validates that required schema properties are present
|
|
82
|
-
* @internal
|
|
83
|
-
*/
|
|
84
|
-
validateRequiredProperties(doctype, schema) {
|
|
85
|
-
const issues = [];
|
|
86
|
-
for (const field of schema) {
|
|
87
|
-
// Check for fieldname
|
|
88
|
-
if (!field.fieldname) {
|
|
89
|
-
issues.push({
|
|
90
|
-
severity: ValidationSeverity.ERROR,
|
|
91
|
-
rule: 'required-fieldname',
|
|
92
|
-
message: 'Field is missing required property: fieldname',
|
|
93
|
-
doctype,
|
|
94
|
-
context: { field },
|
|
95
|
-
});
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
// Check for component or fieldtype
|
|
99
|
-
if (!field.component && !('fieldtype' in field)) {
|
|
100
|
-
issues.push({
|
|
101
|
-
severity: ValidationSeverity.ERROR,
|
|
102
|
-
rule: 'required-component-or-fieldtype',
|
|
103
|
-
message: `Field "${field.fieldname}" must have either component or fieldtype property`,
|
|
104
|
-
doctype,
|
|
105
|
-
fieldname: field.fieldname,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
// Validate nested schemas (recursively)
|
|
109
|
-
if ('schema' in field) {
|
|
110
|
-
const nestedSchema = field.schema;
|
|
111
|
-
const nestedArray = (Array.isArray(nestedSchema) ? nestedSchema : nestedSchema.toArray?.() || []);
|
|
112
|
-
issues.push(...this.validateRequiredProperties(doctype, nestedArray));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return issues;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Validates Link field targets exist in registry
|
|
119
|
-
* @internal
|
|
120
|
-
*/
|
|
121
|
-
validateLinkFields(doctype, schema, registry) {
|
|
122
|
-
const issues = [];
|
|
123
|
-
for (const field of schema) {
|
|
124
|
-
const fieldtype = 'fieldtype' in field ? field.fieldtype : undefined;
|
|
125
|
-
// Check Link fields
|
|
126
|
-
if (fieldtype === 'Link') {
|
|
127
|
-
const options = 'options' in field ? field.options : undefined;
|
|
128
|
-
if (!options) {
|
|
129
|
-
issues.push({
|
|
130
|
-
severity: ValidationSeverity.ERROR,
|
|
131
|
-
rule: 'link-missing-options',
|
|
132
|
-
message: `Link field "${field.fieldname}" is missing options property (target doctype)`,
|
|
133
|
-
doctype,
|
|
134
|
-
fieldname: field.fieldname,
|
|
135
|
-
});
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
// Check if target doctype exists in registry
|
|
139
|
-
// Options should be a string representing the target doctype name
|
|
140
|
-
const targetDoctype = typeof options === 'string' ? options : '';
|
|
141
|
-
if (!targetDoctype) {
|
|
142
|
-
issues.push({
|
|
143
|
-
severity: ValidationSeverity.ERROR,
|
|
144
|
-
rule: 'link-invalid-options',
|
|
145
|
-
message: `Link field "${field.fieldname}" has invalid options format (expected string doctype name)`,
|
|
146
|
-
doctype,
|
|
147
|
-
fieldname: field.fieldname,
|
|
148
|
-
});
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
const targetMeta = registry.registry[targetDoctype] || registry.registry[targetDoctype.toLowerCase()];
|
|
152
|
-
if (!targetMeta) {
|
|
153
|
-
issues.push({
|
|
154
|
-
severity: ValidationSeverity.ERROR,
|
|
155
|
-
rule: 'link-invalid-target',
|
|
156
|
-
message: `Link field "${field.fieldname}" references non-existent doctype: "${targetDoctype}"`,
|
|
157
|
-
doctype,
|
|
158
|
-
fieldname: field.fieldname,
|
|
159
|
-
context: { targetDoctype },
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
// Recursively check nested schemas
|
|
164
|
-
if ('schema' in field) {
|
|
165
|
-
const nestedSchema = field.schema;
|
|
166
|
-
const nestedArray = (Array.isArray(nestedSchema) ? nestedSchema : nestedSchema.toArray?.() || []);
|
|
167
|
-
issues.push(...this.validateLinkFields(doctype, nestedArray, registry));
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return issues;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Validates workflow state machine configuration
|
|
174
|
-
* @internal
|
|
175
|
-
*/
|
|
176
|
-
validateWorkflow(doctype, workflow) {
|
|
177
|
-
const issues = [];
|
|
178
|
-
// Check for initial state
|
|
179
|
-
if (!workflow.initial && !workflow.type) {
|
|
180
|
-
issues.push({
|
|
181
|
-
severity: ValidationSeverity.WARNING,
|
|
182
|
-
rule: 'workflow-missing-initial',
|
|
183
|
-
message: 'Workflow is missing initial state property',
|
|
184
|
-
doctype,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
// Check for states
|
|
188
|
-
if (!workflow.states || Object.keys(workflow.states).length === 0) {
|
|
189
|
-
issues.push({
|
|
190
|
-
severity: ValidationSeverity.WARNING,
|
|
191
|
-
rule: 'workflow-no-states',
|
|
192
|
-
message: 'Workflow has no states defined',
|
|
193
|
-
doctype,
|
|
194
|
-
});
|
|
195
|
-
return issues;
|
|
196
|
-
}
|
|
197
|
-
// Validate initial state exists
|
|
198
|
-
if (workflow.initial && typeof workflow.initial === 'string' && !workflow.states[workflow.initial]) {
|
|
199
|
-
issues.push({
|
|
200
|
-
severity: ValidationSeverity.ERROR,
|
|
201
|
-
rule: 'workflow-invalid-initial',
|
|
202
|
-
message: `Workflow initial state "${workflow.initial}" does not exist in states`,
|
|
203
|
-
doctype,
|
|
204
|
-
context: { initialState: workflow.initial },
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
// Check state reachability (simple check - all states should have at least one incoming transition or be initial)
|
|
208
|
-
const stateNames = Object.keys(workflow.states);
|
|
209
|
-
const reachableStates = new Set();
|
|
210
|
-
// Initial state is always reachable
|
|
211
|
-
if (workflow.initial && typeof workflow.initial === 'string') {
|
|
212
|
-
reachableStates.add(workflow.initial);
|
|
213
|
-
}
|
|
214
|
-
// Find all target states from transitions
|
|
215
|
-
for (const [_stateName, stateConfig] of Object.entries(workflow.states)) {
|
|
216
|
-
const state = stateConfig;
|
|
217
|
-
if (state.on) {
|
|
218
|
-
for (const [_event, transition] of Object.entries(state.on)) {
|
|
219
|
-
if (typeof transition === 'string') {
|
|
220
|
-
reachableStates.add(transition);
|
|
221
|
-
}
|
|
222
|
-
else if (transition && typeof transition === 'object') {
|
|
223
|
-
const target = 'target' in transition ? transition.target : undefined;
|
|
224
|
-
if (typeof target === 'string') {
|
|
225
|
-
reachableStates.add(target);
|
|
226
|
-
}
|
|
227
|
-
else if (Array.isArray(target)) {
|
|
228
|
-
target.forEach((t) => {
|
|
229
|
-
if (typeof t === 'string') {
|
|
230
|
-
reachableStates.add(t);
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
// Check for unreachable states
|
|
239
|
-
for (const stateName of stateNames) {
|
|
240
|
-
if (!reachableStates.has(stateName)) {
|
|
241
|
-
issues.push({
|
|
242
|
-
severity: ValidationSeverity.WARNING,
|
|
243
|
-
rule: 'workflow-unreachable-state',
|
|
244
|
-
message: `Workflow state "${stateName}" may not be reachable`,
|
|
245
|
-
doctype,
|
|
246
|
-
context: { stateName },
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return issues;
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Validates that actions are registered in the FieldTriggerEngine
|
|
254
|
-
* @internal
|
|
255
|
-
*/
|
|
256
|
-
validateActionRegistration(doctype, actions) {
|
|
257
|
-
const issues = [];
|
|
258
|
-
const triggerEngine = getGlobalTriggerEngine();
|
|
259
|
-
for (const [triggerName, actionNames] of Object.entries(actions)) {
|
|
260
|
-
if (!Array.isArray(actionNames)) {
|
|
261
|
-
issues.push({
|
|
262
|
-
severity: ValidationSeverity.ERROR,
|
|
263
|
-
rule: 'action-invalid-format',
|
|
264
|
-
message: `Action configuration for "${triggerName}" must be an array`,
|
|
265
|
-
doctype,
|
|
266
|
-
context: { triggerName, actionNames },
|
|
267
|
-
});
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
// Check each action name
|
|
271
|
-
for (const actionName of actionNames) {
|
|
272
|
-
// Check if action is registered globally
|
|
273
|
-
const engine = triggerEngine;
|
|
274
|
-
const isRegistered = engine.globalActions?.has(actionName) || engine.globalTransitionActions?.has(actionName);
|
|
275
|
-
if (!isRegistered) {
|
|
276
|
-
issues.push({
|
|
277
|
-
severity: ValidationSeverity.WARNING,
|
|
278
|
-
rule: 'action-not-registered',
|
|
279
|
-
message: `Action "${actionName}" referenced in "${triggerName}" is not registered in FieldTriggerEngine`,
|
|
280
|
-
doctype,
|
|
281
|
-
context: { triggerName, actionName },
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return issues;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Creates a validator with the given registry
|
|
291
|
-
* @param registry - Registry instance
|
|
292
|
-
* @param options - Additional validator options
|
|
293
|
-
* @returns SchemaValidator instance
|
|
294
|
-
* @public
|
|
295
|
-
*/
|
|
296
|
-
export function createValidator(registry, options) {
|
|
297
|
-
return new SchemaValidator({
|
|
298
|
-
registry,
|
|
299
|
-
...options,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Quick validation helper
|
|
304
|
-
* @param doctype - Doctype name
|
|
305
|
-
* @param schema - Schema fields
|
|
306
|
-
* @param registry - Registry instance
|
|
307
|
-
* @param workflow - Optional workflow configuration
|
|
308
|
-
* @param actions - Optional actions map
|
|
309
|
-
* @returns Validation result
|
|
310
|
-
* @public
|
|
311
|
-
*/
|
|
312
|
-
export function validateSchema(doctype, schema, registry, workflow, actions) {
|
|
313
|
-
const validator = createValidator(registry);
|
|
314
|
-
return validator.validate(doctype, schema, workflow, actions);
|
|
315
|
-
}
|