@jacebenson/jsn 4.3.13-dev.81.1 → 4.3.13-dev.82.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 +1 -1
- package/src/commands/forms.js +22 -97
- package/src/metadata/records.js +245 -0
package/package.json
CHANGED
package/src/commands/forms.js
CHANGED
|
@@ -1,105 +1,29 @@
|
|
|
1
1
|
import { getStringField, interactiveList } from '../helpers.js';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
if (!record || typeof record !== 'object') return '';
|
|
5
|
-
const val = record[key];
|
|
6
|
-
if (val == null) return '';
|
|
7
|
-
if (typeof val === 'string') return val;
|
|
8
|
-
if (typeof val === 'object') {
|
|
9
|
-
if (val.display_value != null && val.display_value !== '') return String(val.display_value);
|
|
10
|
-
if (val.value != null) return String(val.value);
|
|
11
|
-
}
|
|
12
|
-
return String(val);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function getIntValue(record, key) {
|
|
16
|
-
if (!record || typeof record !== 'object') return 0;
|
|
17
|
-
const val = record[key];
|
|
18
|
-
if (val == null) return 0;
|
|
19
|
-
if (typeof val === 'number') return Math.floor(val);
|
|
20
|
-
if (typeof val === 'string') { const n = parseInt(val, 10); return isNaN(n) ? 0 : n; }
|
|
21
|
-
if (typeof val === 'object' && val.value != null) { const n = parseInt(val.value, 10); return isNaN(n) ? 0 : n; }
|
|
22
|
-
return 0;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function showForm(app, table, viewName) {
|
|
26
|
-
// 1) Look up the view
|
|
27
|
-
const viewParams = new URLSearchParams();
|
|
28
|
-
viewParams.set('sysparm_limit', '1');
|
|
29
|
-
viewParams.set('sysparm_fields', 'sys_id');
|
|
30
|
-
viewParams.set('sysparm_display_value', 'all');
|
|
31
|
-
viewParams.set('sysparm_query', `name=${viewName}`);
|
|
32
|
-
let viewSysID = '';
|
|
33
|
-
try { const vr = await app.sdk.list('sys_ui_view', viewParams); if (vr.length > 0) viewSysID = getStringField(vr[0], 'sys_id'); } catch { /* ignore */ }
|
|
34
|
-
if (!viewSysID) {
|
|
35
|
-
viewParams.set('sysparm_query', `title=${viewName}`);
|
|
36
|
-
try { const vr = await app.sdk.list('sys_ui_view', viewParams); if (vr.length > 0) viewSysID = getStringField(vr[0], 'sys_id'); } catch { /* ignore */ }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// 2) Get sections for this table + view
|
|
40
|
-
const secParams = new URLSearchParams();
|
|
41
|
-
secParams.set('sysparm_limit', '200');
|
|
42
|
-
secParams.set('sysparm_fields', 'sys_id,caption');
|
|
43
|
-
secParams.set('sysparm_display_value', 'all');
|
|
44
|
-
secParams.set('sysparm_query', `name=${table}^view=${viewName}^ORDERBYposition`);
|
|
45
|
-
if (viewSysID) {
|
|
46
|
-
secParams.set('sysparm_query', `name=${table}^view=${viewSysID}^ORDERBYposition`);
|
|
47
|
-
}
|
|
48
|
-
const sections = await app.sdk.list('sys_ui_section', secParams);
|
|
49
|
-
|
|
50
|
-
// 3) Get elements for each section
|
|
51
|
-
const sectionsOut = [];
|
|
52
|
-
for (const sec of sections) {
|
|
53
|
-
const secSysID = getStringField(sec, 'sys_id');
|
|
54
|
-
const sectionCaption = getDisplayValue(sec, 'caption') || '(unnamed)';
|
|
3
|
+
import { createRecordMetadataReader } from '../metadata/records.js';
|
|
55
4
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
sectionsOut.push({
|
|
67
|
-
caption: sectionCaption,
|
|
68
|
-
position: getIntValue(sec, 'position'),
|
|
69
|
-
elements: elements
|
|
70
|
-
.map(e => ({
|
|
71
|
-
type: getDisplayValue(e, 'type'),
|
|
72
|
-
label: getDisplayValue(e, 'label'),
|
|
73
|
-
element: getDisplayValue(e, 'element'),
|
|
74
|
-
position: getIntValue(e, 'position'),
|
|
75
|
-
}))
|
|
76
|
-
.sort((a, b) => a.position - b.position),
|
|
77
|
-
});
|
|
5
|
+
export function formatFormMetadata(metadata) {
|
|
6
|
+
const lines = [`Form: ${metadata.table} → ${metadata.requested_view}`];
|
|
7
|
+
if (metadata.form?.inherited) lines.push(` Inherited layout: ${metadata.form.table}`);
|
|
8
|
+
if (!metadata.form) lines.push(' No unambiguous shared form layout is visible.');
|
|
9
|
+
for (const section of metadata.form?.sections || []) {
|
|
10
|
+
lines.push('', `── ${section.caption || '(unnamed)'} ──`);
|
|
11
|
+
for (const element of section.elements) lines.push(` ${element.label || element.element || '(unnamed)'}${element.type ? ` (${element.type})` : ''}`);
|
|
12
|
+
if (!section.elements.length) lines.push(' (no visible elements)');
|
|
78
13
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const lines = [];
|
|
84
|
-
lines.push(`Form: ${table} → ${viewName}`);
|
|
85
|
-
if (viewSysID) lines.push(` View: ${app.getEffectiveInstance()}/nav_to.do?uri=sys_ui_view.do?sys_id=${viewSysID}`);
|
|
86
|
-
lines.push('');
|
|
87
|
-
|
|
88
|
-
for (const sec of sectionsOut) {
|
|
89
|
-
const secLabel = sec.caption || '(unnamed)';
|
|
90
|
-
lines.push(`── ${secLabel} ──`);
|
|
91
|
-
if (sec.elements.length === 0) {
|
|
92
|
-
lines.push(' (no elements)');
|
|
93
|
-
}
|
|
94
|
-
for (const el of sec.elements) {
|
|
95
|
-
const elLabel = el.label || el.element || '(unnamed)';
|
|
96
|
-
const typeStr = el.type ? ` (${el.type})` : '';
|
|
97
|
-
lines.push(` ${elLabel}${typeStr}`);
|
|
98
|
-
}
|
|
99
|
-
lines.push('');
|
|
14
|
+
const configured = metadata.related_lists.filter(item => item.configured);
|
|
15
|
+
if (configured.length) {
|
|
16
|
+
lines.push('', 'Configured related lists:');
|
|
17
|
+
for (const list of configured) lines.push(` ${list.label || list.id} (${list.query_status})`);
|
|
100
18
|
}
|
|
19
|
+
lines.push('', metadata.coverage.scope);
|
|
20
|
+
for (const warning of metadata.coverage.warnings) lines.push(` ${warning.source}: ${warning.reason}`);
|
|
21
|
+
return lines.join('\n');
|
|
22
|
+
}
|
|
101
23
|
|
|
102
|
-
|
|
24
|
+
async function showForm(app, table, view) {
|
|
25
|
+
const metadata = await createRecordMetadataReader(app.sdk).read({ table, view });
|
|
26
|
+
app.ok({ ...metadata, _formatted: formatFormMetadata(metadata) }, { summary: `Form metadata for ${table}` });
|
|
103
27
|
}
|
|
104
28
|
|
|
105
29
|
export function formsCmd(wrap) {
|
|
@@ -164,7 +88,8 @@ export function formsCmd(wrap) {
|
|
|
164
88
|
aliases: ['get'],
|
|
165
89
|
describe: 'Show form layout',
|
|
166
90
|
builder: (y) => y
|
|
167
|
-
.option('view', { type: 'string', default: 'Default view', describe: '
|
|
91
|
+
.option('view', { type: 'string', default: 'Default view', describe: 'Shared view name, title or sys_id' })
|
|
92
|
+
.epilogue('Discovers shared layouts, inherited forms and related-list metadata. JSON includes coverage limits; custom relationship scripts are never executed. Generic records get presentation is unchanged.'),
|
|
168
93
|
handler: wrap(async (argv, app) => {
|
|
169
94
|
app.requireInstance();
|
|
170
95
|
await showForm(app, argv.table, argv.view);
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { assertSafeExactMatch } from '../helpers.js';
|
|
2
|
+
import { errUsage, errAuth } from '../errors.js';
|
|
3
|
+
|
|
4
|
+
const namePattern = /^[a-zA-Z0-9_]+$/;
|
|
5
|
+
const raw = value => String(value && typeof value === 'object' ? value.value ?? '' : value ?? '');
|
|
6
|
+
const field = (record, name) => raw(record?.[name]);
|
|
7
|
+
const position = row => Number(field(row, 'position')) || 0;
|
|
8
|
+
const ordered = rows => [...rows].sort((a, b) => position(a) - position(b));
|
|
9
|
+
const reference = value => {
|
|
10
|
+
const text = raw(value);
|
|
11
|
+
assertSafeExactMatch(text);
|
|
12
|
+
return text;
|
|
13
|
+
};
|
|
14
|
+
const isName = value => typeof value === 'string' && namePattern.test(value);
|
|
15
|
+
const chunks = values => Array.from({ length: Math.ceil(values.length / 100) }, (_, index) => values.slice(index * 100, (index + 1) * 100));
|
|
16
|
+
|
|
17
|
+
function request(table, query, fields, limit = 1000) {
|
|
18
|
+
return { table, params: new URLSearchParams({
|
|
19
|
+
sysparm_query: query, sysparm_fields: fields, sysparm_limit: String(limit),
|
|
20
|
+
sysparm_display_value: 'all', sysparm_exclude_reference_link: 'true',
|
|
21
|
+
}) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Invocation-scoped, SDK-bound metadata reader. No persistence across users. */
|
|
25
|
+
export function createRecordMetadataReader(sdk, { limit = 1000, maxDepth = 32 } = {}) {
|
|
26
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 10000) throw errUsage('Metadata limit must be an integer from 1 to 10000');
|
|
27
|
+
if (!Number.isInteger(maxDepth) || maxDepth < 1 || maxDepth > 100) throw errUsage('Metadata inheritance depth must be an integer from 1 to 100');
|
|
28
|
+
const cache = new Map();
|
|
29
|
+
const reports = new Map();
|
|
30
|
+
|
|
31
|
+
async function reads(specs, warnings) {
|
|
32
|
+
const missing = specs.filter(spec => !cache.has(`${spec.table}?${spec.params}`));
|
|
33
|
+
// Reserve promises before transport so concurrent metadata requests share
|
|
34
|
+
// both successful reads and observed failures within this reader only.
|
|
35
|
+
const unique = [...new Map(missing.map(spec => [`${spec.table}?${spec.params}`, spec])).values()];
|
|
36
|
+
if (unique.length) {
|
|
37
|
+
const pending = (async () => {
|
|
38
|
+
if (unique.length > 1 && typeof sdk.batch === 'function') {
|
|
39
|
+
try {
|
|
40
|
+
const batch = await sdk.batch(unique.map((spec, index) => ({
|
|
41
|
+
id: `metadata-${index}`, method: 'GET', url: `/api/now/table/${spec.table}?${spec.params}`,
|
|
42
|
+
})), { fallback: true });
|
|
43
|
+
return unique.map((spec, index) => {
|
|
44
|
+
const item = batch.results?.find(result => result.id === `metadata-${index}`);
|
|
45
|
+
if (item?.status === 401) throw errAuth('Authentication failed while reading metadata');
|
|
46
|
+
if (!item?.ok) return { rows: [], error: item?.error?.message || `Metadata read failed (${item?.status || 'unknown status'})` };
|
|
47
|
+
return normalize(item.data?.result, spec);
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error.code === 'auth_error' || error.status === 401) throw error;
|
|
51
|
+
// The batch provider owns unsupported-endpoint fallback. Never
|
|
52
|
+
// replay authentication/ACL/server failures through another path.
|
|
53
|
+
return unique.map(() => ({ rows: [], error: error.message || 'Metadata batch failed' }));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return Promise.all(unique.map(async spec => {
|
|
57
|
+
try { return normalize(await sdk.list(spec.table, spec.params), spec); }
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error.code === 'auth_error' || error.status === 401) throw error;
|
|
60
|
+
return { rows: [], error: error.message || 'Metadata read failed' };
|
|
61
|
+
}
|
|
62
|
+
}));
|
|
63
|
+
})();
|
|
64
|
+
unique.forEach((spec, index) => cache.set(`${spec.table}?${spec.params}`, pending.then(results => results[index])));
|
|
65
|
+
}
|
|
66
|
+
return Promise.all(specs.map(async spec => {
|
|
67
|
+
const result = await cache.get(`${spec.table}?${spec.params}`);
|
|
68
|
+
if (result.error) warnings.push({ source: spec.table, reason: result.error });
|
|
69
|
+
if (result.truncated) warnings.push({ source: spec.table, reason: `Metadata limit ${spec.params.get('sysparm_limit')} reached; results may be incomplete` });
|
|
70
|
+
return result.rows;
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalize(rows, spec) {
|
|
75
|
+
if (!Array.isArray(rows)) return { rows: [], error: 'Metadata response was not an array' };
|
|
76
|
+
return { rows, truncated: rows.length >= Number(spec.params.get('sysparm_limit')) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function read({ table, view = 'Default view' }) {
|
|
80
|
+
if (!isName(table)) throw errUsage('Metadata table must be a table name containing letters, numbers or underscores');
|
|
81
|
+
if (typeof view !== 'string' || !view) throw errUsage('Metadata view must be a nonempty name, title or sys_id');
|
|
82
|
+
assertSafeExactMatch(view);
|
|
83
|
+
const key = `${table}\0${view}`;
|
|
84
|
+
if (!reports.has(key)) reports.set(key, load(table, view));
|
|
85
|
+
// Protect cached objects from a consumer attaching record-specific hints.
|
|
86
|
+
return structuredClone(await reports.get(key));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function load(table, view) {
|
|
90
|
+
const warnings = [];
|
|
91
|
+
const hierarchy = [];
|
|
92
|
+
const visited = new Set();
|
|
93
|
+
let next = request('sys_db_object', `name=${table}`, 'sys_id,name,super_class', 2);
|
|
94
|
+
for (let depth = 0; next && depth < maxDepth; depth++) {
|
|
95
|
+
const [rows] = await reads([next], warnings);
|
|
96
|
+
if (rows.length !== 1) {
|
|
97
|
+
warnings.push({ source: 'sys_db_object', reason: `Expected one visible table definition, found ${rows.length}; inheritance coverage is incomplete` });
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
const row = rows[0];
|
|
101
|
+
const name = field(row, 'name');
|
|
102
|
+
const id = field(row, 'sys_id');
|
|
103
|
+
if (!isName(name) || visited.has(name)) {
|
|
104
|
+
warnings.push({ source: 'sys_db_object', reason: 'Invalid or cyclic table inheritance metadata' });
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
visited.add(name);
|
|
108
|
+
hierarchy.push({ name, sys_id: id });
|
|
109
|
+
if (!Object.hasOwn(row, 'super_class')) warnings.push({ source: 'sys_db_object', reason: 'Parent metadata is not visible; inheritance coverage is incomplete' });
|
|
110
|
+
const parent = reference(row.super_class);
|
|
111
|
+
next = parent ? request('sys_db_object', `sys_id=${parent}`, 'sys_id,name,super_class', 2) : null;
|
|
112
|
+
}
|
|
113
|
+
if (next && hierarchy.length === maxDepth) warnings.push({ source: 'sys_db_object', reason: `Inheritance depth limit ${maxDepth} reached` });
|
|
114
|
+
if (!hierarchy.length) hierarchy.push({ name: table, sys_id: '' });
|
|
115
|
+
const names = hierarchy.map(item => item.name);
|
|
116
|
+
const defaultView = view === 'Default view' || view === 'default';
|
|
117
|
+
const viewQuery = defaultView ? 'name=^ORname=default^ORtitle=Default view' : `name=${view}^ORtitle=${view}^ORsys_id=${view}`;
|
|
118
|
+
const [views] = await reads([request('sys_ui_view', `${viewQuery}^userISEMPTY`, 'sys_id,name,title', limit)], warnings);
|
|
119
|
+
const byId = views.filter(row => field(row, 'sys_id') === view);
|
|
120
|
+
const byName = views.filter(row => field(row, 'name') === (defaultView ? '' : view));
|
|
121
|
+
const byTitle = views.filter(row => field(row, 'title') === view);
|
|
122
|
+
const byAlias = defaultView ? views.filter(row => field(row, 'name') === 'default') : [];
|
|
123
|
+
const candidates = byId.length ? byId : byName.length ? byName : byTitle.length ? byTitle : byAlias;
|
|
124
|
+
let viewId = '';
|
|
125
|
+
const viewCapped = views.length >= limit;
|
|
126
|
+
if (candidates.length === 1 && !viewCapped) {
|
|
127
|
+
viewId = reference(candidates[0].sys_id);
|
|
128
|
+
if (!viewId) warnings.push({ source: 'sys_ui_view', reason: 'View id is absent or not visible' });
|
|
129
|
+
} else if (candidates.length > 1 || viewCapped) warnings.push({ source: 'sys_ui_view', reason: 'View lookup is ambiguous or capped; no layout was selected' });
|
|
130
|
+
else if (!defaultView) warnings.push({ source: 'sys_ui_view', reason: 'Requested view is absent or not visible; no layout was selected' });
|
|
131
|
+
const canReadLayout = !viewCapped && candidates.length <= 1 && (defaultView || !!viewId);
|
|
132
|
+
const viewFilter = viewId ? `view=${viewId}${defaultView ? '^ORviewISEMPTY' : ''}` : 'viewISEMPTY';
|
|
133
|
+
const specs = [
|
|
134
|
+
request('sys_dictionary', `referenceIN${names.join(',')}^elementISNOTEMPTY`, 'sys_id,name,element,reference,column_label', limit),
|
|
135
|
+
request('sys_relationship', `basic_apply_toIN${names.join(',')}`, 'sys_id,name,basic_apply_to,basic_query_from,reference_field,simple_reference,advanced,query_with', limit),
|
|
136
|
+
...(canReadLayout ? [
|
|
137
|
+
request('sys_ui_form', `nameIN${names.join(',')}^${viewFilter}^sys_userISEMPTY`, 'sys_id,name,view', limit),
|
|
138
|
+
request('sys_ui_related_list', `nameIN${names.join(',')}^${viewFilter}^sys_userISEMPTY`, 'sys_id,name,view', limit),
|
|
139
|
+
] : []),
|
|
140
|
+
];
|
|
141
|
+
const [dictionary, relationships, forms = [], containers = []] = await reads(specs, warnings);
|
|
142
|
+
const nearest = rows => {
|
|
143
|
+
for (const name of names) {
|
|
144
|
+
const candidates = rows.filter(row => field(row, 'name') === name);
|
|
145
|
+
const exact = viewId ? candidates.filter(row => field(row, 'view') === viewId) : [];
|
|
146
|
+
const preferred = exact.length ? exact : candidates;
|
|
147
|
+
if (preferred.length > 1) warnings.push({ source: 'layout', reason: `Ambiguous layout for ${name}; no layout was selected` });
|
|
148
|
+
if (preferred.length) {
|
|
149
|
+
if (!field(preferred[0], 'sys_id')) {
|
|
150
|
+
warnings.push({ source: 'layout', reason: `Layout id is absent or not visible for ${name}` });
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return preferred.length === 1 ? preferred[0] : null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
};
|
|
158
|
+
const form = nearest(forms);
|
|
159
|
+
const container = nearest(containers);
|
|
160
|
+
const detailSpecs = [
|
|
161
|
+
...(form ? [request('sys_ui_form_section', `sys_ui_form=${reference(form.sys_id)}^ORDERBYposition`, 'sys_id,sys_ui_form,sys_ui_section,position', limit)] : []),
|
|
162
|
+
...(container ? [request('sys_ui_related_list_entry', `list_id=${reference(container.sys_id)}^ORDERBYposition`, 'sys_id,related_list,position,filter,order_by', limit)] : []),
|
|
163
|
+
];
|
|
164
|
+
const details = await reads(detailSpecs, warnings);
|
|
165
|
+
const links = form ? details.shift() : [];
|
|
166
|
+
const entries = container ? details.shift() : [];
|
|
167
|
+
const sectionIds = [...new Set(links.map(link => reference(link.sys_ui_section)).filter(Boolean))];
|
|
168
|
+
const relationshipIds = entries.map(entry => field(entry, 'related_list')).filter(value => /^REL:[a-f0-9]{32}$/i.test(value)).map(value => value.slice(4));
|
|
169
|
+
const missingRelationships = relationshipIds.filter(id => !relationships.some(row => field(row, 'sys_id') === id));
|
|
170
|
+
const tailSpecs = [
|
|
171
|
+
...chunks(sectionIds).flatMap(ids => [
|
|
172
|
+
{ ...request('sys_ui_section', `sys_idIN${ids.join(',')}^sys_userISEMPTY`, 'sys_id,name,caption,header,title,view', limit), group: 'sections' },
|
|
173
|
+
{ ...request('sys_ui_element', `sys_ui_sectionIN${ids.join(',')}^sys_userISEMPTY^ORDERBYposition`, 'sys_id,sys_ui_section,element,type,position', limit), group: 'elements' },
|
|
174
|
+
]),
|
|
175
|
+
...chunks([...new Set(missingRelationships)]).map(ids => ({
|
|
176
|
+
...request('sys_relationship', `sys_idIN${ids.join(',')}`, 'sys_id,name,basic_apply_to,basic_query_from,reference_field,simple_reference,advanced,query_with', limit), group: 'relationships',
|
|
177
|
+
})),
|
|
178
|
+
];
|
|
179
|
+
const tail = await reads(tailSpecs, warnings);
|
|
180
|
+
const group = name => tail.flatMap((rows, index) => tailSpecs[index].group === name ? rows : []);
|
|
181
|
+
const sections = group('sections');
|
|
182
|
+
const elements = group('elements');
|
|
183
|
+
relationships.push(...group('relationships'));
|
|
184
|
+
const sectionsOut = ordered(links).map(link => {
|
|
185
|
+
const id = field(link, 'sys_ui_section');
|
|
186
|
+
const section = sections.find(row => field(row, 'sys_id') === id);
|
|
187
|
+
if (!section) warnings.push({ source: 'sys_ui_section', reason: `A linked section is absent or not visible` });
|
|
188
|
+
return { sys_id: id, caption: field(section, 'caption'), position: position(link),
|
|
189
|
+
elements: ordered(elements.filter(row => field(row, 'sys_ui_section') === id)).map(row => ({
|
|
190
|
+
element: field(row, 'element'), type: field(row, 'type'), label: field(row, 'label'), position: position(row),
|
|
191
|
+
})) };
|
|
192
|
+
});
|
|
193
|
+
const relatedLists = buildRelationships({ entries, dictionary, relationships, sourceTable: container ? field(container, 'name') : '', names, warnings });
|
|
194
|
+
return {
|
|
195
|
+
table, requested_view: view, view_sys_id: viewId, hierarchy,
|
|
196
|
+
form: form ? { sys_id: field(form, 'sys_id'), table: field(form, 'name'), inherited: field(form, 'name') !== table, sections: sectionsOut } : null,
|
|
197
|
+
related_lists: relatedLists,
|
|
198
|
+
coverage: { state: warnings.length ? 'partial' : 'observed', scope: 'Visible shared layout metadata only; ACLs, personalization and advanced relationships may hide or change runtime results', warnings },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { read, clear() { cache.clear(); reports.clear(); } };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function buildRelationships({ entries, dictionary, relationships, sourceTable, names, warnings }) {
|
|
206
|
+
const result = new Map();
|
|
207
|
+
for (const row of dictionary) {
|
|
208
|
+
const table = field(row, 'name');
|
|
209
|
+
const referenceField = field(row, 'element');
|
|
210
|
+
if (!isName(table) || !isName(referenceField)) {
|
|
211
|
+
warnings.push({ source: 'sys_dictionary', reason: 'Ignored an invalid reference definition' });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (!names.includes(field(row, 'reference'))) warnings.push({ source: 'sys_dictionary', reason: 'A reference target is absent, unrelated, or not visible' });
|
|
215
|
+
const id = `${table}.${referenceField}`;
|
|
216
|
+
result.set(id, { id, kind: 'reference', table, reference_field: referenceField,
|
|
217
|
+
references: field(row, 'reference'), label: field(row, 'column_label'), configured: false,
|
|
218
|
+
query_status: names.includes(field(row, 'reference')) ? 'reference' : 'unavailable' });
|
|
219
|
+
}
|
|
220
|
+
for (const row of relationships) {
|
|
221
|
+
const id = `REL:${field(row, 'sys_id')}`;
|
|
222
|
+
if (!/^REL:[a-f0-9]{32}$/i.test(id)) continue;
|
|
223
|
+
const table = field(row, 'basic_query_from');
|
|
224
|
+
const referenceField = field(row, 'reference_field');
|
|
225
|
+
const queryable = field(row, 'advanced') !== 'true' && field(row, 'advanced') !== '1'
|
|
226
|
+
&& names.includes(field(row, 'basic_apply_to'))
|
|
227
|
+
&& ['true', '1'].includes(field(row, 'simple_reference')) && !field(row, 'query_with').trim()
|
|
228
|
+
&& isName(table) && isName(referenceField);
|
|
229
|
+
result.set(id, { id, kind: 'relationship', relationship_sys_id: field(row, 'sys_id'),
|
|
230
|
+
table: isName(table) ? table : '', reference_field: queryable ? referenceField : '',
|
|
231
|
+
label: field(row, 'name'), configured: false, query_status: queryable ? 'reference' : 'platform_required',
|
|
232
|
+
...(queryable ? {} : { reason: 'Custom/advanced relationship requires platform query logic; scripts are never executed by metadata discovery' }),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
for (const entry of ordered(entries)) {
|
|
236
|
+
const id = field(entry, 'related_list');
|
|
237
|
+
const existing = result.get(id);
|
|
238
|
+
const descriptor = existing || { id, kind: 'unresolved', table: '', reference_field: '', query_status: 'unavailable',
|
|
239
|
+
reason: 'Configured related list metadata is absent, unsupported, or not visible' };
|
|
240
|
+
result.set(id, { ...descriptor, configured: true, source_table: sourceTable, position: position(entry),
|
|
241
|
+
filter: field(entry, 'filter'), order_by: field(entry, 'order_by') });
|
|
242
|
+
if (!existing) warnings.push({ source: 'sys_ui_related_list_entry', reason: descriptor.reason });
|
|
243
|
+
}
|
|
244
|
+
return [...result.values()].sort((a, b) => Number(b.configured) - Number(a.configured) || (a.position || 0) - (b.position || 0));
|
|
245
|
+
}
|