@stonecrop/stonecrop 0.12.8 → 0.13.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/README.md +0 -1
- package/dist/composable.js +1 -0
- package/dist/composables/lazy-link.js +125 -0
- package/dist/composables/operation-log.js +224 -0
- package/dist/composables/stonecrop.js +504 -0
- package/dist/composables/use-lazy-link-state.js +125 -0
- package/dist/composables/use-stonecrop.js +476 -0
- package/dist/doctype.js +242 -0
- package/dist/exceptions.js +16 -0
- package/dist/field-triggers.js +575 -0
- package/dist/index.js +27 -0
- package/dist/operation-log-DB-dGNT9.js +593 -0
- package/dist/operation-log-DB-dGNT9.js.map +1 -0
- package/dist/plugins/index.js +99 -0
- package/dist/registry.js +423 -0
- package/dist/schema-validator.js +407 -0
- 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/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/stores/data.d.ts +11 -0
- package/dist/src/stores/data.d.ts.map +1 -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/doctype.d.ts +0 -2
- package/dist/src/types/doctype.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.css +1 -0
- package/dist/stonecrop.d.ts +0 -2
- 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 +496 -0
- package/dist/stores/index.js +12 -0
- package/dist/stores/operation-log.js +580 -0
- 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/composable.js +0 -0
- package/dist/types/doctype.js +0 -0
- package/dist/types/field-triggers.js +4 -0
- package/dist/types/hst.js +0 -0
- package/dist/types/index.js +10 -0
- package/dist/types/operation-log.js +0 -0
- package/dist/types/plugin.js +0 -0
- package/dist/types/registry.js +0 -0
- package/dist/types/schema-validator.js +13 -0
- package/dist/types/stonecrop.js +0 -0
- package/dist/utils.js +46 -0
- package/package.json +4 -4
- package/src/types/doctype.ts +0 -2
package/dist/registry.js
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
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
|
+
*
|
|
20
|
+
* @defaultValue `{}`
|
|
21
|
+
* @see {@link Doctype}
|
|
22
|
+
*/
|
|
23
|
+
registry = {};
|
|
24
|
+
/**
|
|
25
|
+
* Reverse index: backlink fieldname → list of \{ doctype slug, link fieldname \}.
|
|
26
|
+
* Multiple doctypes can declare a link with the same backlink name, so each key
|
|
27
|
+
* maps to an array. Built at schema load time for O(1) ancestor lookups.
|
|
28
|
+
*
|
|
29
|
+
* @defaultValue `new Map()`
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
_ancestorIndex = new Map();
|
|
33
|
+
/**
|
|
34
|
+
* Whether the ancestor index needs rebuilding
|
|
35
|
+
*
|
|
36
|
+
* @defaultValue `true`
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
_ancestorIndexDirty = true;
|
|
40
|
+
/**
|
|
41
|
+
* The Vue router instance
|
|
42
|
+
* @see {@link https://router.vuejs.org/}
|
|
43
|
+
*/
|
|
44
|
+
router;
|
|
45
|
+
/**
|
|
46
|
+
* Creates a new Registry instance (singleton pattern)
|
|
47
|
+
* @param router - Optional Vue router instance for route management
|
|
48
|
+
* @param getMeta - Optional function to fetch doctype metadata from an API
|
|
49
|
+
*/
|
|
50
|
+
constructor(router, getMeta) {
|
|
51
|
+
if (Registry._root) {
|
|
52
|
+
return Registry._root;
|
|
53
|
+
}
|
|
54
|
+
Registry._root = this;
|
|
55
|
+
this.router = router;
|
|
56
|
+
this.getMeta = getMeta;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The getMeta function fetches doctype metadata from an API based on route context
|
|
60
|
+
* @see {@link Doctype}
|
|
61
|
+
*/
|
|
62
|
+
getMeta;
|
|
63
|
+
/**
|
|
64
|
+
* Get doctype metadata
|
|
65
|
+
* @param doctype - The doctype to fetch metadata for
|
|
66
|
+
* @returns The doctype metadata
|
|
67
|
+
* @see {@link Doctype}
|
|
68
|
+
*/
|
|
69
|
+
addDoctype(doctype) {
|
|
70
|
+
if (!(doctype.slug in this.registry)) {
|
|
71
|
+
this.registry[doctype.slug] = doctype;
|
|
72
|
+
this._ancestorIndexDirty = true;
|
|
73
|
+
}
|
|
74
|
+
// Register actions (including field triggers) with the field trigger engine
|
|
75
|
+
const triggerEngine = getGlobalTriggerEngine();
|
|
76
|
+
// Register under both doctype name and slug to handle different lookup patterns
|
|
77
|
+
triggerEngine.registerDoctypeActions(doctype.doctype, doctype.actions);
|
|
78
|
+
if (doctype.slug !== doctype.doctype) {
|
|
79
|
+
triggerEngine.registerDoctypeActions(doctype.slug, doctype.actions);
|
|
80
|
+
}
|
|
81
|
+
if (doctype.component && this.router && !this.router.hasRoute(doctype.doctype)) {
|
|
82
|
+
this.router.addRoute({
|
|
83
|
+
path: `/${doctype.slug}`,
|
|
84
|
+
name: doctype.slug,
|
|
85
|
+
component: doctype.component,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolve nested Doctype fields in a schema by embedding child schemas inline.
|
|
91
|
+
*
|
|
92
|
+
* Accepts a Doctype and extracts `fields` and `links` internally.
|
|
93
|
+
* Fields array contains both scalar fields and link fields (with fieldtype: 'Link').
|
|
94
|
+
* Render order is determined by the order of fields in the fields array.
|
|
95
|
+
*
|
|
96
|
+
* For each link field:
|
|
97
|
+
* - Looks up the corresponding link declaration in `links` by fieldname
|
|
98
|
+
* - `cardinality: 'noneOrMany'` or `'atLeastOne'`: auto-derives `columns` from the target's schema,
|
|
99
|
+
* sets `component` to `link.component ?? 'ATable'`, `config: { view: 'list' }`.
|
|
100
|
+
* - `cardinality: 'one'` or `'atMostOne'`: embeds the target schema as the entry's
|
|
101
|
+
* `schema` property, sets `component` to `link.component ?? 'AForm'`.
|
|
102
|
+
*
|
|
103
|
+
* Recurses for deeply nested doctypes. Circular references are protected against.
|
|
104
|
+
* Returns a new array — does not mutate the original.
|
|
105
|
+
*
|
|
106
|
+
* @param doctype - The doctype to resolve
|
|
107
|
+
* @param visited - Internal — set of already-visited doctype slugs for cycle detection
|
|
108
|
+
* @returns A new schema array with nested links resolved
|
|
109
|
+
*
|
|
110
|
+
* @public
|
|
111
|
+
*/
|
|
112
|
+
resolveSchema(doctype, visited) {
|
|
113
|
+
const seen = visited ?? new Set();
|
|
114
|
+
const slug = doctype.slug;
|
|
115
|
+
// Prevent circular resolution
|
|
116
|
+
if (seen.has(slug)) {
|
|
117
|
+
return doctype.schema ? (Array.isArray(doctype.schema) ? doctype.schema : Array.from(doctype.schema)) : [];
|
|
118
|
+
}
|
|
119
|
+
seen.add(slug);
|
|
120
|
+
// Convert schema to array
|
|
121
|
+
const schemaArray = doctype.schema
|
|
122
|
+
? Array.isArray(doctype.schema)
|
|
123
|
+
? doctype.schema
|
|
124
|
+
: Array.from(doctype.schema)
|
|
125
|
+
: [];
|
|
126
|
+
// Build a map of link declarations by fieldname for quick lookup
|
|
127
|
+
// Use the link's fieldname property if set, otherwise use the key
|
|
128
|
+
const linksByFieldname = new Map();
|
|
129
|
+
if (doctype.links) {
|
|
130
|
+
for (const [key, link] of Object.entries(doctype.links)) {
|
|
131
|
+
const linkFieldname = link.fieldname ?? key;
|
|
132
|
+
linksByFieldname.set(linkFieldname, link);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Process fields in order: scalar fields copied as-is, link fields resolved
|
|
136
|
+
const resolvedFields = [];
|
|
137
|
+
for (const field of schemaArray) {
|
|
138
|
+
// Check if this field is a link field (fieldtype: 'Link')
|
|
139
|
+
if ('fieldtype' in field && field.fieldtype === 'Link') {
|
|
140
|
+
const link = linksByFieldname.get(field.fieldname);
|
|
141
|
+
if (!link) {
|
|
142
|
+
// Link field without corresponding link declaration - copy as-is
|
|
143
|
+
resolvedFields.push({ ...field });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const targetDoctype = this.registry[link.target];
|
|
147
|
+
if (!targetDoctype) {
|
|
148
|
+
// Target not found - copy as-is
|
|
149
|
+
resolvedFields.push({ ...field });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const childSchema = this.resolveSchema(targetDoctype, seen);
|
|
153
|
+
// Extract properties consumed by resolution; preserve everything else
|
|
154
|
+
// TODO: options and cardinality are untyped runtime properties on link fields; add them to
|
|
155
|
+
// FormSchema (or a dedicated link field type) to remove this cast
|
|
156
|
+
const { fieldtype: _ft, options: _opt, cardinality: _card, ...fieldRest } = field;
|
|
157
|
+
if (link.cardinality === 'noneOrMany' || link.cardinality === 'atLeastOne') {
|
|
158
|
+
// Many relationship — build table config
|
|
159
|
+
resolvedFields.push(this.buildTableConfig({ ...fieldRest, label: fieldRest.label || field.fieldname }, childSchema, link.component));
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// One relationship — embed form schema
|
|
163
|
+
// TODO: remove assertion once resolved link output has a dedicated type separate from input schema
|
|
164
|
+
resolvedFields.push({
|
|
165
|
+
...fieldRest,
|
|
166
|
+
label: fieldRest.label || field.fieldname,
|
|
167
|
+
component: link.component || fieldRest.component || 'AForm',
|
|
168
|
+
schema: childSchema,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else if ('schema' in field && Array.isArray(field.schema)) {
|
|
173
|
+
// Fieldset — recursively resolve nested fields
|
|
174
|
+
const resolvedChildren = this.resolveFields(field.schema, linksByFieldname, seen);
|
|
175
|
+
resolvedFields.push({ ...field, schema: resolvedChildren });
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
// Scalar field — copy as-is
|
|
179
|
+
resolvedFields.push({ ...field });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
seen.delete(slug);
|
|
183
|
+
return resolvedFields;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Recursively resolve a flat fields array using the provided link context.
|
|
187
|
+
* Used by resolveSchema to handle fieldset children.
|
|
188
|
+
* @internal
|
|
189
|
+
*/
|
|
190
|
+
resolveFields(fields, links, visited) {
|
|
191
|
+
const resolved = [];
|
|
192
|
+
for (const field of fields) {
|
|
193
|
+
if ('fieldtype' in field && field.fieldtype === 'Link') {
|
|
194
|
+
const link = links.get(field.fieldname);
|
|
195
|
+
if (!link) {
|
|
196
|
+
resolved.push({ ...field });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const targetDoctype = this.registry[link.target];
|
|
200
|
+
if (!targetDoctype) {
|
|
201
|
+
resolved.push({ ...field });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const childSchema = this.resolveSchema(targetDoctype, new Set(visited));
|
|
205
|
+
const { fieldtype: _ft, options: _opt, cardinality: _card, ...fieldRest } = field;
|
|
206
|
+
if (link.cardinality === 'noneOrMany' || link.cardinality === 'atLeastOne') {
|
|
207
|
+
resolved.push(this.buildTableConfig({ ...fieldRest, label: fieldRest.label || field.fieldname }, childSchema, link.component));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// TODO: remove assertion once resolved link output has a dedicated type separate from input schema
|
|
211
|
+
resolved.push({
|
|
212
|
+
...fieldRest,
|
|
213
|
+
label: fieldRest.label || field.fieldname,
|
|
214
|
+
component: link.component || fieldRest.component || 'AForm',
|
|
215
|
+
schema: childSchema,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else if ('schema' in field && Array.isArray(field.schema)) {
|
|
220
|
+
resolved.push({ ...field, schema: this.resolveFields(field.schema, links, visited) });
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
resolved.push({ ...field });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return resolved;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Build an ATable configuration from a field and child schema.
|
|
230
|
+
* Data-model properties from the source field are preserved via the spread `field` argument.
|
|
231
|
+
* @internal
|
|
232
|
+
*/
|
|
233
|
+
buildTableConfig(field, childSchema, component) {
|
|
234
|
+
const resolved = {
|
|
235
|
+
...field,
|
|
236
|
+
fieldname: field.fieldname,
|
|
237
|
+
component: component || field.component || 'ATable',
|
|
238
|
+
columns: field.columns,
|
|
239
|
+
config: field.config,
|
|
240
|
+
};
|
|
241
|
+
if (!resolved.columns) {
|
|
242
|
+
resolved.columns = childSchema
|
|
243
|
+
.filter(childField => 'fieldtype' in childField)
|
|
244
|
+
.map(childField => ({
|
|
245
|
+
name: childField.fieldname,
|
|
246
|
+
label: ('label' in childField && childField.label) || childField.fieldname,
|
|
247
|
+
fieldtype: 'fieldtype' in childField ? childField.fieldtype : 'Data',
|
|
248
|
+
align: 'align' in childField ? childField.align : 'left',
|
|
249
|
+
edit: 'edit' in childField ? childField.edit : true,
|
|
250
|
+
width: ('width' in childField && childField.width) || '20ch',
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
if (!resolved.config) {
|
|
254
|
+
resolved.config = { view: 'list' };
|
|
255
|
+
}
|
|
256
|
+
return resolved;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Initialize a new record with default values based on a schema.
|
|
260
|
+
*
|
|
261
|
+
* @remarks
|
|
262
|
+
* Creates a plain object with keys from the schema's fieldnames and default values
|
|
263
|
+
* derived from each field's `fieldtype`:
|
|
264
|
+
* - Data, Text → `''`
|
|
265
|
+
* - Check → `false`
|
|
266
|
+
* - Int, Float, Decimal, Currency, Quantity → `0`
|
|
267
|
+
* - JSON → `{}`
|
|
268
|
+
* - Doctype with `cardinality: 'noneOrMany'` or `'atLeastOne'` → `[]`
|
|
269
|
+
* - Doctype without `cardinality` or `cardinality: 'one'` → recursively initializes nested record
|
|
270
|
+
* - All others → `null`
|
|
271
|
+
*
|
|
272
|
+
* For Doctype fields with a resolved `schema` array (cardinality: 'one'), recursively
|
|
273
|
+
* initializes the nested record.
|
|
274
|
+
*
|
|
275
|
+
* @param schema - The schema array to derive defaults from
|
|
276
|
+
* @returns A plain object with default values for each field
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* ```ts
|
|
280
|
+
* const defaults = registry.initializeRecord(addressSchema)
|
|
281
|
+
* // { street: '', city: '', state: '', zip_code: '' }
|
|
282
|
+
* ```
|
|
283
|
+
*
|
|
284
|
+
* @public
|
|
285
|
+
*/
|
|
286
|
+
initializeRecord(schema) {
|
|
287
|
+
const record = {};
|
|
288
|
+
schema.forEach(field => {
|
|
289
|
+
const fieldtype = 'fieldtype' in field ? field.fieldtype : 'Data';
|
|
290
|
+
const cardinality = 'cardinality' in field ? field.cardinality : undefined;
|
|
291
|
+
// 1:many — cardinality signals an array
|
|
292
|
+
if (cardinality === 'noneOrMany' || cardinality === 'atLeastOne') {
|
|
293
|
+
record[field.fieldname] = [];
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
// Resolved 1:many table entry — structural detection via columns
|
|
297
|
+
// TODO: replace 'columns' presence check with a type discriminant on SchemaTypes once one exists
|
|
298
|
+
if ('columns' in field) {
|
|
299
|
+
record[field.fieldname] = [];
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
// Resolved 1:1 link entry — has schema property (e.g., FieldsetSchema with nested schema)
|
|
303
|
+
if ('schema' in field && Array.isArray(field.schema)) {
|
|
304
|
+
record[field.fieldname] = this.initializeRecord(field.schema);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
switch (fieldtype) {
|
|
308
|
+
case 'Data':
|
|
309
|
+
case 'Text':
|
|
310
|
+
case 'Code':
|
|
311
|
+
record[field.fieldname] = '';
|
|
312
|
+
break;
|
|
313
|
+
case 'Check':
|
|
314
|
+
record[field.fieldname] = false;
|
|
315
|
+
break;
|
|
316
|
+
case 'Int':
|
|
317
|
+
case 'Float':
|
|
318
|
+
case 'Decimal':
|
|
319
|
+
case 'Currency':
|
|
320
|
+
case 'Quantity':
|
|
321
|
+
record[field.fieldname] = 0;
|
|
322
|
+
break;
|
|
323
|
+
case 'JSON':
|
|
324
|
+
record[field.fieldname] = {};
|
|
325
|
+
break;
|
|
326
|
+
default:
|
|
327
|
+
record[field.fieldname] = null;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
return record;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Get a registered doctype by slug
|
|
334
|
+
* @param slug - The doctype slug to look up
|
|
335
|
+
* @returns The Doctype instance if found, or undefined
|
|
336
|
+
* @public
|
|
337
|
+
*/
|
|
338
|
+
getDoctype(slug) {
|
|
339
|
+
return this.registry[slug];
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Get all links declared on a doctype.
|
|
343
|
+
*
|
|
344
|
+
* @param doctypeSlug - The doctype slug to get links for
|
|
345
|
+
* @returns Array of link declarations with fieldname, or empty array if none
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```ts
|
|
349
|
+
* const links = registry.getDescendantLinks('recipe')
|
|
350
|
+
* // [{ fieldname: 'tasks', target: 'recipe-task', cardinality: 'noneOrMany', backlink: 'recipe' }]
|
|
351
|
+
* ```
|
|
352
|
+
*
|
|
353
|
+
* @public
|
|
354
|
+
*/
|
|
355
|
+
getDescendantLinks(doctypeSlug) {
|
|
356
|
+
const doctype = this.registry[doctypeSlug];
|
|
357
|
+
if (!doctype?.links)
|
|
358
|
+
return [];
|
|
359
|
+
return Object.entries(doctype.links).map(([fieldname, link]) => ({
|
|
360
|
+
...link,
|
|
361
|
+
fieldname,
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Get links on other doctypes that target the given doctype.
|
|
366
|
+
*
|
|
367
|
+
* @param doctypeSlug - The doctype slug to find ancestor links for
|
|
368
|
+
* @returns Array of link declarations with fieldname and declaring doctype slug, or empty array
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* ```ts
|
|
372
|
+
* const ancestors = registry.getAncestorLinks('recipe-task')
|
|
373
|
+
* // [{ fieldname: 'tasks', target: 'recipe-task', cardinality: 'noneOrMany', backlink: 'recipe', doctype: 'recipe' }]
|
|
374
|
+
* ```
|
|
375
|
+
*
|
|
376
|
+
* @public
|
|
377
|
+
*/
|
|
378
|
+
getAncestorLinks(doctypeSlug) {
|
|
379
|
+
this._ensureAncestorIndex();
|
|
380
|
+
const results = [];
|
|
381
|
+
for (const [_backlink, entries] of this._ancestorIndex) {
|
|
382
|
+
for (const { slug: declaringSlug, fieldname } of entries) {
|
|
383
|
+
const declaringDoctype = this.registry[declaringSlug];
|
|
384
|
+
if (!declaringDoctype?.links)
|
|
385
|
+
continue;
|
|
386
|
+
const link = declaringDoctype.links[fieldname];
|
|
387
|
+
if (link?.target === doctypeSlug) {
|
|
388
|
+
results.push({
|
|
389
|
+
...link,
|
|
390
|
+
fieldname,
|
|
391
|
+
doctype: declaringSlug,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return results;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Ensure the ancestor index is up to date
|
|
400
|
+
* @internal
|
|
401
|
+
*/
|
|
402
|
+
_ensureAncestorIndex() {
|
|
403
|
+
if (!this._ancestorIndexDirty)
|
|
404
|
+
return;
|
|
405
|
+
this._ancestorIndexDirty = false;
|
|
406
|
+
this._ancestorIndex.clear();
|
|
407
|
+
for (const [slug, doctype] of Object.entries(this.registry)) {
|
|
408
|
+
if (!doctype.links)
|
|
409
|
+
continue;
|
|
410
|
+
for (const [fieldname, link] of Object.entries(doctype.links)) {
|
|
411
|
+
if (link.backlink) {
|
|
412
|
+
const existing = this._ancestorIndex.get(link.backlink);
|
|
413
|
+
if (existing) {
|
|
414
|
+
existing.push({ slug, fieldname });
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
this._ancestorIndex.set(link.backlink, [{ slug, fieldname }]);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|