@stonecrop/stonecrop 0.10.16 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -29
- package/dist/composable.js +1 -0
- package/dist/composables/lazy-link.js +125 -0
- package/dist/composables/stonecrop.js +123 -68
- package/dist/composables/use-lazy-link-state.js +125 -0
- package/dist/composables/use-stonecrop.js +476 -0
- package/dist/doctype.js +10 -2
- package/dist/field-triggers.js +15 -3
- package/dist/index.js +4 -3
- package/dist/operation-log-DB-dGNT9.js +593 -0
- package/dist/operation-log-DB-dGNT9.js.map +1 -0
- package/dist/registry.js +261 -101
- package/dist/schema-validator.js +105 -1
- package/dist/src/composable.d.ts +11 -0
- package/dist/src/composable.d.ts.map +1 -0
- package/dist/src/composable.js +477 -0
- package/dist/src/composables/lazy-link.d.ts +25 -0
- package/dist/src/composables/lazy-link.d.ts.map +1 -0
- package/dist/src/composables/operation-log.d.ts +5 -5
- package/dist/src/composables/operation-log.d.ts.map +1 -1
- package/dist/src/composables/operation-log.js +224 -0
- package/dist/src/composables/stonecrop.d.ts +11 -1
- package/dist/src/composables/stonecrop.d.ts.map +1 -1
- package/dist/src/composables/stonecrop.js +574 -0
- package/dist/src/composables/use-lazy-link-state.d.ts +25 -0
- package/dist/src/composables/use-lazy-link-state.d.ts.map +1 -0
- package/dist/src/composables/use-stonecrop.d.ts +93 -0
- package/dist/src/composables/use-stonecrop.d.ts.map +1 -0
- package/dist/src/composables/useNestedSchema.d.ts +110 -0
- package/dist/src/composables/useNestedSchema.d.ts.map +1 -0
- package/dist/src/composables/useNestedSchema.js +155 -0
- package/dist/src/doctype.d.ts +9 -1
- package/dist/src/doctype.d.ts.map +1 -1
- package/dist/src/doctype.js +234 -0
- package/dist/src/exceptions.js +16 -0
- package/dist/src/field-triggers.d.ts +6 -0
- package/dist/src/field-triggers.d.ts.map +1 -1
- package/dist/src/field-triggers.js +567 -0
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +23 -0
- package/dist/src/plugins/index.js +96 -0
- package/dist/src/registry.d.ts +102 -23
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +246 -0
- package/dist/src/schema-validator.d.ts +8 -1
- package/dist/src/schema-validator.d.ts.map +1 -1
- package/dist/src/schema-validator.js +315 -0
- package/dist/src/stonecrop.d.ts +73 -28
- package/dist/src/stonecrop.d.ts.map +1 -1
- package/dist/src/stonecrop.js +339 -0
- package/dist/src/stores/data.d.ts +11 -0
- package/dist/src/stores/data.d.ts.map +1 -0
- package/dist/src/stores/hst.d.ts +5 -75
- package/dist/src/stores/hst.d.ts.map +1 -1
- package/dist/src/stores/hst.js +495 -0
- package/dist/src/stores/index.js +12 -0
- package/dist/src/stores/operation-log.d.ts +14 -14
- package/dist/src/stores/operation-log.d.ts.map +1 -1
- package/dist/src/stores/operation-log.js +568 -0
- package/dist/src/stores/xstate.d.ts +31 -0
- package/dist/src/stores/xstate.d.ts.map +1 -0
- package/dist/src/tsdoc-metadata.json +11 -0
- package/dist/src/types/composable.d.ts +50 -12
- package/dist/src/types/composable.d.ts.map +1 -1
- package/dist/src/types/doctype.d.ts +6 -7
- package/dist/src/types/doctype.d.ts.map +1 -1
- package/dist/src/types/field-triggers.d.ts +1 -1
- package/dist/src/types/field-triggers.d.ts.map +1 -1
- package/dist/src/types/field-triggers.js +4 -0
- package/dist/src/types/hst.d.ts +70 -0
- package/dist/src/types/hst.d.ts.map +1 -0
- package/dist/src/types/index.d.ts +1 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/types/index.js +4 -0
- package/dist/src/types/operation-log.d.ts +4 -4
- package/dist/src/types/operation-log.d.ts.map +1 -1
- package/dist/src/types/operation-log.js +0 -0
- package/dist/src/types/registry.js +0 -0
- package/dist/src/types/schema-validator.d.ts +2 -0
- package/dist/src/types/schema-validator.d.ts.map +1 -1
- package/dist/src/utils.d.ts +24 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/stonecrop.d.ts +317 -99
- package/dist/stonecrop.js +2191 -1897
- package/dist/stonecrop.js.map +1 -1
- package/dist/stonecrop.umd.cjs +6 -0
- package/dist/stonecrop.umd.cjs.map +1 -0
- package/dist/stores/data.js +7 -0
- package/dist/stores/hst.js +27 -25
- package/dist/stores/operation-log.js +59 -47
- package/dist/stores/xstate.js +29 -0
- package/dist/tests/setup.d.ts +5 -0
- package/dist/tests/setup.d.ts.map +1 -0
- package/dist/tests/setup.js +15 -0
- package/dist/types/hst.js +0 -0
- package/dist/types/index.js +1 -0
- package/dist/utils.js +46 -0
- package/package.json +5 -5
- package/src/composables/lazy-link.ts +146 -0
- package/src/composables/operation-log.ts +1 -1
- package/src/composables/stonecrop.ts +142 -73
- package/src/doctype.ts +13 -4
- package/src/field-triggers.ts +18 -4
- package/src/index.ts +4 -2
- package/src/registry.ts +289 -111
- package/src/schema-validator.ts +120 -1
- package/src/stonecrop.ts +230 -106
- package/src/stores/hst.ts +29 -104
- package/src/stores/operation-log.ts +64 -50
- package/src/types/composable.ts +55 -12
- package/src/types/doctype.ts +6 -7
- package/src/types/field-triggers.ts +1 -1
- package/src/types/hst.ts +77 -0
- package/src/types/index.ts +1 -0
- package/src/types/operation-log.ts +4 -4
- package/src/types/schema-validator.ts +2 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { nextTick } from 'vue';
|
|
2
|
+
import Registry from '../registry';
|
|
3
|
+
import { Stonecrop } from '../stonecrop';
|
|
4
|
+
import { useOperationLogStore } from '../stores/operation-log';
|
|
5
|
+
/**
|
|
6
|
+
* Setup auto-initialization for user-defined initialization logic
|
|
7
|
+
* This function handles the post-mount initialization automatically
|
|
8
|
+
*/
|
|
9
|
+
async function setupAutoInitialization(registry, stonecrop, onRouterInitialized) {
|
|
10
|
+
// Wait for the next tick to ensure the app is mounted
|
|
11
|
+
await nextTick();
|
|
12
|
+
try {
|
|
13
|
+
await onRouterInitialized(registry, stonecrop);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Silent error handling - application should handle initialization errors
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Stonecrop Vue plugin
|
|
21
|
+
* @param app - The Vue app instance
|
|
22
|
+
* @param options - The plugin options
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { createApp } from 'vue'
|
|
26
|
+
* import Stonecrop from '@stonecrop/stonecrop'
|
|
27
|
+
* import { StonecropClient } from '@stonecrop/graphql-client'
|
|
28
|
+
* import router from './router'
|
|
29
|
+
*
|
|
30
|
+
* const client = new StonecropClient({ endpoint: '/graphql' })
|
|
31
|
+
*
|
|
32
|
+
* const app = createApp(App)
|
|
33
|
+
* app.use(Stonecrop, {
|
|
34
|
+
* router,
|
|
35
|
+
* client,
|
|
36
|
+
* getMeta: async (routeContext) => {
|
|
37
|
+
* // routeContext contains: { path, segments }
|
|
38
|
+
* // use the client to fetch doctype meta
|
|
39
|
+
* return client.getMeta({ doctype: routeContext.segments[0] })
|
|
40
|
+
* },
|
|
41
|
+
* autoInitializeRouter: true,
|
|
42
|
+
* onRouterInitialized: async (registry, stonecrop) => {
|
|
43
|
+
* // your custom initialization logic here
|
|
44
|
+
* }
|
|
45
|
+
* })
|
|
46
|
+
* app.mount('#app')
|
|
47
|
+
* ```
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
const plugin = {
|
|
51
|
+
install: (app, options) => {
|
|
52
|
+
// Check for existing router installation
|
|
53
|
+
const existingRouter = app.config.globalProperties.$router;
|
|
54
|
+
const providedRouter = options?.router;
|
|
55
|
+
const router = existingRouter || providedRouter;
|
|
56
|
+
if (!existingRouter && providedRouter) {
|
|
57
|
+
app.use(providedRouter);
|
|
58
|
+
}
|
|
59
|
+
// Create registry with available router
|
|
60
|
+
const registry = new Registry(router, options?.getMeta);
|
|
61
|
+
app.provide('$registry', registry);
|
|
62
|
+
app.config.globalProperties.$registry = registry;
|
|
63
|
+
// Create and provide a global Stonecrop instance
|
|
64
|
+
const stonecrop = new Stonecrop(registry, undefined, options?.client ? { client: options.client } : undefined);
|
|
65
|
+
app.provide('$stonecrop', stonecrop);
|
|
66
|
+
app.config.globalProperties.$stonecrop = stonecrop;
|
|
67
|
+
// Initialize operation log store if Pinia is available
|
|
68
|
+
// This ensures the store is created with the app's Pinia instance
|
|
69
|
+
try {
|
|
70
|
+
const pinia = app.config.globalProperties.$pinia;
|
|
71
|
+
if (pinia) {
|
|
72
|
+
// Initialize the operation log store with the app's Pinia instance
|
|
73
|
+
const operationLogStore = useOperationLogStore(pinia);
|
|
74
|
+
// Provide the store so components can access it
|
|
75
|
+
app.provide('$operationLogStore', operationLogStore);
|
|
76
|
+
app.config.globalProperties.$operationLogStore = operationLogStore;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
// Pinia not available - operation log won't work, but app should still function
|
|
81
|
+
// eslint-disable-next-line no-console
|
|
82
|
+
console.warn('Pinia not available - operation log features will be disabled:', error);
|
|
83
|
+
}
|
|
84
|
+
// Register custom components
|
|
85
|
+
if (options?.components) {
|
|
86
|
+
for (const [tag, component] of Object.entries(options.components)) {
|
|
87
|
+
app.component(tag, component);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Setup auto-initialization if requested
|
|
91
|
+
if (options?.autoInitializeRouter && options.onRouterInitialized) {
|
|
92
|
+
void setupAutoInitialization(registry, stonecrop, options.onRouterInitialized);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
export default plugin;
|
package/dist/src/registry.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SchemaTypes } from '@stonecrop/aform';
|
|
2
|
+
import type { DoctypeMeta, LinkDeclaration } from '@stonecrop/schema';
|
|
2
3
|
import { Router } from 'vue-router';
|
|
3
4
|
import Doctype from './doctype';
|
|
4
5
|
import { RouteContext } from './types/registry';
|
|
@@ -19,9 +20,27 @@ export default class Registry {
|
|
|
19
20
|
readonly name: string;
|
|
20
21
|
/**
|
|
21
22
|
* The registry property contains a collection of doctypes
|
|
23
|
+
*
|
|
24
|
+
* @defaultValue `{}`
|
|
22
25
|
* @see {@link Doctype}
|
|
23
26
|
*/
|
|
24
27
|
readonly registry: Record<string, Doctype>;
|
|
28
|
+
/**
|
|
29
|
+
* Reverse index: backlink fieldname → list of \{ doctype slug, link fieldname \}.
|
|
30
|
+
* Multiple doctypes can declare a link with the same backlink name, so each key
|
|
31
|
+
* maps to an array. Built at schema load time for O(1) ancestor lookups.
|
|
32
|
+
*
|
|
33
|
+
* @defaultValue `new Map()`
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
private _ancestorIndex;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the ancestor index needs rebuilding
|
|
39
|
+
*
|
|
40
|
+
* @defaultValue `true`
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
private _ancestorIndexDirty;
|
|
25
44
|
/**
|
|
26
45
|
* The Vue router instance
|
|
27
46
|
* @see {@link https://router.vuejs.org/}
|
|
@@ -48,35 +67,32 @@ export default class Registry {
|
|
|
48
67
|
/**
|
|
49
68
|
* Resolve nested Doctype fields in a schema by embedding child schemas inline.
|
|
50
69
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
70
|
+
* Accepts a Doctype and extracts `fields` and `links` internally.
|
|
71
|
+
* Fields array contains both scalar fields and link fields (with fieldtype: 'Link').
|
|
72
|
+
* Render order is determined by the order of fields in the fields array.
|
|
54
73
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* -
|
|
58
|
-
* `
|
|
74
|
+
* For each link field:
|
|
75
|
+
* - Looks up the corresponding link declaration in `links` by fieldname
|
|
76
|
+
* - `cardinality: 'noneOrMany'` or `'atLeastOne'`: auto-derives `columns` from the target's schema,
|
|
77
|
+
* sets `component` to `link.component ?? 'ATable'`, `config: { view: 'list' }`, `rows: []`.
|
|
78
|
+
* - `cardinality: 'one'` or `'atMostOne'`: embeds the target schema as the entry's
|
|
79
|
+
* `schema` property, sets `component` to `link.component ?? 'AForm'`.
|
|
59
80
|
*
|
|
60
81
|
* Recurses for deeply nested doctypes. Circular references are protected against.
|
|
82
|
+
* Returns a new array — does not mutate the original.
|
|
61
83
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* @
|
|
65
|
-
* @returns A new schema array with nested Doctype fields resolved
|
|
66
|
-
*
|
|
67
|
-
* @example
|
|
68
|
-
* ```ts
|
|
69
|
-
* registry.addDoctype(addressDoctype)
|
|
70
|
-
* registry.addDoctype(customerDoctype)
|
|
71
|
-
*
|
|
72
|
-
* // Before: customer schema has { fieldname: 'address', fieldtype: 'Doctype', options: 'address' }
|
|
73
|
-
* const resolved = registry.resolveSchema(customerSchema)
|
|
74
|
-
* // After: address field now has schema: [...address fields...]
|
|
75
|
-
* ```
|
|
84
|
+
* @param doctype - The doctype to resolve
|
|
85
|
+
* @param visited - Internal — set of already-visited doctype slugs for cycle detection
|
|
86
|
+
* @returns A new schema array with nested links resolved
|
|
76
87
|
*
|
|
77
88
|
* @public
|
|
78
89
|
*/
|
|
79
|
-
resolveSchema(
|
|
90
|
+
resolveSchema(doctype: Doctype, visited?: Set<string>): SchemaTypes[];
|
|
91
|
+
/**
|
|
92
|
+
* Build an ATable configuration from a field and child schema
|
|
93
|
+
* @internal
|
|
94
|
+
*/
|
|
95
|
+
private buildTableConfig;
|
|
80
96
|
/**
|
|
81
97
|
* Initialize a new record with default values based on a schema.
|
|
82
98
|
*
|
|
@@ -87,7 +103,7 @@ export default class Registry {
|
|
|
87
103
|
* - Check → `false`
|
|
88
104
|
* - Int, Float, Decimal, Currency, Quantity → `0`
|
|
89
105
|
* - JSON → `{}`
|
|
90
|
-
* - Doctype with `cardinality: '
|
|
106
|
+
* - Doctype with `cardinality: 'noneOrMany'` or `'atLeastOne'` → `[]`
|
|
91
107
|
* - Doctype without `cardinality` or `cardinality: 'one'` → recursively initializes nested record
|
|
92
108
|
* - All others → `null`
|
|
93
109
|
*
|
|
@@ -113,5 +129,68 @@ export default class Registry {
|
|
|
113
129
|
* @public
|
|
114
130
|
*/
|
|
115
131
|
getDoctype(slug: string): Doctype | undefined;
|
|
132
|
+
/**
|
|
133
|
+
* Get all links declared on a doctype.
|
|
134
|
+
*
|
|
135
|
+
* @param doctypeSlug - The doctype slug to get links for
|
|
136
|
+
* @returns Array of link declarations with fieldname, or empty array if none
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* const links = registry.getDescendantLinks('recipe')
|
|
141
|
+
* // [{ fieldname: 'tasks', target: 'recipe-task', cardinality: 'noneOrMany', backlink: 'recipe' }]
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* @public
|
|
145
|
+
*/
|
|
146
|
+
getDescendantLinks(doctypeSlug: string): Array<LinkDeclaration & {
|
|
147
|
+
fieldname: string;
|
|
148
|
+
}>;
|
|
149
|
+
/**
|
|
150
|
+
* Get links on other doctypes that target the given doctype.
|
|
151
|
+
*
|
|
152
|
+
* @param doctypeSlug - The doctype slug to find ancestor links for
|
|
153
|
+
* @returns Array of link declarations with fieldname and declaring doctype slug, or empty array
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* const ancestors = registry.getAncestorLinks('recipe-task')
|
|
158
|
+
* // [{ fieldname: 'tasks', target: 'recipe-task', cardinality: 'noneOrMany', backlink: 'recipe', doctype: 'recipe' }]
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* @public
|
|
162
|
+
*/
|
|
163
|
+
getAncestorLinks(doctypeSlug: string): Array<LinkDeclaration & {
|
|
164
|
+
fieldname: string;
|
|
165
|
+
doctype: string;
|
|
166
|
+
}>;
|
|
167
|
+
/**
|
|
168
|
+
* Ensure the ancestor index is up to date
|
|
169
|
+
* @internal
|
|
170
|
+
*/
|
|
171
|
+
private _ensureAncestorIndex;
|
|
172
|
+
/**
|
|
173
|
+
* Convert the registry to a Map of DoctypeMeta objects for use with StonecropClient.
|
|
174
|
+
*
|
|
175
|
+
* This allows passing a Registry instance to StonecropClient by deriving the
|
|
176
|
+
* Map\<string, DoctypeMeta\> that StonecropClient needs for building nested GraphQL queries.
|
|
177
|
+
*
|
|
178
|
+
* @returns Map of doctype metadata keyed by doctype name
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* const registry = new Registry()
|
|
183
|
+
* registry.addDoctype(Doctype.fromObject(customerSchema))
|
|
184
|
+
* registry.addDoctype(Doctype.fromObject(orderSchema))
|
|
185
|
+
*
|
|
186
|
+
* const client = new StonecropClient({
|
|
187
|
+
* endpoint: '/graphql',
|
|
188
|
+
* registry: registry.toMetaMap(), // Convert once, use with client
|
|
189
|
+
* })
|
|
190
|
+
* ```
|
|
191
|
+
*
|
|
192
|
+
* @public
|
|
193
|
+
*/
|
|
194
|
+
toMetaMap(): Map<string, DoctypeMeta>;
|
|
116
195
|
}
|
|
117
196
|
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAe,MAAM,kBAAkB,CAAA;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACrE,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAEnC,OAAO,OAAO,MAAM,WAAW,CAAA;AAE/B,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAE/C;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,QAAQ;IAC5B;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAA;IAEtB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAa;IAElC;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAK;IAE/C;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc,CAAqE;IAE3F;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB,CAAgB;IAE3C;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IAExB;;;;OAIG;gBACS,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,YAAY,EAAE,YAAY,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IASjG;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,YAAY,EAAE,YAAY,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAEpE;;;;;OAKG;IACH,UAAU,CAAC,OAAO,EAAE,OAAO;IAuB3B;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,WAAW,EAAE;IA6ErE;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAiCxB;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAoD5D;;;;;OAKG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAI7C;;;;;;;;;;;;;OAaG;IACH,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,KAAK,CAAC,eAAe,GAAG;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAUvF;;;;;;;;;;;;;OAaG;IACH,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,KAAK,CAAC,eAAe,GAAG;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAwBtG;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAoB5B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,SAAS,IAAI,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC;CA4BrC"}
|
|
@@ -0,0 +1,246 @@
|
|
|
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
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
6
|
import type { SchemaTypes } from '@stonecrop/aform';
|
|
7
|
+
import type { LinkDeclaration } from '@stonecrop/schema';
|
|
7
8
|
import type { List, Map as ImmutableMap } from 'immutable';
|
|
8
9
|
import type { AnyStateNodeConfig } from 'xstate';
|
|
9
10
|
import type Registry from './registry';
|
|
@@ -25,9 +26,10 @@ export declare class SchemaValidator {
|
|
|
25
26
|
* @param schema - Schema fields (List or Array)
|
|
26
27
|
* @param workflow - Optional workflow configuration
|
|
27
28
|
* @param actions - Optional actions map
|
|
29
|
+
* @param links - Optional links object
|
|
28
30
|
* @returns Validation result
|
|
29
31
|
*/
|
|
30
|
-
validate(doctype: string, schema: List<SchemaTypes> | SchemaTypes[] | undefined, workflow?: AnyStateNodeConfig, actions?: ImmutableMap<string, string[]> | Map<string, string[]>): ValidationResult;
|
|
32
|
+
validate(doctype: string, schema: List<SchemaTypes> | SchemaTypes[] | undefined, workflow?: AnyStateNodeConfig, actions?: ImmutableMap<string, string[]> | Map<string, string[]>, links?: Record<string, LinkDeclaration>): ValidationResult;
|
|
31
33
|
/**
|
|
32
34
|
* Validates that required schema properties are present
|
|
33
35
|
* @internal
|
|
@@ -38,6 +40,11 @@ export declare class SchemaValidator {
|
|
|
38
40
|
* @internal
|
|
39
41
|
*/
|
|
40
42
|
private validateLinkFields;
|
|
43
|
+
/**
|
|
44
|
+
* Validates link declarations: target resolution, backlink consistency, Link field correspondence
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
private validateLinkDeclarations;
|
|
41
48
|
/**
|
|
42
49
|
* Validates workflow state machine configuration
|
|
43
50
|
* @internal
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-validator.d.ts","sourceRoot":"","sources":["../../src/schema-validator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,YAAY,EAAE,MAAM,WAAW,CAAA;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,QAAQ,CAAA;AAGhD,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAA;AAEtC,OAAO,KAAK,EAAmB,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAEnG;;;GAGG;AACH,qBAAa,eAAe;IAC3B,OAAO,CAAC,OAAO,CAA4B;IAE3C;;;OAGG;gBACS,OAAO,GAAE,gBAAqB;
|
|
1
|
+
{"version":3,"file":"schema-validator.d.ts","sourceRoot":"","sources":["../../src/schema-validator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,YAAY,EAAE,MAAM,WAAW,CAAA;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,QAAQ,CAAA;AAGhD,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAA;AAEtC,OAAO,KAAK,EAAmB,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAEnG;;;GAGG;AACH,qBAAa,eAAe;IAC3B,OAAO,CAAC,OAAO,CAA4B;IAE3C;;;OAGG;gBACS,OAAO,GAAE,gBAAqB;IAW1C;;;;;;;;OAQG;IACH,QAAQ,CACP,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,GAAG,WAAW,EAAE,GAAG,SAAS,EACrD,QAAQ,CAAC,EAAE,kBAAkB,EAC7B,OAAO,CAAC,EAAE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAChE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GACrC,gBAAgB;IA8CnB;;;OAGG;IACH,OAAO,CAAC,0BAA0B;IAwClC;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IA4D1B;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IA0GhC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAmFxB;;;OAGG;IACH,OAAO,CAAC,0BAA0B;CAuClC;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,eAAe,CAKxG;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC7B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,GAAG,WAAW,EAAE,GAAG,SAAS,EACrD,QAAQ,EAAE,QAAQ,EAClB,QAAQ,CAAC,EAAE,kBAAkB,EAC7B,OAAO,CAAC,EAAE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAC9D,gBAAgB,CAGlB"}
|