@objectstack/lint 17.0.0-rc.0 → 17.0.0-rc.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/CHANGELOG.md +3627 -0
- package/dist/index.cjs +1836 -863
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +297 -61
- package/dist/index.d.ts +297 -61
- package/dist/index.js +1791 -840
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
package/dist/index.cjs
CHANGED
|
@@ -20,7 +20,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
ACTION_BODY_WRITE_EXCLUSIONS: () => ACTION_BODY_WRITE_EXCLUSIONS,
|
|
24
|
+
ACTION_BODY_WRITE_PATTERNS: () => ACTION_BODY_WRITE_PATTERNS,
|
|
25
|
+
ACTION_BODY_WRITE_PATTERN_IDS: () => ACTION_BODY_WRITE_PATTERN_IDS,
|
|
26
|
+
ACTION_BODY_WRITE_UNKNOWN_FIELD: () => ACTION_BODY_WRITE_UNKNOWN_FIELD,
|
|
23
27
|
ACTION_NAME_UNDEFINED: () => ACTION_NAME_UNDEFINED,
|
|
28
|
+
ACTION_RECORD_WRITE_DISCARDED: () => ACTION_RECORD_WRITE_DISCARDED,
|
|
29
|
+
ACTION_RECORD_WRITE_PATTERNS: () => ACTION_RECORD_WRITE_PATTERNS,
|
|
30
|
+
ACTION_RECORD_WRITE_PATTERN_IDS: () => ACTION_RECORD_WRITE_PATTERN_IDS,
|
|
24
31
|
AGENT_AUTHORING_WITHDRAWN: () => AGENT_AUTHORING_WITHDRAWN,
|
|
25
32
|
AI_SKILL_SURFACE_MISMATCH: () => AI_SKILL_SURFACE_MISMATCH,
|
|
26
33
|
AI_SKILL_TOOL_UNRESOLVED: () => AI_SKILL_TOOL_UNRESOLVED,
|
|
@@ -47,13 +54,20 @@ __export(index_exports, {
|
|
|
47
54
|
FIELD_GROUP_UNDECLARED: () => FIELD_GROUP_UNDECLARED,
|
|
48
55
|
FILTER_TOKEN_UNKNOWN: () => FILTER_TOKEN_UNKNOWN,
|
|
49
56
|
FLOW_DRAFT_STATUS_AMBIGUOUS: () => FLOW_DRAFT_STATUS_AMBIGUOUS,
|
|
57
|
+
FLOW_NODE_WRITE_UNKNOWN_FIELD: () => FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
50
58
|
FLOW_TEMPLATE_LOOKUP_TRAVERSAL: () => FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
|
|
51
59
|
FLOW_TEMPLATE_UNKNOWN_FIELD: () => FLOW_TEMPLATE_UNKNOWN_FIELD,
|
|
52
60
|
FLOW_TRIGGER_UNKNOWN_OBJECT: () => FLOW_TRIGGER_UNKNOWN_OBJECT,
|
|
53
61
|
FLOW_UPDATE_READONLY_FIELD: () => FLOW_UPDATE_READONLY_FIELD,
|
|
54
62
|
FLOW_UPDATE_READONLY_WHEN_FIELD: () => FLOW_UPDATE_READONLY_WHEN_FIELD,
|
|
63
|
+
FLOW_WRITE_NODE_TYPES: () => FLOW_WRITE_NODE_TYPES,
|
|
64
|
+
FLOW_WRITE_NODE_TYPES_DEFERRED: () => FLOW_WRITE_NODE_TYPES_DEFERRED,
|
|
55
65
|
FORM_COLSPAN_ABSOLUTE: () => FORM_COLSPAN_ABSOLUTE,
|
|
56
66
|
FORM_FIELD_UNKNOWN: () => FORM_FIELD_UNKNOWN,
|
|
67
|
+
HOOK_BODY_WRITE_EXCLUSIONS: () => HOOK_BODY_WRITE_EXCLUSIONS,
|
|
68
|
+
HOOK_BODY_WRITE_PATTERNS: () => HOOK_BODY_WRITE_PATTERNS,
|
|
69
|
+
HOOK_BODY_WRITE_PATTERN_IDS: () => HOOK_BODY_WRITE_PATTERN_IDS,
|
|
70
|
+
HOOK_BODY_WRITE_UNKNOWN_FIELD: () => HOOK_BODY_WRITE_UNKNOWN_FIELD,
|
|
57
71
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE: () => LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
58
72
|
MEASURE_AGGREGATE_INCOHERENT: () => MEASURE_AGGREGATE_INCOHERENT,
|
|
59
73
|
NAV_OBJECT_UNGRANTED: () => NAV_OBJECT_UNGRANTED,
|
|
@@ -67,6 +81,7 @@ __export(index_exports, {
|
|
|
67
81
|
REACT_CHART_AXIS_UNKNOWN: () => REACT_CHART_AXIS_UNKNOWN,
|
|
68
82
|
REACT_CHART_FIELD_UNKNOWN: () => REACT_CHART_FIELD_UNKNOWN,
|
|
69
83
|
REFERENCE_INTEGRITY_RULES: () => REFERENCE_INTEGRITY_RULES,
|
|
84
|
+
SEARCHABLE_FIELD_UNKNOWN: () => SEARCHABLE_FIELD_UNKNOWN,
|
|
70
85
|
SECURITY_ANCHOR_HIGH_PRIVILEGE: () => SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
71
86
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET: () => SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
72
87
|
SECURITY_DELEGATION_MISSING_REASON: () => SECURITY_DELEGATION_MISSING_REASON,
|
|
@@ -99,6 +114,9 @@ __export(index_exports, {
|
|
|
99
114
|
WIDGET_MEASURE_UNKNOWN: () => WIDGET_MEASURE_UNKNOWN,
|
|
100
115
|
buildAccessMatrix: () => buildAccessMatrix,
|
|
101
116
|
diffAccessMatrix: () => diffAccessMatrix,
|
|
117
|
+
extractHookBodyWriteSet: () => extractHookBodyWriteSet,
|
|
118
|
+
extractHookBodyWrites: () => extractHookBodyWrites,
|
|
119
|
+
validateActionBodyWrites: () => validateActionBodyWrites,
|
|
102
120
|
validateActionNameRefs: () => validateActionNameRefs,
|
|
103
121
|
validateAiAgentAuthoring: () => validateAiAgentAuthoring,
|
|
104
122
|
validateAiSurfaceAffinity: () => validateAiSurfaceAffinity,
|
|
@@ -108,9 +126,11 @@ __export(index_exports, {
|
|
|
108
126
|
validateChartBindings: () => validateChartBindings,
|
|
109
127
|
validateDashboardActionRefs: () => validateDashboardActionRefs,
|
|
110
128
|
validateFilterTokens: () => validateFilterTokens,
|
|
129
|
+
validateFlowNodeWrites: () => validateFlowNodeWrites,
|
|
111
130
|
validateFlowTemplatePaths: () => validateFlowTemplatePaths,
|
|
112
131
|
validateFlowTriggerReadiness: () => validateFlowTriggerReadiness,
|
|
113
132
|
validateFormLayout: () => validateFormLayout,
|
|
133
|
+
validateHookBodyWrites: () => validateHookBodyWrites,
|
|
114
134
|
validateJsxPages: () => validateJsxPages,
|
|
115
135
|
validateListViewMode: () => validateListViewMode,
|
|
116
136
|
validateNavAccess: () => validateNavAccess,
|
|
@@ -124,6 +144,7 @@ __export(index_exports, {
|
|
|
124
144
|
validateRecordTitle: () => validateRecordTitle,
|
|
125
145
|
validateReferenceIntegrity: () => validateReferenceIntegrity,
|
|
126
146
|
validateResponsiveStyles: () => validateResponsiveStyles,
|
|
147
|
+
validateSearchableFields: () => validateSearchableFields,
|
|
127
148
|
validateSecurityPosture: () => validateSecurityPosture,
|
|
128
149
|
validateSeedReplaySafety: () => validateSeedReplaySafety,
|
|
129
150
|
validateSeedStateMachine: () => validateSeedStateMachine,
|
|
@@ -137,7 +158,18 @@ __export(index_exports, {
|
|
|
137
158
|
module.exports = __toCommonJS(index_exports);
|
|
138
159
|
|
|
139
160
|
// src/validate-widget-bindings.ts
|
|
161
|
+
var import_data2 = require("@objectstack/spec/data");
|
|
162
|
+
var import_ui = require("@objectstack/spec/ui");
|
|
163
|
+
|
|
164
|
+
// src/system-fields.ts
|
|
140
165
|
var import_data = require("@objectstack/spec/data");
|
|
166
|
+
var import_system = require("@objectstack/spec/system");
|
|
167
|
+
var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
168
|
+
...import_data.FIELD_GROUP_SYSTEM_FIELDS,
|
|
169
|
+
...Object.values(import_system.SystemFieldName)
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
// src/validate-widget-bindings.ts
|
|
141
173
|
var WIDGET_DATASET_UNKNOWN = "widget-dataset-unknown";
|
|
142
174
|
var WIDGET_DIMENSION_UNKNOWN = "widget-dimension-unknown";
|
|
143
175
|
var WIDGET_MEASURE_UNKNOWN = "widget-measure-unknown";
|
|
@@ -168,20 +200,18 @@ function asArray(v) {
|
|
|
168
200
|
function asStrings(v) {
|
|
169
201
|
return Array.isArray(v) ? v.filter((s) => typeof s === "string") : [];
|
|
170
202
|
}
|
|
171
|
-
var
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
"
|
|
177
|
-
"
|
|
178
|
-
"
|
|
179
|
-
"funnel",
|
|
180
|
-
"scatter",
|
|
181
|
-
"treemap",
|
|
182
|
-
"sankey",
|
|
183
|
-
"radar"
|
|
203
|
+
var MEASURE_EXEMPT_CHART_TYPES = /* @__PURE__ */ new Set([
|
|
204
|
+
"gauge",
|
|
205
|
+
"solid-gauge",
|
|
206
|
+
"metric",
|
|
207
|
+
"kpi",
|
|
208
|
+
"bullet",
|
|
209
|
+
"table",
|
|
210
|
+
"pivot"
|
|
184
211
|
]);
|
|
212
|
+
var CHART_TYPES = new Set(
|
|
213
|
+
import_ui.ChartTypeSchema.options.filter((t) => !MEASURE_EXEMPT_CHART_TYPES.has(t))
|
|
214
|
+
);
|
|
185
215
|
function levenshtein(a, b) {
|
|
186
216
|
const m = a.length, n = b.length;
|
|
187
217
|
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
@@ -227,18 +257,6 @@ function list(names) {
|
|
|
227
257
|
}
|
|
228
258
|
var DATE_RANGE_FILTER_NAME = "dateRange";
|
|
229
259
|
var DATE_RANGE_DEFAULT_FIELD = "created_at";
|
|
230
|
-
var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
231
|
-
"id",
|
|
232
|
-
"created_at",
|
|
233
|
-
"created_by",
|
|
234
|
-
"updated_at",
|
|
235
|
-
"updated_by",
|
|
236
|
-
"owner_id",
|
|
237
|
-
"organization_id",
|
|
238
|
-
"tenant_id",
|
|
239
|
-
"user_id",
|
|
240
|
-
"deleted_at"
|
|
241
|
-
]);
|
|
242
260
|
function dashboardFilterDefs(dash) {
|
|
243
261
|
const byName = /* @__PURE__ */ new Map();
|
|
244
262
|
const dateRange = dash.dateRange;
|
|
@@ -293,7 +311,7 @@ function validateWidgetBindings(stack) {
|
|
|
293
311
|
const aggregate = typeof m.aggregate === "string" ? m.aggregate : void 0;
|
|
294
312
|
if (!field || !aggregate) continue;
|
|
295
313
|
const ftype = fieldTypes.get(field);
|
|
296
|
-
if (ftype && (0,
|
|
314
|
+
if (ftype && (0, import_data2.isIncoherentAggregate)(aggregate, ftype)) {
|
|
297
315
|
findings.push({
|
|
298
316
|
severity: "warning",
|
|
299
317
|
rule: MEASURE_AGGREGATE_INCOHERENT,
|
|
@@ -472,6 +490,7 @@ function validateWidgetBindings(stack) {
|
|
|
472
490
|
|
|
473
491
|
// src/validate-expressions.ts
|
|
474
492
|
var import_formula = require("@objectstack/formula");
|
|
493
|
+
var import_automation = require("@objectstack/spec/automation");
|
|
475
494
|
function asArray2(v) {
|
|
476
495
|
if (Array.isArray(v)) return v;
|
|
477
496
|
if (v && typeof v === "object") {
|
|
@@ -532,37 +551,53 @@ function validateStackExpressions(stack) {
|
|
|
532
551
|
for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
|
|
533
552
|
for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
|
|
534
553
|
};
|
|
554
|
+
const checkDeclaredPredicate = (where, raw) => {
|
|
555
|
+
if (raw == null) return;
|
|
556
|
+
const res = (0, import_formula.validateExpression)("predicate", raw);
|
|
557
|
+
for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
|
|
558
|
+
for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
|
|
559
|
+
};
|
|
535
560
|
for (const flow of asArray2(stack.flows)) {
|
|
536
561
|
const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
|
|
537
562
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
538
|
-
const edges = Array.isArray(flow.edges) ? flow.edges : [];
|
|
539
563
|
const startNode = nodes.find((n) => n.type === "start");
|
|
540
564
|
const startCfg = startNode?.config ?? {};
|
|
541
565
|
const objectName = typeof startCfg.objectName === "string" ? startCfg.objectName : void 0;
|
|
542
|
-
for (const
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
566
|
+
for (const graph of (0, import_automation.collectFlowGraphs)(flow)) {
|
|
567
|
+
const at = graph.scope ? `flow '${flowName}' \xB7 ${graph.scope}` : `flow '${flowName}'`;
|
|
568
|
+
for (const node of graph.nodes) {
|
|
569
|
+
const cfg = node.config ?? {};
|
|
570
|
+
check(`${at} \xB7 node '${node.id}' (${node.type}) condition`, cfg.condition, objectName);
|
|
571
|
+
const nodeType = typeof node.type === "string" ? node.type : "";
|
|
572
|
+
for (const found of (0, import_automation.resolveFlowNodeExpressions)(nodeType, cfg)) {
|
|
573
|
+
if (found.entry.role !== "predicate") continue;
|
|
574
|
+
checkDeclaredPredicate(
|
|
575
|
+
`${at} \xB7 node '${node.id}' (${nodeType}) ${found.entry.label} at config.${found.path}`,
|
|
576
|
+
found.value
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
if (node.type === "script") {
|
|
580
|
+
const fn = (typeof cfg.function === "string" ? cfg.function.trim() : "") || (typeof cfg.functionName === "string" ? cfg.functionName.trim() : "");
|
|
581
|
+
const action = typeof cfg.actionType === "string" ? cfg.actionType.trim() : "";
|
|
582
|
+
const inline = typeof cfg.script === "string" ? cfg.script.trim() : "";
|
|
583
|
+
if (!fn && !action && !inline) {
|
|
584
|
+
issues.push({
|
|
585
|
+
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
586
|
+
message: `script node declares neither \`actionType\` nor \`function\` \u2014 it would do nothing at runtime. Name a built-in action (e.g. \`actionType: 'email'\`) or a registered function (\`function: 'my_fn'\`, registered via \`defineStack({ functions })\`).`,
|
|
587
|
+
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
588
|
+
});
|
|
589
|
+
} else if (action === "invoke_function" && !fn) {
|
|
590
|
+
issues.push({
|
|
591
|
+
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
592
|
+
message: `script node uses \`actionType: 'invoke_function'\` but no \`function\` (or \`functionName\`) \u2014 it names no callable. Set \`function: 'my_fn'\` and register it via \`defineStack({ functions })\`.`,
|
|
593
|
+
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
594
|
+
});
|
|
595
|
+
}
|
|
561
596
|
}
|
|
562
597
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
598
|
+
for (const edge of graph.edges) {
|
|
599
|
+
check(`${at} \xB7 edge '${edge.id}' (${edge.source}\u2192${edge.target}) condition`, edge.condition, objectName);
|
|
600
|
+
}
|
|
566
601
|
}
|
|
567
602
|
}
|
|
568
603
|
for (const obj of objects) {
|
|
@@ -815,6 +850,85 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
815
850
|
return findings;
|
|
816
851
|
}
|
|
817
852
|
|
|
853
|
+
// src/flow-walk.ts
|
|
854
|
+
var import_automation2 = require("@objectstack/spec/automation");
|
|
855
|
+
function isRec(v) {
|
|
856
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
857
|
+
}
|
|
858
|
+
function strName(v) {
|
|
859
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
860
|
+
}
|
|
861
|
+
var REGION_SLOTS = new Map(
|
|
862
|
+
[...import_automation2.FLOW_REGION_SLOTS_BY_TYPE].map(([type, slots]) => [type, slots.map((s) => s.key)])
|
|
863
|
+
);
|
|
864
|
+
var REGION_CONFIG_KEYS = import_automation2.FLOW_REGION_CONFIG_KEYS;
|
|
865
|
+
var MAX_REGION_DEPTH = 16;
|
|
866
|
+
function flowNodeLabel(node, index) {
|
|
867
|
+
return strName(node.label) ?? strName(node.id) ?? `#${index}`;
|
|
868
|
+
}
|
|
869
|
+
function stripRegions(config) {
|
|
870
|
+
if (!isRec(config)) return void 0;
|
|
871
|
+
let out;
|
|
872
|
+
for (const key of Object.keys(config)) {
|
|
873
|
+
if (!REGION_CONFIG_KEYS.has(key)) continue;
|
|
874
|
+
out ?? (out = { ...config });
|
|
875
|
+
delete out[key];
|
|
876
|
+
}
|
|
877
|
+
return out ?? config;
|
|
878
|
+
}
|
|
879
|
+
function walkFlowNodes(flow, flowPath) {
|
|
880
|
+
const out = [];
|
|
881
|
+
if (!isRec(flow)) return out;
|
|
882
|
+
const visitList = (nodes, basePath, trail, depth) => {
|
|
883
|
+
if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
|
|
884
|
+
nodes.forEach((raw, index) => {
|
|
885
|
+
if (!isRec(raw)) return;
|
|
886
|
+
const path = `${basePath}[${index}]`;
|
|
887
|
+
out.push({
|
|
888
|
+
node: raw,
|
|
889
|
+
path,
|
|
890
|
+
localConfig: stripRegions(raw.config),
|
|
891
|
+
regionTrail: trail,
|
|
892
|
+
depth
|
|
893
|
+
});
|
|
894
|
+
const type = strName(raw.type);
|
|
895
|
+
const slots = type ? REGION_SLOTS.get(type) : void 0;
|
|
896
|
+
if (!slots || !isRec(raw.config)) return;
|
|
897
|
+
const config = raw.config;
|
|
898
|
+
const here = `${type} "${flowNodeLabel(raw, index)}"`;
|
|
899
|
+
for (const slot of slots) {
|
|
900
|
+
const value = config[slot];
|
|
901
|
+
if (slot === "branches") {
|
|
902
|
+
if (!Array.isArray(value)) continue;
|
|
903
|
+
value.forEach((branch, b) => {
|
|
904
|
+
if (!isRec(branch)) return;
|
|
905
|
+
const branchName = strName(branch.name) ?? `#${b}`;
|
|
906
|
+
visitList(
|
|
907
|
+
branch.nodes,
|
|
908
|
+
`${path}.config.branches[${b}].nodes`,
|
|
909
|
+
joinTrail(trail, `${here} \u203A branch ${branchName}`),
|
|
910
|
+
depth + 1
|
|
911
|
+
);
|
|
912
|
+
});
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
if (!isRec(value)) continue;
|
|
916
|
+
visitList(
|
|
917
|
+
value.nodes,
|
|
918
|
+
`${path}.config.${slot}.nodes`,
|
|
919
|
+
joinTrail(trail, `${here} \u203A ${slot}`),
|
|
920
|
+
depth + 1
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
};
|
|
925
|
+
visitList(flow.nodes, `${flowPath}.nodes`, "", 0);
|
|
926
|
+
return out;
|
|
927
|
+
}
|
|
928
|
+
function joinTrail(trail, segment) {
|
|
929
|
+
return trail ? `${trail} \u203A ${segment}` : segment;
|
|
930
|
+
}
|
|
931
|
+
|
|
818
932
|
// src/validate-flow-template-paths.ts
|
|
819
933
|
var FLOW_TEMPLATE_UNKNOWN_FIELD = "flow-template-unknown-field";
|
|
820
934
|
var FLOW_TEMPLATE_LOOKUP_TRAVERSAL = "flow-template-lookup-traversal";
|
|
@@ -828,19 +942,10 @@ function asArray5(v) {
|
|
|
828
942
|
}
|
|
829
943
|
return [];
|
|
830
944
|
}
|
|
831
|
-
var
|
|
832
|
-
|
|
945
|
+
var IMPLICIT_HEADS = /* @__PURE__ */ new Set([
|
|
946
|
+
...SYSTEM_FIELDS,
|
|
833
947
|
"name",
|
|
834
948
|
"owner",
|
|
835
|
-
"owner_id",
|
|
836
|
-
"created_at",
|
|
837
|
-
"created_by",
|
|
838
|
-
"updated_at",
|
|
839
|
-
"updated_by",
|
|
840
|
-
"organization_id",
|
|
841
|
-
"tenant_id",
|
|
842
|
-
"is_deleted",
|
|
843
|
-
"deleted_at",
|
|
844
949
|
"record_type"
|
|
845
950
|
]);
|
|
846
951
|
var RELATION_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -970,12 +1075,13 @@ function validateFlowTemplatePaths(stack) {
|
|
|
970
1075
|
if (!obj) return;
|
|
971
1076
|
const fieldTypes = fieldTypesOf(obj);
|
|
972
1077
|
const expandSet = declaredExpandOf(flow);
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
const
|
|
1078
|
+
walkFlowNodes(flow, `flows[${flowIndex}]`).forEach(({ node, path: nodePath, regionTrail, localConfig }, walkIndex) => {
|
|
1079
|
+
const nodeLabel = typeof node.type === "string" ? node.type : typeof node.id === "string" ? node.id : `#${walkIndex}`;
|
|
1080
|
+
const where = regionTrail ? `flow "${flowName}" ${regionTrail} node "${nodeLabel}"` : `flow "${flowName}" node "${nodeLabel}"`;
|
|
976
1081
|
const nodeType = typeof node.type === "string" ? node.type : "";
|
|
977
1082
|
const guarded = FILTER_GUARDED_NODE_TYPES.has(nodeType);
|
|
978
|
-
const
|
|
1083
|
+
const scanNode = localConfig !== void 0 && localConfig !== node.config ? { ...node, config: localConfig } : node;
|
|
1084
|
+
const leaves = collectNodeLeaves(scanNode, guarded);
|
|
979
1085
|
if (leaves.length === 0) return;
|
|
980
1086
|
const seenUnknown = /* @__PURE__ */ new Set();
|
|
981
1087
|
const seenTraversal = /* @__PURE__ */ new Set();
|
|
@@ -985,15 +1091,15 @@ function validateFlowTemplatePaths(stack) {
|
|
|
985
1091
|
const head = rest[0];
|
|
986
1092
|
const hasSubPath = rest.length > 1;
|
|
987
1093
|
const nextIsIdentifier = hasSubPath && !/^\d+$/.test(rest[1]);
|
|
988
|
-
const isKnown = fieldTypes.has(head) ||
|
|
1094
|
+
const isKnown = fieldTypes.has(head) || IMPLICIT_HEADS.has(head);
|
|
989
1095
|
if (!isKnown) {
|
|
990
1096
|
if (seenUnknown.has(head)) continue;
|
|
991
1097
|
seenUnknown.add(head);
|
|
992
1098
|
findings.push({
|
|
993
1099
|
severity: inFilter ? "error" : "warning",
|
|
994
1100
|
rule: FLOW_TEMPLATE_UNKNOWN_FIELD,
|
|
995
|
-
where
|
|
996
|
-
path:
|
|
1101
|
+
where,
|
|
1102
|
+
path: nodePath,
|
|
997
1103
|
message: inFilter ? `${nodeType} filter references '{record.${rest.join(".")}}', but '${head}' is not a field on object '${objectName}' \u2014 the token resolves to nothing, which DROPS the condition from the query instead of narrowing it. The node refuses to run at execution time (#3810).` : `template references '{record.${rest.join(".")}}', but '${head}' is not a field on object '${objectName}' \u2014 it resolves to an empty string at runtime (silently).`,
|
|
998
1104
|
hint: inFilter ? `Check the field name against the object's field definitions (e.g. '{record.full_name}', not '{record.full_naem}'); system columns like id/created_at/owner are also addressable. This gates the build rather than warning: an absent condition WIDENS the query, so the runtime has already decided to refuse this node.` : `Check the field name against the object's field definitions (e.g. '{record.full_name}', not '{record.full_naem}'). System columns like id/created_at/owner are also addressable.`
|
|
999
1105
|
});
|
|
@@ -1008,8 +1114,8 @@ function validateFlowTemplatePaths(stack) {
|
|
|
1008
1114
|
findings.push({
|
|
1009
1115
|
severity: inFilter ? "error" : "warning",
|
|
1010
1116
|
rule: FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
|
|
1011
|
-
where
|
|
1012
|
-
path:
|
|
1117
|
+
where,
|
|
1118
|
+
path: nodePath,
|
|
1013
1119
|
message: inFilter ? `${nodeType} filter references '{record.${key}}', a cross-object hop through the ${headType} field '${head}' \u2014 the flow record carries '${head}' as a scalar id, not an expanded object, so the token resolves to nothing and the condition is DROPPED from the query instead of narrowing it. The node refuses to run at execution time (#3810).` : `template references '{record.${key}}', a cross-object hop through the ${headType} field '${head}' \u2014 the flow record carries '${head}' as a scalar id, not an expanded object, so this resolves to an empty string at runtime (silently).`,
|
|
1014
1120
|
hint: inFilter ? `Opt in to resolve it: add '${head}' to the start node's config.expand (#3475) and the engine re-reads it as the run's identity. Otherwise filter on the foreign-key id directly ('{record.${head}}'), or project the value via a formula field on '${objectName}'. This gates the build rather than warning: an absent condition WIDENS the query.` : `Opt in to resolve it: add '${head}' to the start node's config.expand (#3475) and the engine re-reads it as the run's identity. Otherwise reference the foreign-key id directly ('{record.${head}}'), or project the value via a formula field on '${objectName}'.`
|
|
1015
1121
|
});
|
|
@@ -1073,8 +1179,8 @@ function validateReadonlyFlowWrites(stack) {
|
|
|
1073
1179
|
if (flow.runAs === "system") return;
|
|
1074
1180
|
const runAs = flow.runAs === "user" || flow.runAs === "system" ? flow.runAs : "user";
|
|
1075
1181
|
const flowName = typeof flow.name === "string" ? flow.name : `#${flowIndex}`;
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1182
|
+
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
1183
|
+
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
1078
1184
|
if (node?.type !== "update_record") return;
|
|
1079
1185
|
const config = node.config ?? {};
|
|
1080
1186
|
const objectName = readLiteralObjectName(config);
|
|
@@ -1083,7 +1189,8 @@ function validateReadonlyFlowWrites(stack) {
|
|
|
1083
1189
|
if (!fieldMap) return;
|
|
1084
1190
|
const fields = config.fields;
|
|
1085
1191
|
if (!fields || typeof fields !== "object" || Array.isArray(fields)) return;
|
|
1086
|
-
const nodeName =
|
|
1192
|
+
const nodeName = flowNodeLabel(node, walkIndex);
|
|
1193
|
+
const where = regionTrail ? `flow "${flowName}" \u203A ${regionTrail} \u203A node "${nodeName}"` : `flow "${flowName}" \u203A node "${nodeName}"`;
|
|
1087
1194
|
for (const fieldName of Object.keys(fields)) {
|
|
1088
1195
|
const meta = fieldMap.get(fieldName);
|
|
1089
1196
|
if (!meta) continue;
|
|
@@ -1091,8 +1198,8 @@ function validateReadonlyFlowWrites(stack) {
|
|
|
1091
1198
|
findings.push({
|
|
1092
1199
|
severity: "error",
|
|
1093
1200
|
rule: FLOW_UPDATE_READONLY_FIELD,
|
|
1094
|
-
where
|
|
1095
|
-
path:
|
|
1201
|
+
where,
|
|
1202
|
+
path: `${nodePath}.config.fields.${fieldName}`,
|
|
1096
1203
|
message: `writes field '${fieldName}', which object '${objectName}' declares readonly:true. Under runAs:'${runAs}' the engine silently strips readonly fields from the UPDATE payload (#2948), so this write never lands \u2014 while the step still reports success.`,
|
|
1097
1204
|
hint: `If automation is meant to maintain this field, declare the flow runAs:'system' (the intended channel \u2014 readonly governs the end-user/API surface, not trusted system writers). Otherwise remove '${fieldName}' from this update_record node.`
|
|
1098
1205
|
});
|
|
@@ -1100,8 +1207,8 @@ function validateReadonlyFlowWrites(stack) {
|
|
|
1100
1207
|
findings.push({
|
|
1101
1208
|
severity: "warning",
|
|
1102
1209
|
rule: FLOW_UPDATE_READONLY_WHEN_FIELD,
|
|
1103
|
-
where
|
|
1104
|
-
path:
|
|
1210
|
+
where,
|
|
1211
|
+
path: `${nodePath}.config.fields.${fieldName}`,
|
|
1105
1212
|
message: `writes field '${fieldName}', which object '${objectName}' declares readonlyWhen. On records where that predicate is TRUE, a runAs:'${runAs}' UPDATE strips the field (#3042), so this write may silently not land depending on the record's state.`,
|
|
1106
1213
|
hint: `If automation must maintain this field regardless of record state, run the flow runAs:'system'. Otherwise confirm this node only targets records whose readonlyWhen predicate is FALSE.`
|
|
1107
1214
|
});
|
|
@@ -1553,208 +1660,586 @@ function validateReactPages(stack) {
|
|
|
1553
1660
|
|
|
1554
1661
|
// src/validate-react-page-props.ts
|
|
1555
1662
|
var import_node_module2 = require("module");
|
|
1556
|
-
var
|
|
1557
|
-
var
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
throw new Error(
|
|
1566
|
-
`@objectstack/lint: validating a kind:'react' page requires the "typescript" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "typescript" in the image; it is only loaded when a react-source page is validated.`
|
|
1567
|
-
);
|
|
1663
|
+
var import_ui2 = require("@objectstack/spec/ui");
|
|
1664
|
+
var import_data3 = require("@objectstack/spec/data");
|
|
1665
|
+
|
|
1666
|
+
// src/validate-searchable-fields.ts
|
|
1667
|
+
var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
|
|
1668
|
+
function asArray10(v) {
|
|
1669
|
+
if (Array.isArray(v)) return v;
|
|
1670
|
+
if (v && typeof v === "object") {
|
|
1671
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
1568
1672
|
}
|
|
1569
|
-
return
|
|
1673
|
+
return [];
|
|
1570
1674
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
import_ui.REACT_BLOCKS.map((b) => [
|
|
1574
|
-
b.tag,
|
|
1575
|
-
{
|
|
1576
|
-
requiredBindings: b.interactions.filter((i) => i.required).map((i) => i.name),
|
|
1577
|
-
knownProps: new Set(b.interactions.map((i) => i.name))
|
|
1578
|
-
}
|
|
1579
|
-
])
|
|
1580
|
-
);
|
|
1581
|
-
function editDistance(a, b, cap = 2) {
|
|
1582
|
-
if (Math.abs(a.length - b.length) > cap) return cap + 1;
|
|
1583
|
-
const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
|
|
1584
|
-
for (let j = 1; j <= b.length; j++) {
|
|
1585
|
-
let prev = dp[0];
|
|
1586
|
-
dp[0] = j;
|
|
1587
|
-
for (let i = 1; i <= a.length; i++) {
|
|
1588
|
-
const tmp = dp[i];
|
|
1589
|
-
dp[i] = Math.min(dp[i] + 1, dp[i - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1590
|
-
prev = tmp;
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
return dp[a.length];
|
|
1675
|
+
function isRec2(v) {
|
|
1676
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1594
1677
|
}
|
|
1595
|
-
function
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1678
|
+
function strName2(v) {
|
|
1679
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1680
|
+
}
|
|
1681
|
+
function declaredFieldNames(obj) {
|
|
1682
|
+
const fields = obj.fields;
|
|
1683
|
+
if (!fields || typeof fields !== "object") return null;
|
|
1684
|
+
const names = /* @__PURE__ */ new Set();
|
|
1685
|
+
for (const f of asArray10(fields)) {
|
|
1686
|
+
const n = strName2(f.name);
|
|
1687
|
+
if (n) names.add(n);
|
|
1605
1688
|
}
|
|
1606
|
-
return
|
|
1689
|
+
return names.size > 0 ? names : null;
|
|
1607
1690
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
if (node.kind === tsc.SyntaxKind.NullKeyword) return null;
|
|
1617
|
-
if (tsc.isArrayLiteralExpression(node)) {
|
|
1618
|
-
const out = [];
|
|
1619
|
-
for (const el of node.elements) {
|
|
1620
|
-
const v = staticValue(tsc, sf, el);
|
|
1621
|
-
if (v === NOT_STATIC) return NOT_STATIC;
|
|
1622
|
-
out.push(v);
|
|
1691
|
+
function suggest2(target, known) {
|
|
1692
|
+
let best;
|
|
1693
|
+
let bestScore = Infinity;
|
|
1694
|
+
for (const candidate of known) {
|
|
1695
|
+
const d = distance(target, candidate);
|
|
1696
|
+
if (d < bestScore) {
|
|
1697
|
+
bestScore = d;
|
|
1698
|
+
best = candidate;
|
|
1623
1699
|
}
|
|
1624
|
-
return out;
|
|
1625
1700
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1701
|
+
const limit = Math.max(2, Math.floor(target.length / 3));
|
|
1702
|
+
return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
|
|
1703
|
+
}
|
|
1704
|
+
function distance(a, b) {
|
|
1705
|
+
const m = a.length;
|
|
1706
|
+
const n = b.length;
|
|
1707
|
+
if (m === 0) return n;
|
|
1708
|
+
if (n === 0) return m;
|
|
1709
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
1710
|
+
for (let i = 1; i <= m; i++) {
|
|
1711
|
+
const curr = [i, ...new Array(n).fill(0)];
|
|
1712
|
+
for (let j = 1; j <= n; j++) {
|
|
1713
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
1714
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
1635
1715
|
}
|
|
1636
|
-
|
|
1716
|
+
prev = curr;
|
|
1637
1717
|
}
|
|
1638
|
-
return
|
|
1639
|
-
}
|
|
1640
|
-
function attrValue(tsc, sf, attr) {
|
|
1641
|
-
const init = attr.initializer;
|
|
1642
|
-
if (!init) return true;
|
|
1643
|
-
if (tsc.isStringLiteral(init)) return init.text;
|
|
1644
|
-
if (tsc.isJsxExpression(init)) return staticValue(tsc, sf, init.expression);
|
|
1645
|
-
return NOT_STATIC;
|
|
1718
|
+
return prev[n];
|
|
1646
1719
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
"created_at",
|
|
1654
|
-
"created_by",
|
|
1655
|
-
"updated_at",
|
|
1656
|
-
"updated_by",
|
|
1657
|
-
"owner_id",
|
|
1658
|
-
"organization_id",
|
|
1659
|
-
"tenant_id",
|
|
1660
|
-
"user_id",
|
|
1661
|
-
"deleted_at"
|
|
1662
|
-
]);
|
|
1663
|
-
function namedArray(v) {
|
|
1664
|
-
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
1665
|
-
if (v && typeof v === "object") {
|
|
1666
|
-
return Object.entries(v).map(([name, def]) => ({
|
|
1667
|
-
name,
|
|
1668
|
-
...def && typeof def === "object" ? def : {}
|
|
1669
|
-
}));
|
|
1720
|
+
function indexObjectSearchTargets(stack) {
|
|
1721
|
+
const fieldsByObject = /* @__PURE__ */ new Map();
|
|
1722
|
+
if (!isRec2(stack)) return fieldsByObject;
|
|
1723
|
+
for (const obj of asArray10(stack.objects)) {
|
|
1724
|
+
const name = strName2(obj.name);
|
|
1725
|
+
if (name) fieldsByObject.set(name, declaredFieldNames(obj));
|
|
1670
1726
|
}
|
|
1671
|
-
return
|
|
1727
|
+
return fieldsByObject;
|
|
1672
1728
|
}
|
|
1673
|
-
function
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1729
|
+
function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject) {
|
|
1730
|
+
const findings = [];
|
|
1731
|
+
if (!Array.isArray(declared) || declared.length === 0) return findings;
|
|
1732
|
+
if (!objectName) return findings;
|
|
1733
|
+
if (!fieldsByObject.has(objectName)) return findings;
|
|
1734
|
+
const known = fieldsByObject.get(objectName);
|
|
1735
|
+
if (!known) return findings;
|
|
1736
|
+
for (let i = 0; i < declared.length; i++) {
|
|
1737
|
+
const entry = declared[i];
|
|
1738
|
+
const name = strName2(entry);
|
|
1677
1739
|
if (!name) continue;
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1740
|
+
if (known.has(name) || SYSTEM_FIELDS.has(name)) continue;
|
|
1741
|
+
const dotted = name.includes(".");
|
|
1742
|
+
findings.push({
|
|
1743
|
+
severity: "error",
|
|
1744
|
+
rule: SEARCHABLE_FIELD_UNKNOWN,
|
|
1745
|
+
where,
|
|
1746
|
+
path: `${path}[${i}]`,
|
|
1747
|
+
message: `${subject} entry "${name}" is not a field on object "${objectName}". The declaration is stale: searching it can never match, and the engine silently drops it \u2014 leaving a narrower search than declared, or the auto-default set once every entry is dropped.` + (dotted ? "" : suggest2(name, known)),
|
|
1748
|
+
hint: (dotted ? `'search' scans this object's own columns, so a related record's column cannot be a search target \u2014 expand the relation and search the related object, or copy the value onto a formula field here. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + `Clients echo this declaration verbatim as the '$searchFields' override, so a stale entry becomes a 400 INVALID_FIELD on list search (#4254), not just a quietly narrowed one.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
|
|
1749
|
+
});
|
|
1683
1750
|
}
|
|
1684
|
-
return
|
|
1751
|
+
return findings;
|
|
1685
1752
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
const
|
|
1690
|
-
const
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
if (!isRec(aggregate)) return;
|
|
1695
|
-
const fn = strOf(aggregate.function);
|
|
1696
|
-
const field = strOf(aggregate.field);
|
|
1697
|
-
const groupBy = aggregate.groupBy;
|
|
1698
|
-
const groupByField = strOf(groupBy) ?? (isRec(groupBy) ? strOf(groupBy.field) : void 0);
|
|
1699
|
-
if (fn && !CHART_FUNCTIONS.includes(fn)) {
|
|
1700
|
-
push(
|
|
1701
|
-
"error",
|
|
1702
|
-
REACT_CHART_AGGREGATE_INVALID,
|
|
1703
|
-
`aggregate.function "${fn}" is not an aggregation this chart can run.`,
|
|
1704
|
-
`Use one of: ${CHART_FUNCTIONS.join(", ")}.`
|
|
1753
|
+
function validateSearchableFields(stack) {
|
|
1754
|
+
const findings = [];
|
|
1755
|
+
if (!isRec2(stack)) return findings;
|
|
1756
|
+
const objects = asArray10(stack.objects);
|
|
1757
|
+
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
1758
|
+
const check = (declared, objectName, where, path, subject) => {
|
|
1759
|
+
findings.push(
|
|
1760
|
+
...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject)
|
|
1705
1761
|
);
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1762
|
+
};
|
|
1763
|
+
for (let oi = 0; oi < objects.length; oi++) {
|
|
1764
|
+
const obj = objects[oi];
|
|
1765
|
+
if (!isRec2(obj)) continue;
|
|
1766
|
+
const objName = strName2(obj.name);
|
|
1767
|
+
const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
|
|
1768
|
+
check(
|
|
1769
|
+
obj.searchableFields,
|
|
1770
|
+
objName,
|
|
1771
|
+
label2,
|
|
1772
|
+
`objects[${oi}].searchableFields`,
|
|
1773
|
+
"searchableFields"
|
|
1712
1774
|
);
|
|
1775
|
+
if (isRec2(obj.listViews)) {
|
|
1776
|
+
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
1777
|
+
if (!isRec2(lv)) continue;
|
|
1778
|
+
check(
|
|
1779
|
+
lv.searchableFields,
|
|
1780
|
+
// A built-in list view belongs to its object; an inline `data.object`
|
|
1781
|
+
// may still retarget it (ADR-0047 allows the explicit binding).
|
|
1782
|
+
listViewObject(lv) ?? objName,
|
|
1783
|
+
`${label2} \u203A listViews.${key}`,
|
|
1784
|
+
`objects[${oi}].listViews.${key}.searchableFields`,
|
|
1785
|
+
"list-view searchableFields"
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1713
1789
|
}
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
`
|
|
1725
|
-
`
|
|
1790
|
+
const views = asArray10(stack.views);
|
|
1791
|
+
for (let vi = 0; vi < views.length; vi++) {
|
|
1792
|
+
const view = views[vi];
|
|
1793
|
+
if (!isRec2(view)) continue;
|
|
1794
|
+
const viewLabel = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
|
|
1795
|
+
const viewObject = strName2(view.objectName) ?? strName2(view.object);
|
|
1796
|
+
if (isRec2(view.list)) {
|
|
1797
|
+
check(
|
|
1798
|
+
view.list.searchableFields,
|
|
1799
|
+
listViewObject(view.list) ?? viewObject,
|
|
1800
|
+
`view "${viewLabel}" \u203A list`,
|
|
1801
|
+
`views[${vi}].list.searchableFields`,
|
|
1802
|
+
"list-view searchableFields"
|
|
1726
1803
|
);
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1804
|
+
}
|
|
1805
|
+
if (isRec2(view.listViews)) {
|
|
1806
|
+
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
1807
|
+
if (!isRec2(lv)) continue;
|
|
1808
|
+
check(
|
|
1809
|
+
lv.searchableFields,
|
|
1810
|
+
listViewObject(lv) ?? viewObject,
|
|
1811
|
+
`view "${viewLabel}" \u203A listViews.${key}`,
|
|
1812
|
+
`views[${vi}].listViews.${key}.searchableFields`,
|
|
1813
|
+
"list-view searchableFields"
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1730
1817
|
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
const
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1818
|
+
return findings;
|
|
1819
|
+
}
|
|
1820
|
+
function listViewObject(listView) {
|
|
1821
|
+
const data = listView.data;
|
|
1822
|
+
return isRec2(data) ? strName2(data.object) : void 0;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// src/page-walk.ts
|
|
1826
|
+
function isRec3(v) {
|
|
1827
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1828
|
+
}
|
|
1829
|
+
function strName3(v) {
|
|
1830
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1831
|
+
}
|
|
1832
|
+
var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
|
|
1833
|
+
function isSourceAuthoredPage(page) {
|
|
1834
|
+
const kind = strName3(page.kind);
|
|
1835
|
+
return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
|
|
1836
|
+
}
|
|
1837
|
+
function walkPageComponents(page, pagePath) {
|
|
1838
|
+
const out = [];
|
|
1839
|
+
if (!isRec3(page) || isSourceAuthoredPage(page)) return out;
|
|
1840
|
+
const pageObject = strName3(page.object);
|
|
1841
|
+
const visit = (node, path, inheritedObject) => {
|
|
1842
|
+
if (!isRec3(node)) return;
|
|
1843
|
+
const props = isRec3(node.properties) ? node.properties : void 0;
|
|
1844
|
+
const dataSource = isRec3(node.dataSource) ? node.dataSource : void 0;
|
|
1845
|
+
const objectName = strName3(dataSource?.object) ?? strName3(props?.object) ?? inheritedObject;
|
|
1846
|
+
out.push({ component: node, path, objectName });
|
|
1847
|
+
if (!props) return;
|
|
1848
|
+
if (Array.isArray(props.items)) {
|
|
1849
|
+
for (let i = 0; i < props.items.length; i++) {
|
|
1850
|
+
const item = props.items[i];
|
|
1851
|
+
if (!isRec3(item) || !Array.isArray(item.children)) continue;
|
|
1852
|
+
for (let c = 0; c < item.children.length; c++) {
|
|
1853
|
+
visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
if (Array.isArray(props.children)) {
|
|
1858
|
+
for (let i = 0; i < props.children.length; i++) {
|
|
1859
|
+
visit(props.children[i], `${path}.properties.children[${i}]`, objectName);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
for (const key of ["body", "footer"]) {
|
|
1863
|
+
const slotList = props[key];
|
|
1864
|
+
if (!Array.isArray(slotList)) continue;
|
|
1865
|
+
for (let i = 0; i < slotList.length; i++) {
|
|
1866
|
+
visit(slotList[i], `${path}.properties.${key}[${i}]`, objectName);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
1871
|
+
for (let r = 0; r < regions.length; r++) {
|
|
1872
|
+
const region = regions[r];
|
|
1873
|
+
if (!isRec3(region) || !Array.isArray(region.components)) continue;
|
|
1874
|
+
for (let c = 0; c < region.components.length; c++) {
|
|
1875
|
+
visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
const slots = isRec3(page.slots) ? page.slots : void 0;
|
|
1879
|
+
if (slots) {
|
|
1880
|
+
for (const [slot, value] of Object.entries(slots)) {
|
|
1881
|
+
const list3 = Array.isArray(value) ? value : [value];
|
|
1882
|
+
const indexed = Array.isArray(value);
|
|
1883
|
+
for (let i = 0; i < list3.length; i++) {
|
|
1884
|
+
visit(list3[i], `${pagePath}.slots.${slot}${indexed ? `[${i}]` : ""}`, pageObject);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return out;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// src/validate-page-field-bindings.ts
|
|
1892
|
+
var PAGE_FIELD_UNKNOWN = "page-field-unknown";
|
|
1893
|
+
function asArray11(v) {
|
|
1894
|
+
if (Array.isArray(v)) return v;
|
|
1895
|
+
if (v && typeof v === "object") {
|
|
1896
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
1897
|
+
}
|
|
1898
|
+
return [];
|
|
1899
|
+
}
|
|
1900
|
+
function strName4(v) {
|
|
1901
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1902
|
+
}
|
|
1903
|
+
function isRec4(v) {
|
|
1904
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1905
|
+
}
|
|
1906
|
+
function fieldRefsFrom(value, basePath) {
|
|
1907
|
+
const out = [];
|
|
1908
|
+
const one = (v, path) => {
|
|
1909
|
+
const bare = strName4(v);
|
|
1910
|
+
if (bare) {
|
|
1911
|
+
out.push({ name: bare, path });
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
if (!isRec4(v)) return;
|
|
1915
|
+
const named = strName4(v.field) ?? strName4(v.name);
|
|
1916
|
+
if (named) out.push({ name: named, path: `${path}.${strName4(v.field) ? "field" : "name"}` });
|
|
1917
|
+
};
|
|
1918
|
+
if (Array.isArray(value)) {
|
|
1919
|
+
for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
|
|
1920
|
+
} else {
|
|
1921
|
+
one(value, basePath);
|
|
1922
|
+
}
|
|
1923
|
+
return out;
|
|
1924
|
+
}
|
|
1925
|
+
function sortFieldRefs(value, basePath) {
|
|
1926
|
+
if (typeof value === "string") {
|
|
1927
|
+
const head = value.trim().split(/\s+/)[0];
|
|
1928
|
+
return head ? [{ name: head, path: basePath }] : [];
|
|
1929
|
+
}
|
|
1930
|
+
return fieldRefsFrom(value, basePath);
|
|
1931
|
+
}
|
|
1932
|
+
var COMPONENT_FIELD_SPECS = {
|
|
1933
|
+
"record:highlights": { props: ["fields"] },
|
|
1934
|
+
// `sections`/`hideFields` are not in RecordDetailsProps, but every real page
|
|
1935
|
+
// authors them (they survive because `properties` is unvalidated).
|
|
1936
|
+
"record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
|
|
1937
|
+
"record:path": { props: ["statusField"] },
|
|
1938
|
+
"element:number": { props: ["field"] },
|
|
1939
|
+
"element:filter": { props: ["fields"] },
|
|
1940
|
+
"element:form": { props: ["fields"] },
|
|
1941
|
+
// The schema says `displayField`; real pages author `labelField`. Accept both.
|
|
1942
|
+
"element:record_picker": { props: ["displayField", "labelField", "searchFields"] }
|
|
1943
|
+
};
|
|
1944
|
+
var RELATED_LIST_TYPE = "record:related_list";
|
|
1945
|
+
function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
1946
|
+
const spec = COMPONENT_FIELD_SPECS[type];
|
|
1947
|
+
if (!spec) return null;
|
|
1948
|
+
const refs = [];
|
|
1949
|
+
for (const key of spec.props ?? []) {
|
|
1950
|
+
refs.push(...fieldRefsFrom(props[key], `${basePath}${sep}${key}`));
|
|
1951
|
+
}
|
|
1952
|
+
for (const key of spec.nestedSections ?? []) {
|
|
1953
|
+
const sections = Array.isArray(props[key]) ? props[key] : [];
|
|
1954
|
+
for (let si = 0; si < sections.length; si++) {
|
|
1955
|
+
const section = sections[si];
|
|
1956
|
+
if (!isRec4(section)) continue;
|
|
1957
|
+
refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
return refs;
|
|
1961
|
+
}
|
|
1962
|
+
function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
1963
|
+
const add = isRec4(props.add) ? props.add : void 0;
|
|
1964
|
+
const picker = add && isRec4(add.picker) ? add.picker : void 0;
|
|
1965
|
+
const at = (key) => `${basePath}${sep}${key}`;
|
|
1966
|
+
return {
|
|
1967
|
+
relatedObject: strName4(props.objectName),
|
|
1968
|
+
related: [
|
|
1969
|
+
...fieldRefsFrom(props.columns, at("columns")),
|
|
1970
|
+
...sortFieldRefs(props.sort, at("sort")),
|
|
1971
|
+
...fieldRefsFrom(props.filter, at("filter")),
|
|
1972
|
+
...fieldRefsFrom(props.relationshipField, at("relationshipField")),
|
|
1973
|
+
...add ? fieldRefsFrom(add.linkField, at("add.linkField")) : []
|
|
1974
|
+
],
|
|
1975
|
+
parent: fieldRefsFrom(props.relationshipValueField, at("relationshipValueField")),
|
|
1976
|
+
pickerObject: picker ? strName4(picker.object) : void 0,
|
|
1977
|
+
picker: picker ? [
|
|
1978
|
+
...fieldRefsFrom(picker.valueField, at("add.picker.valueField")),
|
|
1979
|
+
...fieldRefsFrom(picker.labelField, at("add.picker.labelField"))
|
|
1980
|
+
] : []
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
function indexObjectFields(stack) {
|
|
1984
|
+
const objectFields = /* @__PURE__ */ new Map();
|
|
1985
|
+
if (!isRec4(stack)) return objectFields;
|
|
1986
|
+
for (const obj of asArray11(stack.objects)) {
|
|
1987
|
+
const name = strName4(obj.name);
|
|
1988
|
+
if (!name) continue;
|
|
1989
|
+
const names = /* @__PURE__ */ new Set();
|
|
1990
|
+
for (const f of asArray11(obj.fields)) {
|
|
1991
|
+
const fn = strName4(f.name);
|
|
1992
|
+
if (fn) names.add(fn);
|
|
1993
|
+
}
|
|
1994
|
+
objectFields.set(name, names);
|
|
1995
|
+
}
|
|
1996
|
+
return objectFields;
|
|
1997
|
+
}
|
|
1998
|
+
function checkFieldRefs(refs, objectName, objectFields, where, consequence = "skipped") {
|
|
1999
|
+
const findings = [];
|
|
2000
|
+
if (!objectName) return findings;
|
|
2001
|
+
const known = objectFields.get(objectName);
|
|
2002
|
+
if (!known) return findings;
|
|
2003
|
+
for (const ref of refs) {
|
|
2004
|
+
if (ref.name.includes(".")) continue;
|
|
2005
|
+
if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) continue;
|
|
2006
|
+
findings.push({
|
|
2007
|
+
severity: consequence === "queried" ? "error" : "warning",
|
|
2008
|
+
rule: PAGE_FIELD_UNKNOWN,
|
|
2009
|
+
where,
|
|
2010
|
+
path: ref.path,
|
|
2011
|
+
message: `field "${ref.name}" is not a field on object "${objectName}" \u2014 ` + (consequence === "queried" ? 'it is used in a QUERY, so the predicate can never match: the surface renders an empty result that looks exactly like "there is no data".' : "the component silently skips it, so it never renders."),
|
|
2012
|
+
hint: `Fix the field name, or add "${ref.name}" to ${objectName}. References must match the object's field names exactly.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
return findings;
|
|
2016
|
+
}
|
|
2017
|
+
function validatePageFieldBindings(stack) {
|
|
2018
|
+
const findings = [];
|
|
2019
|
+
if (!stack || typeof stack !== "object") return findings;
|
|
2020
|
+
const objectFields = indexObjectFields(stack);
|
|
2021
|
+
const pages = asArray11(stack.pages);
|
|
2022
|
+
for (let pi = 0; pi < pages.length; pi++) {
|
|
2023
|
+
const page = pages[pi];
|
|
2024
|
+
if (!page || typeof page !== "object") continue;
|
|
2025
|
+
const pageName = strName4(page.name) ?? `#${pi}`;
|
|
2026
|
+
const checkRefs = (refs, objectName, where) => {
|
|
2027
|
+
findings.push(...checkFieldRefs(refs, objectName, objectFields, where));
|
|
2028
|
+
};
|
|
2029
|
+
for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2030
|
+
const type = strName4(component.type);
|
|
2031
|
+
const props = isRec4(component.properties) ? component.properties : void 0;
|
|
2032
|
+
if (!type || !props) continue;
|
|
2033
|
+
const where = `page "${pageName}" \xB7 ${type}`;
|
|
2034
|
+
const base = `${path}.properties`;
|
|
2035
|
+
if (type === RELATED_LIST_TYPE) {
|
|
2036
|
+
const split = relatedListFieldRefs(props, base);
|
|
2037
|
+
checkRefs(split.related, split.relatedObject, where);
|
|
2038
|
+
checkRefs(split.parent, objectName, where);
|
|
2039
|
+
checkRefs(split.picker, split.pickerObject, where);
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
const refs = componentFieldRefs(type, props, base);
|
|
2043
|
+
if (!refs) continue;
|
|
2044
|
+
checkRefs(refs, objectName, where);
|
|
2045
|
+
}
|
|
2046
|
+
const cfg = isRec4(page.interfaceConfig) ? page.interfaceConfig : void 0;
|
|
2047
|
+
if (cfg) {
|
|
2048
|
+
const cfgObject = strName4(cfg.source) ?? strName4(page.object);
|
|
2049
|
+
const base = `pages[${pi}].interfaceConfig`;
|
|
2050
|
+
const refs = [
|
|
2051
|
+
...fieldRefsFrom(cfg.columns, `${base}.columns`),
|
|
2052
|
+
...sortFieldRefs(cfg.sort, `${base}.sort`),
|
|
2053
|
+
...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
|
|
2054
|
+
];
|
|
2055
|
+
const userFilters = isRec4(cfg.userFilters) ? cfg.userFilters : void 0;
|
|
2056
|
+
if (userFilters) {
|
|
2057
|
+
refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
|
|
2058
|
+
}
|
|
2059
|
+
checkRefs(refs, cfgObject, `page "${pageName}" \xB7 interfaceConfig`);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
return findings;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// src/validate-react-page-props.ts
|
|
2066
|
+
var import_meta2 = {};
|
|
2067
|
+
var cachedTs = null;
|
|
2068
|
+
function loadTypeScript() {
|
|
2069
|
+
if (cachedTs) return cachedTs;
|
|
2070
|
+
const anchor = typeof import_meta2 !== "undefined" && import_meta2.url ? import_meta2.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
2071
|
+
try {
|
|
2072
|
+
cachedTs = (0, import_node_module2.createRequire)(anchor)("typescript");
|
|
2073
|
+
} catch (err) {
|
|
2074
|
+
throw new Error(
|
|
2075
|
+
`@objectstack/lint: validating a kind:'react' page requires the "typescript" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "typescript" in the image; it is only loaded when a react-source page is validated.`
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
return cachedTs;
|
|
2079
|
+
}
|
|
2080
|
+
var asArray12 = (v) => Array.isArray(v) ? v : [];
|
|
2081
|
+
var BLOCKS = new Map(
|
|
2082
|
+
import_ui2.REACT_BLOCKS.map((b) => [
|
|
2083
|
+
b.tag,
|
|
2084
|
+
{
|
|
2085
|
+
requiredBindings: b.interactions.filter((i) => i.required).map((i) => i.name),
|
|
2086
|
+
knownProps: new Set(b.interactions.map((i) => i.name))
|
|
2087
|
+
}
|
|
2088
|
+
])
|
|
2089
|
+
);
|
|
2090
|
+
function editDistance(a, b, cap = 2) {
|
|
2091
|
+
if (Math.abs(a.length - b.length) > cap) return cap + 1;
|
|
2092
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
|
|
2093
|
+
for (let j = 1; j <= b.length; j++) {
|
|
2094
|
+
let prev = dp[0];
|
|
2095
|
+
dp[0] = j;
|
|
2096
|
+
for (let i = 1; i <= a.length; i++) {
|
|
2097
|
+
const tmp = dp[i];
|
|
2098
|
+
dp[i] = Math.min(dp[i] + 1, dp[i - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
2099
|
+
prev = tmp;
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
return dp[a.length];
|
|
2103
|
+
}
|
|
2104
|
+
function nearestKnown(prop, known) {
|
|
2105
|
+
if (known.has(prop)) return null;
|
|
2106
|
+
let best = null;
|
|
2107
|
+
let bestD = 3;
|
|
2108
|
+
for (const k of known) {
|
|
2109
|
+
const d = editDistance(prop, k);
|
|
2110
|
+
if (d < bestD) {
|
|
2111
|
+
bestD = d;
|
|
2112
|
+
best = k;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
return bestD <= 2 ? best : null;
|
|
2116
|
+
}
|
|
2117
|
+
var NOT_STATIC = /* @__PURE__ */ Symbol("not-static");
|
|
2118
|
+
function staticValue(tsc, sf, node) {
|
|
2119
|
+
if (!node) return NOT_STATIC;
|
|
2120
|
+
if (tsc.isParenthesizedExpression(node)) return staticValue(tsc, sf, node.expression);
|
|
2121
|
+
if (tsc.isStringLiteral(node) || tsc.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
2122
|
+
if (tsc.isNumericLiteral(node)) return Number(node.text);
|
|
2123
|
+
if (node.kind === tsc.SyntaxKind.TrueKeyword) return true;
|
|
2124
|
+
if (node.kind === tsc.SyntaxKind.FalseKeyword) return false;
|
|
2125
|
+
if (node.kind === tsc.SyntaxKind.NullKeyword) return null;
|
|
2126
|
+
if (tsc.isArrayLiteralExpression(node)) {
|
|
2127
|
+
const out = [];
|
|
2128
|
+
for (const el of node.elements) {
|
|
2129
|
+
const v = staticValue(tsc, sf, el);
|
|
2130
|
+
if (v === NOT_STATIC) return NOT_STATIC;
|
|
2131
|
+
out.push(v);
|
|
2132
|
+
}
|
|
2133
|
+
return out;
|
|
2134
|
+
}
|
|
2135
|
+
if (tsc.isObjectLiteralExpression(node)) {
|
|
2136
|
+
const out = {};
|
|
2137
|
+
for (const p of node.properties) {
|
|
2138
|
+
if (!tsc.isPropertyAssignment(p)) return NOT_STATIC;
|
|
2139
|
+
const key = tsc.isIdentifier(p.name) || tsc.isStringLiteral(p.name) ? p.name.text : null;
|
|
2140
|
+
if (key === null) return NOT_STATIC;
|
|
2141
|
+
const v = staticValue(tsc, sf, p.initializer);
|
|
2142
|
+
if (v === NOT_STATIC) return NOT_STATIC;
|
|
2143
|
+
out[key] = v;
|
|
2144
|
+
}
|
|
2145
|
+
return out;
|
|
2146
|
+
}
|
|
2147
|
+
return NOT_STATIC;
|
|
2148
|
+
}
|
|
2149
|
+
function attrValue(tsc, sf, attr) {
|
|
2150
|
+
const init = attr.initializer;
|
|
2151
|
+
if (!init) return true;
|
|
2152
|
+
if (tsc.isStringLiteral(init)) return init.text;
|
|
2153
|
+
if (tsc.isJsxExpression(init)) return staticValue(tsc, sf, init.expression);
|
|
2154
|
+
return NOT_STATIC;
|
|
2155
|
+
}
|
|
2156
|
+
function filterAttrValue(tsc, sf, attr) {
|
|
2157
|
+
const init = attr.initializer;
|
|
2158
|
+
if (!init || !tsc.isJsxExpression(init)) return NOT_STATIC;
|
|
2159
|
+
const perPosition = (node) => {
|
|
2160
|
+
if (!node) return NOT_STATIC;
|
|
2161
|
+
if (tsc.isParenthesizedExpression(node)) return perPosition(node.expression);
|
|
2162
|
+
if (tsc.isArrayLiteralExpression(node)) return node.elements.map((el) => perPosition(el));
|
|
2163
|
+
return staticValue(tsc, sf, node);
|
|
2164
|
+
};
|
|
2165
|
+
return perPosition(init.expression);
|
|
2166
|
+
}
|
|
2167
|
+
var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
2168
|
+
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
2169
|
+
var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
2170
|
+
var CHART_FUNCTIONS = ["count", "sum", "avg", "min", "max"];
|
|
2171
|
+
var isRec5 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2172
|
+
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2173
|
+
function checkObjectChart(attrs, objectFields, findings) {
|
|
2174
|
+
const { values, where, path } = attrs;
|
|
2175
|
+
const push = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
|
|
2176
|
+
if (values.has("data")) return;
|
|
2177
|
+
const aggregate = values.get("aggregate");
|
|
2178
|
+
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
2179
|
+
if (!isRec5(aggregate)) return;
|
|
2180
|
+
const fn = strOf(aggregate.function);
|
|
2181
|
+
const field = strOf(aggregate.field);
|
|
2182
|
+
const groupBy = aggregate.groupBy;
|
|
2183
|
+
const groupByField = strOf(groupBy) ?? (isRec5(groupBy) ? strOf(groupBy.field) : void 0);
|
|
2184
|
+
if (fn && !CHART_FUNCTIONS.includes(fn)) {
|
|
2185
|
+
push(
|
|
2186
|
+
"error",
|
|
2187
|
+
REACT_CHART_AGGREGATE_INVALID,
|
|
2188
|
+
`aggregate.function "${fn}" is not an aggregation this chart can run.`,
|
|
2189
|
+
`Use one of: ${CHART_FUNCTIONS.join(", ")}.`
|
|
2190
|
+
);
|
|
2191
|
+
} else if (fn && fn !== "count" && !field) {
|
|
2192
|
+
push(
|
|
2193
|
+
"error",
|
|
2194
|
+
REACT_CHART_AGGREGATE_INVALID,
|
|
2195
|
+
`aggregate.function "${fn}" has no "field" to aggregate.`,
|
|
2196
|
+
'Add aggregate.field, or use function "count" (the only one that may omit it).'
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
const objectName = strOf(values.get("objectName"));
|
|
2200
|
+
const known = objectName ? objectFields.get(objectName) : void 0;
|
|
2201
|
+
if (objectName && known) {
|
|
2202
|
+
const fieldRef = (name, prop) => {
|
|
2203
|
+
if (!name) return;
|
|
2204
|
+
if (name.includes(".")) return;
|
|
2205
|
+
if (known.has(name) || SYSTEM_FIELDS.has(name)) return;
|
|
2206
|
+
push(
|
|
2207
|
+
"error",
|
|
2208
|
+
REACT_CHART_FIELD_UNKNOWN,
|
|
2209
|
+
`aggregate.${prop} "${name}" is not a field on object "${objectName}" \u2014 the aggregate query has nothing to ${prop === "groupBy" ? "group by" : "aggregate"}, so the chart comes back empty.`,
|
|
2210
|
+
`Fix the field name, or add "${name}" to ${objectName}.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
|
|
2211
|
+
);
|
|
2212
|
+
};
|
|
2213
|
+
fieldRef(field, "field");
|
|
2214
|
+
fieldRef(groupByField, "groupBy");
|
|
2215
|
+
}
|
|
2216
|
+
const keys = (0, import_ui2.chartAggregateResultKeys)({ field, function: fn, groupBy });
|
|
2217
|
+
const columns = [keys.category, keys.value].filter((k) => !!k);
|
|
2218
|
+
if (columns.length === 0) return;
|
|
2219
|
+
const axisRef = (name, prop) => {
|
|
2220
|
+
if (!name) return;
|
|
2221
|
+
if (columns.includes(name)) return;
|
|
2222
|
+
if (keys.comparison && name === keys.comparison) return;
|
|
2223
|
+
push(
|
|
2224
|
+
"error",
|
|
2225
|
+
REACT_CHART_AXIS_UNKNOWN,
|
|
1741
2226
|
`"${name}" is not a column this aggregate returns, so the axis plots nothing. Object-bound aggregate rows are keyed by the RAW FIELD NAMES (unlike a dataset, whose rows are keyed by measure name).`,
|
|
1742
2227
|
`Result columns: ${columns.join(", ")}` + (keys.comparison ? ` (plus "${keys.comparison}" with a comparison overlay)` : "") + `. Bind ${prop} to one of them.`
|
|
1743
2228
|
);
|
|
1744
2229
|
};
|
|
1745
2230
|
const xAxisRaw = values.get("xAxis");
|
|
1746
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
2231
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec5(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
1747
2232
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
1748
2233
|
axisRef(categoryAxis, categoryProp);
|
|
1749
2234
|
const yAxisRaw = values.get("yAxis");
|
|
1750
2235
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
1751
2236
|
for (const a of yAxisList) {
|
|
1752
|
-
axisRef(strOf(a) ?? (
|
|
2237
|
+
axisRef(strOf(a) ?? (isRec5(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
1753
2238
|
}
|
|
1754
2239
|
const series = values.get("series");
|
|
1755
2240
|
if (Array.isArray(series)) {
|
|
1756
2241
|
for (const s of series) {
|
|
1757
|
-
if (!
|
|
2242
|
+
if (!isRec5(s)) continue;
|
|
1758
2243
|
const dataKey = strOf(s.dataKey);
|
|
1759
2244
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
1760
2245
|
}
|
|
@@ -1768,10 +2253,156 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
1768
2253
|
);
|
|
1769
2254
|
}
|
|
1770
2255
|
}
|
|
2256
|
+
var REACT_FIELD_SPECS = {
|
|
2257
|
+
ListView: {
|
|
2258
|
+
// `fields` is the React overlay's "limit/order the columns"; `columns` the
|
|
2259
|
+
// spec ListView prop. Both name columns on the bound object, and a page may
|
|
2260
|
+
// write either. `hiddenFields`/`fieldOrder`/`filterableFields` are schema
|
|
2261
|
+
// props outside the curated contract — unadvertised but honored by the
|
|
2262
|
+
// renderer, so a stale name there is drift just the same.
|
|
2263
|
+
fields: ["fields", "columns", "hiddenFields", "fieldOrder", "filterableFields"],
|
|
2264
|
+
sorts: ["sort"],
|
|
2265
|
+
nestedFields: ["userFilters", "grouping"],
|
|
2266
|
+
filterArrays: ["filters"]
|
|
2267
|
+
},
|
|
2268
|
+
ObjectForm: {
|
|
2269
|
+
fields: ["fields"],
|
|
2270
|
+
keyedByField: ["initialValues"],
|
|
2271
|
+
// `groups` is FormViewSchema's legacy alias for `sections`.
|
|
2272
|
+
sections: ["sections", "groups"]
|
|
2273
|
+
},
|
|
2274
|
+
ObjectChart: {
|
|
2275
|
+
// The axes are result columns (checkObjectChart owns them); `filter` is an
|
|
2276
|
+
// ordinary ObjectQL predicate over the bound object, like ListView's.
|
|
2277
|
+
filterArrays: ["filter"]
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
var PATH_SEP = " \u203A ";
|
|
2281
|
+
var SCHEMA_TYPE_BY_TAG = new Map(
|
|
2282
|
+
import_ui2.REACT_BLOCKS.map((b) => [b.tag, b.schemaType])
|
|
2283
|
+
);
|
|
2284
|
+
var FILTER_PROPS = new Set(
|
|
2285
|
+
Object.values(REACT_FIELD_SPECS).flatMap((s) => s.filterArrays ?? [])
|
|
2286
|
+
);
|
|
2287
|
+
function readableProps(values) {
|
|
2288
|
+
const out = {};
|
|
2289
|
+
for (const [k, v] of values) if (v !== NOT_STATIC) out[k] = v;
|
|
2290
|
+
return out;
|
|
2291
|
+
}
|
|
2292
|
+
function subformFieldRefs(value, basePath) {
|
|
2293
|
+
const child = [];
|
|
2294
|
+
const parent = [];
|
|
2295
|
+
if (!Array.isArray(value)) return { child, parent };
|
|
2296
|
+
for (let i = 0; i < value.length; i++) {
|
|
2297
|
+
const sub = value[i];
|
|
2298
|
+
if (!isRec5(sub)) continue;
|
|
2299
|
+
const at = (key) => `${basePath}[${i}].${key}`;
|
|
2300
|
+
child.push({
|
|
2301
|
+
objectName: strOf(sub.childObject),
|
|
2302
|
+
refs: [
|
|
2303
|
+
...fieldRefsFrom(sub.columns, at("columns")),
|
|
2304
|
+
...fieldRefsFrom(sub.relationshipField, at("relationshipField")),
|
|
2305
|
+
...fieldRefsFrom(sub.amountField, at("amountField"))
|
|
2306
|
+
]
|
|
2307
|
+
});
|
|
2308
|
+
parent.push(...fieldRefsFrom(sub.totalField, at("totalField")));
|
|
2309
|
+
}
|
|
2310
|
+
return { child, parent };
|
|
2311
|
+
}
|
|
2312
|
+
function filterFieldRefs(node, basePath, out) {
|
|
2313
|
+
if (!Array.isArray(node) || node.length === 0) return;
|
|
2314
|
+
const head = node[0];
|
|
2315
|
+
if (typeof head === "string" && (head.toLowerCase() === "and" || head.toLowerCase() === "or")) {
|
|
2316
|
+
for (let i = 1; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
if (Array.isArray(head)) {
|
|
2320
|
+
for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data3.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
|
|
2324
|
+
out.push({ name: head, path: `${basePath}[0]` });
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
function reactFieldRefs(spec, values, basePath) {
|
|
2328
|
+
const own = [];
|
|
2329
|
+
const queried = [];
|
|
2330
|
+
const readable = (key) => {
|
|
2331
|
+
const v = values.get(key);
|
|
2332
|
+
return v === NOT_STATIC ? void 0 : v;
|
|
2333
|
+
};
|
|
2334
|
+
const at = (key) => `${basePath}${PATH_SEP}${key}`;
|
|
2335
|
+
for (const key of spec.fields ?? []) {
|
|
2336
|
+
own.push(...fieldRefsFrom(readable(key), at(key)));
|
|
2337
|
+
}
|
|
2338
|
+
for (const key of spec.sorts ?? []) {
|
|
2339
|
+
own.push(...sortFieldRefs(readable(key), at(key)));
|
|
2340
|
+
}
|
|
2341
|
+
for (const key of spec.nestedFields ?? []) {
|
|
2342
|
+
const v = readable(key);
|
|
2343
|
+
if (isRec5(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
2344
|
+
}
|
|
2345
|
+
for (const key of spec.sections ?? []) {
|
|
2346
|
+
const v = readable(key);
|
|
2347
|
+
if (!Array.isArray(v)) continue;
|
|
2348
|
+
for (let i = 0; i < v.length; i++) {
|
|
2349
|
+
const section = v[i];
|
|
2350
|
+
if (!isRec5(section)) continue;
|
|
2351
|
+
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
for (const key of spec.keyedByField ?? []) {
|
|
2355
|
+
const v = readable(key);
|
|
2356
|
+
if (!isRec5(v)) continue;
|
|
2357
|
+
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
2358
|
+
}
|
|
2359
|
+
for (const key of spec.filterArrays ?? []) {
|
|
2360
|
+
filterFieldRefs(values.get(key), at(key), queried);
|
|
2361
|
+
}
|
|
2362
|
+
return { own, queried };
|
|
2363
|
+
}
|
|
2364
|
+
function checkBlockFieldProps(tag, values, objectFields, where, path) {
|
|
2365
|
+
const objectName = strOf(values.get("objectName"));
|
|
2366
|
+
const out = [];
|
|
2367
|
+
const spec = REACT_FIELD_SPECS[tag];
|
|
2368
|
+
if (spec) {
|
|
2369
|
+
const { own, queried } = reactFieldRefs(spec, values, path);
|
|
2370
|
+
out.push(...checkFieldRefs(own, objectName, objectFields, where));
|
|
2371
|
+
out.push(...checkFieldRefs(queried, objectName, objectFields, where, "queried"));
|
|
2372
|
+
}
|
|
2373
|
+
if (tag === "ObjectForm") {
|
|
2374
|
+
const raw = values.get("subforms");
|
|
2375
|
+
const subs = subformFieldRefs(raw === NOT_STATIC ? void 0 : raw, `${path}${PATH_SEP}subforms`);
|
|
2376
|
+
for (const sub of subs.child) {
|
|
2377
|
+
out.push(...checkFieldRefs(sub.refs, sub.objectName, objectFields, where));
|
|
2378
|
+
}
|
|
2379
|
+
out.push(...checkFieldRefs(subs.parent, objectName, objectFields, where));
|
|
2380
|
+
}
|
|
2381
|
+
const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
|
|
2382
|
+
if (schemaType) {
|
|
2383
|
+
const props = readableProps(values);
|
|
2384
|
+
if (schemaType === RELATED_LIST_TYPE) {
|
|
2385
|
+
const split = relatedListFieldRefs(props, path, PATH_SEP);
|
|
2386
|
+
out.push(...checkFieldRefs(split.related, split.relatedObject, objectFields, where));
|
|
2387
|
+
out.push(...checkFieldRefs(split.picker, split.pickerObject, objectFields, where));
|
|
2388
|
+
} else if (COMPONENT_FIELD_SPECS[schemaType]) {
|
|
2389
|
+
out.push(
|
|
2390
|
+
...checkFieldRefs(
|
|
2391
|
+
componentFieldRefs(schemaType, props, path, PATH_SEP) ?? [],
|
|
2392
|
+
objectName,
|
|
2393
|
+
objectFields,
|
|
2394
|
+
where
|
|
2395
|
+
)
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
return out;
|
|
2400
|
+
}
|
|
1771
2401
|
function validateReactPageProps(stack) {
|
|
1772
2402
|
const findings = [];
|
|
1773
2403
|
const objectFields = indexObjectFields(stack);
|
|
1774
|
-
const
|
|
2404
|
+
const searchTargets = indexObjectSearchTargets(stack);
|
|
2405
|
+
const pages = asArray12(stack.pages);
|
|
1775
2406
|
for (let p = 0; p < pages.length; p++) {
|
|
1776
2407
|
const page = pages[p];
|
|
1777
2408
|
if (!page || page.kind !== "react") continue;
|
|
@@ -1801,7 +2432,10 @@ function validateReactPageProps(stack) {
|
|
|
1801
2432
|
if (tsc.isJsxAttribute(a)) {
|
|
1802
2433
|
const propName = a.name.getText(sf);
|
|
1803
2434
|
used.add(propName);
|
|
1804
|
-
values.set(
|
|
2435
|
+
values.set(
|
|
2436
|
+
propName,
|
|
2437
|
+
FILTER_PROPS.has(propName) ? filterAttrValue(tsc, sf, a) : attrValue(tsc, sf, a)
|
|
2438
|
+
);
|
|
1805
2439
|
}
|
|
1806
2440
|
}
|
|
1807
2441
|
const where = `page "${name}" \u203A <${tag}>`;
|
|
@@ -1836,6 +2470,23 @@ function validateReactPageProps(stack) {
|
|
|
1836
2470
|
if (tag === "ObjectChart" && !hasSpread) {
|
|
1837
2471
|
checkObjectChart({ values, where, path }, objectFields, findings);
|
|
1838
2472
|
}
|
|
2473
|
+
if (tag === "ListView" && !hasSpread) {
|
|
2474
|
+
findings.push(
|
|
2475
|
+
...checkSearchableFieldList(
|
|
2476
|
+
values.get("searchableFields"),
|
|
2477
|
+
strOf(values.get("objectName")),
|
|
2478
|
+
searchTargets,
|
|
2479
|
+
where,
|
|
2480
|
+
`${path} \u203A searchableFields`,
|
|
2481
|
+
"searchableFields"
|
|
2482
|
+
)
|
|
2483
|
+
);
|
|
2484
|
+
}
|
|
2485
|
+
if (!hasSpread) {
|
|
2486
|
+
findings.push(
|
|
2487
|
+
...checkBlockFieldProps(tag, values, objectFields, where, path)
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
1839
2490
|
}
|
|
1840
2491
|
}
|
|
1841
2492
|
tsc.forEachChild(node, visit);
|
|
@@ -1847,11 +2498,11 @@ function validateReactPageProps(stack) {
|
|
|
1847
2498
|
|
|
1848
2499
|
// src/validate-page-source-styling.ts
|
|
1849
2500
|
var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
|
|
1850
|
-
var
|
|
2501
|
+
var asArray13 = (v) => Array.isArray(v) ? v : [];
|
|
1851
2502
|
var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
|
|
1852
2503
|
function validatePageSourceStyling(stack) {
|
|
1853
2504
|
const findings = [];
|
|
1854
|
-
const pages =
|
|
2505
|
+
const pages = asArray13(stack.pages);
|
|
1855
2506
|
for (let p = 0; p < pages.length; p++) {
|
|
1856
2507
|
const page = pages[p];
|
|
1857
2508
|
if (!page) continue;
|
|
@@ -1877,10 +2528,10 @@ function validatePageSourceStyling(stack) {
|
|
|
1877
2528
|
}
|
|
1878
2529
|
|
|
1879
2530
|
// src/validate-record-title.ts
|
|
1880
|
-
var
|
|
2531
|
+
var import_data4 = require("@objectstack/spec/data");
|
|
1881
2532
|
var TITLE_FORMAT_RETIRED = "title-format-retired";
|
|
1882
2533
|
var TITLE_UNRESOLVABLE = "title-unresolvable";
|
|
1883
|
-
function
|
|
2534
|
+
function asArray14(v) {
|
|
1884
2535
|
if (Array.isArray(v)) return v;
|
|
1885
2536
|
if (v && typeof v === "object") {
|
|
1886
2537
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1889,7 +2540,7 @@ function asArray12(v) {
|
|
|
1889
2540
|
}
|
|
1890
2541
|
function validateRecordTitle(stack) {
|
|
1891
2542
|
const findings = [];
|
|
1892
|
-
const objects =
|
|
2543
|
+
const objects = asArray14(stack.objects);
|
|
1893
2544
|
for (let i = 0; i < objects.length; i++) {
|
|
1894
2545
|
const obj = objects[i];
|
|
1895
2546
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
@@ -1905,7 +2556,7 @@ function validateRecordTitle(stack) {
|
|
|
1905
2556
|
hint: `titleFormat is a render-only template the server cannot return or query, and an explicit nameField now takes precedence. For a single-field title set nameField: '<field>'. For a composite title, add a formula field (returnType: 'text') and designate it via nameField.`
|
|
1906
2557
|
});
|
|
1907
2558
|
}
|
|
1908
|
-
const completeness = (0,
|
|
2559
|
+
const completeness = (0, import_data4.objectTitleCompleteness)(obj);
|
|
1909
2560
|
if (completeness.status === "none") {
|
|
1910
2561
|
findings.push({
|
|
1911
2562
|
severity: "warning",
|
|
@@ -1925,7 +2576,7 @@ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
|
|
|
1925
2576
|
var FIELD_GROUP_EMPTY = "field-group-empty";
|
|
1926
2577
|
var FIELD_GROUP_SHADOWED = "field-group-shadowed";
|
|
1927
2578
|
var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
|
|
1928
|
-
function
|
|
2579
|
+
function asArray15(v) {
|
|
1929
2580
|
if (Array.isArray(v)) return v;
|
|
1930
2581
|
if (v && typeof v === "object") {
|
|
1931
2582
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1934,7 +2585,7 @@ function asArray13(v) {
|
|
|
1934
2585
|
}
|
|
1935
2586
|
function validateSemanticRoles(stack) {
|
|
1936
2587
|
const findings = [];
|
|
1937
|
-
const objects =
|
|
2588
|
+
const objects = asArray15(stack.objects);
|
|
1938
2589
|
for (let i = 0; i < objects.length; i++) {
|
|
1939
2590
|
const obj = objects[i];
|
|
1940
2591
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -2029,7 +2680,7 @@ function validateSemanticRoles(stack) {
|
|
|
2029
2680
|
// src/validate-form-layout.ts
|
|
2030
2681
|
var FORM_FIELD_UNKNOWN = "form-field-unknown";
|
|
2031
2682
|
var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
|
|
2032
|
-
function
|
|
2683
|
+
function asArray16(v) {
|
|
2033
2684
|
if (Array.isArray(v)) return v;
|
|
2034
2685
|
if (v && typeof v === "object") {
|
|
2035
2686
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -2054,13 +2705,13 @@ function boundObject(view) {
|
|
|
2054
2705
|
function validateFormLayout(stack) {
|
|
2055
2706
|
const findings = [];
|
|
2056
2707
|
const objectFields = /* @__PURE__ */ new Map();
|
|
2057
|
-
for (const obj of
|
|
2708
|
+
for (const obj of asArray16(stack.objects)) {
|
|
2058
2709
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
2059
2710
|
if (!name) continue;
|
|
2060
2711
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
|
|
2061
2712
|
objectFields.set(name, new Set(fields));
|
|
2062
2713
|
}
|
|
2063
|
-
const views =
|
|
2714
|
+
const views = asArray16(stack.views);
|
|
2064
2715
|
for (let i = 0; i < views.length; i++) {
|
|
2065
2716
|
const view = views[i];
|
|
2066
2717
|
if (!view || typeof view !== "object") continue;
|
|
@@ -2110,7 +2761,7 @@ var VISIBILITY_ALIAS_DEPRECATED = "visibility-alias-deprecated";
|
|
|
2110
2761
|
var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
|
|
2111
2762
|
var CANONICAL = "visibleWhen";
|
|
2112
2763
|
var ALIASES = ["visibleOn", "visibility"];
|
|
2113
|
-
function
|
|
2764
|
+
function asArray17(v) {
|
|
2114
2765
|
if (Array.isArray(v)) return v;
|
|
2115
2766
|
if (v && typeof v === "object") {
|
|
2116
2767
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -2172,7 +2823,7 @@ function isFieldObject(entry) {
|
|
|
2172
2823
|
function validateVisibilityPredicates(stack, opts = {}) {
|
|
2173
2824
|
const layer = opts.layer ?? "runtime";
|
|
2174
2825
|
const findings = [];
|
|
2175
|
-
const views =
|
|
2826
|
+
const views = asArray17(stack.views);
|
|
2176
2827
|
for (let i = 0; i < views.length; i++) {
|
|
2177
2828
|
const view = views[i];
|
|
2178
2829
|
if (!view || typeof view !== "object") continue;
|
|
@@ -2195,7 +2846,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
|
|
|
2195
2846
|
}
|
|
2196
2847
|
}
|
|
2197
2848
|
}
|
|
2198
|
-
const pages =
|
|
2849
|
+
const pages = asArray17(stack.pages);
|
|
2199
2850
|
for (let i = 0; i < pages.length; i++) {
|
|
2200
2851
|
const page = pages[i];
|
|
2201
2852
|
if (!page || typeof page !== "object") continue;
|
|
@@ -2219,7 +2870,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
|
|
|
2219
2870
|
// src/validate-capability-references.ts
|
|
2220
2871
|
var import_security = require("@objectstack/spec/security");
|
|
2221
2872
|
var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
|
|
2222
|
-
function
|
|
2873
|
+
function asArray18(v) {
|
|
2223
2874
|
if (Array.isArray(v)) return v;
|
|
2224
2875
|
if (v && typeof v === "object") {
|
|
2225
2876
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -2244,13 +2895,13 @@ function validateCapabilityReferences(stack) {
|
|
|
2244
2895
|
const findings = [];
|
|
2245
2896
|
if (!stack || typeof stack !== "object") return findings;
|
|
2246
2897
|
const known = new Set(import_security.PLATFORM_CAPABILITY_NAMES);
|
|
2247
|
-
for (const cap of
|
|
2898
|
+
for (const cap of asArray18(stack.capabilities)) {
|
|
2248
2899
|
if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
|
|
2249
2900
|
}
|
|
2250
|
-
for (const ps of
|
|
2901
|
+
for (const ps of asArray18(stack.permissions)) {
|
|
2251
2902
|
for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
|
|
2252
2903
|
}
|
|
2253
|
-
for (const seed of
|
|
2904
|
+
for (const seed of asArray18(stack.data)) {
|
|
2254
2905
|
if (seed.object !== "sys_capability") continue;
|
|
2255
2906
|
for (const rec of Array.isArray(seed.records) ? seed.records : []) {
|
|
2256
2907
|
const name = rec?.name;
|
|
@@ -2269,7 +2920,7 @@ function validateCapabilityReferences(stack) {
|
|
|
2269
2920
|
hint
|
|
2270
2921
|
});
|
|
2271
2922
|
};
|
|
2272
|
-
const objects =
|
|
2923
|
+
const objects = asArray18(stack.objects);
|
|
2273
2924
|
for (let i = 0; i < objects.length; i++) {
|
|
2274
2925
|
const obj = objects[i];
|
|
2275
2926
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -2278,27 +2929,27 @@ function validateCapabilityReferences(stack) {
|
|
|
2278
2929
|
for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
|
|
2279
2930
|
flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
|
|
2280
2931
|
}
|
|
2281
|
-
const fields =
|
|
2932
|
+
const fields = asArray18(obj.fields);
|
|
2282
2933
|
for (const f of fields) {
|
|
2283
2934
|
const fname = typeof f.name === "string" ? f.name : "(field)";
|
|
2284
2935
|
for (const cap of asCapArray(f.requiredPermissions)) {
|
|
2285
2936
|
flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
|
|
2286
2937
|
}
|
|
2287
2938
|
}
|
|
2288
|
-
for (const [ai, action] of
|
|
2939
|
+
for (const [ai, action] of asArray18(obj.actions).entries()) {
|
|
2289
2940
|
const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
|
|
2290
2941
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
2291
2942
|
flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
|
|
2292
2943
|
}
|
|
2293
2944
|
}
|
|
2294
2945
|
}
|
|
2295
|
-
for (const [i, action] of
|
|
2946
|
+
for (const [i, action] of asArray18(stack.actions).entries()) {
|
|
2296
2947
|
const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
|
|
2297
2948
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
2298
2949
|
flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
|
|
2299
2950
|
}
|
|
2300
2951
|
}
|
|
2301
|
-
const apps =
|
|
2952
|
+
const apps = asArray18(stack.apps);
|
|
2302
2953
|
for (let i = 0; i < apps.length; i++) {
|
|
2303
2954
|
const app = apps[i];
|
|
2304
2955
|
if (!app || typeof app !== "object") continue;
|
|
@@ -2325,7 +2976,7 @@ function validateCapabilityReferences(stack) {
|
|
|
2325
2976
|
}
|
|
2326
2977
|
|
|
2327
2978
|
// src/validate-approval-approvers.ts
|
|
2328
|
-
var
|
|
2979
|
+
var import_automation3 = require("@objectstack/spec/automation");
|
|
2329
2980
|
var import_spec = require("@objectstack/spec");
|
|
2330
2981
|
var import_formula2 = require("@objectstack/formula");
|
|
2331
2982
|
var APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
|
|
@@ -2347,7 +2998,7 @@ var TYPE_FIX = {
|
|
|
2347
2998
|
business_unit: "department",
|
|
2348
2999
|
bu: "department"
|
|
2349
3000
|
};
|
|
2350
|
-
function
|
|
3001
|
+
function asArray19(v) {
|
|
2351
3002
|
if (Array.isArray(v)) return v;
|
|
2352
3003
|
if (v && typeof v === "object") {
|
|
2353
3004
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -2357,16 +3008,16 @@ function asArray17(v) {
|
|
|
2357
3008
|
function validateApprovalApprovers(stack) {
|
|
2358
3009
|
const findings = [];
|
|
2359
3010
|
if (!stack || typeof stack !== "object") return findings;
|
|
2360
|
-
const flows =
|
|
2361
|
-
const validTypes = new Set(
|
|
3011
|
+
const flows = asArray19(stack.flows);
|
|
3012
|
+
const validTypes = new Set(import_automation3.ApproverType.options);
|
|
2362
3013
|
for (let fi = 0; fi < flows.length; fi++) {
|
|
2363
3014
|
const flow = flows[fi];
|
|
2364
3015
|
if (!flow || typeof flow !== "object") continue;
|
|
2365
3016
|
const flowName = typeof flow.name === "string" ? flow.name : `(flow ${fi})`;
|
|
2366
|
-
const
|
|
2367
|
-
for (let ni = 0; ni <
|
|
2368
|
-
const node =
|
|
2369
|
-
if (!node || node.type !==
|
|
3017
|
+
const walked = walkFlowNodes(flow, `flows[${fi}]`);
|
|
3018
|
+
for (let ni = 0; ni < walked.length; ni++) {
|
|
3019
|
+
const { node, path: nodePath } = walked[ni];
|
|
3020
|
+
if (!node || node.type !== import_automation3.APPROVAL_NODE_TYPE) continue;
|
|
2370
3021
|
const nodeId = typeof node.id === "string" ? node.id : `(node ${ni})`;
|
|
2371
3022
|
const cfg = node.config ?? {};
|
|
2372
3023
|
const approvers = Array.isArray(cfg.approvers) ? cfg.approvers : [];
|
|
@@ -2376,7 +3027,7 @@ function validateApprovalApprovers(stack) {
|
|
|
2376
3027
|
if (!a || typeof a !== "object") continue;
|
|
2377
3028
|
const type = typeof a.type === "string" ? a.type : "";
|
|
2378
3029
|
const value = typeof a.value === "string" ? a.value : "";
|
|
2379
|
-
const path =
|
|
3030
|
+
const path = `${nodePath}.config.approvers[${ai}]`;
|
|
2380
3031
|
if (type && !validTypes.has(type)) {
|
|
2381
3032
|
const fix = TYPE_FIX[type];
|
|
2382
3033
|
findings.push({
|
|
@@ -2384,12 +3035,12 @@ function validateApprovalApprovers(stack) {
|
|
|
2384
3035
|
rule: APPROVAL_APPROVER_TYPE_UNKNOWN,
|
|
2385
3036
|
where,
|
|
2386
3037
|
path: `${path}.type`,
|
|
2387
|
-
message: `approver type '${type}' is not an ApproverType (${
|
|
3038
|
+
message: `approver type '${type}' is not an ApproverType (${import_automation3.ApproverType.options.join(" | ")}).`,
|
|
2388
3039
|
hint: fix ? `Use the spec value: { type: '${fix}', value: '${value}' }.` : `Pick one of the spec values; unmapped types degrade to an inert '${type}:${value}' literal at runtime.`
|
|
2389
3040
|
});
|
|
2390
3041
|
continue;
|
|
2391
3042
|
}
|
|
2392
|
-
const canonical = (0,
|
|
3043
|
+
const canonical = (0, import_automation3.canonicalApproverType)(type);
|
|
2393
3044
|
if (canonical === "expression") {
|
|
2394
3045
|
const source = value.trim();
|
|
2395
3046
|
if (!source) {
|
|
@@ -2446,8 +3097,8 @@ function validateApprovalApprovers(stack) {
|
|
|
2446
3097
|
message: `approver { type: '${type}', value: '${value}' } resolves against the better-auth org-membership tier (sys_member.role: ${MEMBERSHIP_TIER_LIST}) \u2014 '${value}' is not a membership tier, so this approver matches nobody and the request stalls.`,
|
|
2447
3098
|
hint: `If '${value}' is an org position, author { type: 'position', value: '${value}' } (resolved via sys_user_position, ADR-0090 D3). Keep type 'org_membership_level' only for membership tiers (${MEMBERSHIP_TIER_LIST}) \u2014 the vocabulary is closed (ADR-0108), so a business role is always a position.`
|
|
2448
3099
|
});
|
|
2449
|
-
} else if (type in
|
|
2450
|
-
const fix = (0,
|
|
3100
|
+
} else if (type in import_automation3.DEPRECATED_APPROVER_TYPES) {
|
|
3101
|
+
const fix = (0, import_automation3.canonicalApproverType)(type);
|
|
2451
3102
|
findings.push({
|
|
2452
3103
|
severity: "warning",
|
|
2453
3104
|
rule: APPROVAL_APPROVER_TYPE_DEPRECATED,
|
|
@@ -2456,7 +3107,7 @@ function validateApprovalApprovers(stack) {
|
|
|
2456
3107
|
message: `approver type '${type}' is the deprecated spelling of '${fix}' (ADR-0090 D3) and is removed in the next major.`,
|
|
2457
3108
|
hint: `Author { type: '${fix}', value: '${value}' }. It resolves identically today.`
|
|
2458
3109
|
});
|
|
2459
|
-
} else if (
|
|
3110
|
+
} else if (import_automation3.APPROVER_VALUE_BINDINGS[canonical]?.source === "unsupported") {
|
|
2460
3111
|
findings.push({
|
|
2461
3112
|
severity: "warning",
|
|
2462
3113
|
rule: APPROVAL_APPROVER_TYPE_UNSUPPORTED,
|
|
@@ -2467,7 +3118,7 @@ function validateApprovalApprovers(stack) {
|
|
|
2467
3118
|
});
|
|
2468
3119
|
}
|
|
2469
3120
|
const declaredOrg = a.organization;
|
|
2470
|
-
if (typeof declaredOrg === "string" && declaredOrg.trim() !== "" &&
|
|
3121
|
+
if (typeof declaredOrg === "string" && declaredOrg.trim() !== "" && import_automation3.ApproverType.options.includes(canonical) && !(0, import_automation3.approverTypeIsOrgScoped)(canonical)) {
|
|
2471
3122
|
findings.push({
|
|
2472
3123
|
severity: "error",
|
|
2473
3124
|
rule: APPROVAL_APPROVER_CROSS_ORG_UNSUPPORTED,
|
|
@@ -2481,38 +3132,38 @@ function validateApprovalApprovers(stack) {
|
|
|
2481
3132
|
const routable = approvers.filter(
|
|
2482
3133
|
(a) => a && typeof a === "object" && typeof a.type === "string"
|
|
2483
3134
|
);
|
|
2484
|
-
if (routable.length > 0 && routable.every((a) => GROUP_ROUTED_TYPES.has((0,
|
|
3135
|
+
if (routable.length > 0 && routable.every((a) => GROUP_ROUTED_TYPES.has((0, import_automation3.canonicalApproverType)(String(a.type))))) {
|
|
2485
3136
|
const locks = cfg.lockRecord !== false;
|
|
2486
3137
|
findings.push({
|
|
2487
3138
|
severity: "info",
|
|
2488
3139
|
rule: APPROVAL_APPROVERS_MAY_RESOLVE_EMPTY,
|
|
2489
3140
|
where,
|
|
2490
|
-
path:
|
|
3141
|
+
path: `${nodePath}.config.approvers`,
|
|
2491
3142
|
message: `every approver on this node routes to a group (position/team/department) whose members are runtime data \u2014 if none is staffed, the request resolves to an empty slate and waits forever` + (locks ? `, and (lockRecord) the record stays locked with no in-product recovery.` : `.`),
|
|
2492
3143
|
hint: `Make sure at least one target is always staffed, or add a guaranteed-staffed fallback approver, e.g. { type: 'org_membership_level', value: 'owner' }. A request that still lands empty is recoverable only by a platform/tenant admin override (#3424).`
|
|
2493
3144
|
});
|
|
2494
3145
|
}
|
|
2495
3146
|
const hasExpression = approvers.some(
|
|
2496
|
-
(a) => a && typeof a === "object" && (0,
|
|
3147
|
+
(a) => a && typeof a === "object" && (0, import_automation3.canonicalApproverType)(String(a.type ?? "")) === "expression"
|
|
2497
3148
|
);
|
|
2498
3149
|
if (hasExpression && cfg.onEmptyApprovers == null) {
|
|
2499
3150
|
findings.push({
|
|
2500
3151
|
severity: "info",
|
|
2501
3152
|
rule: APPROVAL_EXPRESSION_NO_EMPTY_POLICY,
|
|
2502
3153
|
where,
|
|
2503
|
-
path:
|
|
3154
|
+
path: `${nodePath}.config`,
|
|
2504
3155
|
message: `this node resolves approvers from an expression but declares no onEmptyApprovers \u2014 an empty result falls back to the default ('admin_rescue': request opens, only a privileged admin can act).`,
|
|
2505
3156
|
hint: `Declare the empty-slate policy explicitly: onEmptyApprovers: 'admin_rescue' (hold for admin takeover), 'fail' (fail the node \u2014 config bug), or 'auto_approve' (wave through, output.autoApproved = true).`
|
|
2506
3157
|
});
|
|
2507
3158
|
}
|
|
2508
|
-
const declaredOutputs = (0,
|
|
3159
|
+
const declaredOutputs = (0, import_automation3.normalizeDecisionOutputs)(cfg.decisionOutputs).map((d) => d.key);
|
|
2509
3160
|
const reserved = declaredOutputs.filter((k) => RESERVED_OUTPUT_KEYS.has(k));
|
|
2510
3161
|
if (reserved.length) {
|
|
2511
3162
|
findings.push({
|
|
2512
3163
|
severity: "error",
|
|
2513
3164
|
rule: APPROVAL_DECISION_OUTPUTS_RESERVED,
|
|
2514
3165
|
where,
|
|
2515
|
-
path:
|
|
3166
|
+
path: `${nodePath}.config.decisionOutputs`,
|
|
2516
3167
|
message: `decisionOutputs declares reserved key(s) \`${reserved.join("`, `")}\` \u2014 the resume envelope owns them, so every decide carrying them is rejected.`,
|
|
2517
3168
|
hint: `Rename the output key(s); any name other than 'decision'/'requestId' works.`
|
|
2518
3169
|
});
|
|
@@ -2525,7 +3176,7 @@ function validateApprovalApprovers(stack) {
|
|
|
2525
3176
|
severity: "warning",
|
|
2526
3177
|
rule: APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
|
|
2527
3178
|
where,
|
|
2528
|
-
path:
|
|
3179
|
+
path: `${nodePath}.config.escalation.escalateTo`,
|
|
2529
3180
|
message: `escalation.action is 'reassign' but escalateTo is empty \u2014 at runtime the escalation degrades to a notify and the request stays with the original approvers.`,
|
|
2530
3181
|
hint: `Set escalateTo to a position machine name (expanded via sys_user_position, ADR-0090 D3) or a specific user id, or change action to 'notify'.`
|
|
2531
3182
|
});
|
|
@@ -2652,7 +3303,7 @@ var OWD_WIDTH = {
|
|
|
2652
3303
|
public_read: 1,
|
|
2653
3304
|
public_read_write: 2
|
|
2654
3305
|
};
|
|
2655
|
-
function
|
|
3306
|
+
function asArray20(v) {
|
|
2656
3307
|
if (Array.isArray(v)) return v;
|
|
2657
3308
|
if (v && typeof v === "object") {
|
|
2658
3309
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -2678,7 +3329,7 @@ function refOf(def) {
|
|
|
2678
3329
|
return typeof r === "string" && r ? r : void 0;
|
|
2679
3330
|
}
|
|
2680
3331
|
function firstMasterDetailField(obj) {
|
|
2681
|
-
for (const f of
|
|
3332
|
+
for (const f of asArray20(obj.fields)) {
|
|
2682
3333
|
if (f.type === "master_detail") {
|
|
2683
3334
|
return { name: String(f.name ?? "?"), parent: refOf(f) };
|
|
2684
3335
|
}
|
|
@@ -2691,8 +3342,8 @@ function grantsObjectAccess(p) {
|
|
|
2691
3342
|
function validateSecurityPosture(stack, opts) {
|
|
2692
3343
|
const findings = [];
|
|
2693
3344
|
if (!stack || typeof stack !== "object") return findings;
|
|
2694
|
-
const objects =
|
|
2695
|
-
const permissionSets =
|
|
3345
|
+
const objects = asArray20(stack.objects);
|
|
3346
|
+
const permissionSets = asArray20(stack.permissions);
|
|
2696
3347
|
for (let i = 0; i < objects.length; i++) {
|
|
2697
3348
|
const obj = objects[i];
|
|
2698
3349
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -2821,10 +3472,10 @@ function validateSecurityPosture(stack, opts) {
|
|
|
2821
3472
|
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
2822
3473
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
2823
3474
|
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
2824
|
-
for (const f of
|
|
3475
|
+
for (const f of asArray20(obj.fields)) {
|
|
2825
3476
|
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
2826
3477
|
}
|
|
2827
|
-
for (const [ai, action] of
|
|
3478
|
+
for (const [ai, action] of asArray20(obj.actions).entries()) {
|
|
2828
3479
|
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
2829
3480
|
}
|
|
2830
3481
|
}
|
|
@@ -2833,19 +3484,19 @@ function validateSecurityPosture(stack, opts) {
|
|
|
2833
3484
|
if (!ps || typeof ps !== "object") continue;
|
|
2834
3485
|
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
2835
3486
|
}
|
|
2836
|
-
for (const [i, pos] of
|
|
3487
|
+
for (const [i, pos] of asArray20(stack.positions).entries()) {
|
|
2837
3488
|
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
2838
3489
|
}
|
|
2839
|
-
for (const [i, app] of
|
|
3490
|
+
for (const [i, app] of asArray20(stack.apps).entries()) {
|
|
2840
3491
|
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
2841
3492
|
}
|
|
2842
|
-
for (const [i, book] of
|
|
3493
|
+
for (const [i, book] of asArray20(stack.books).entries()) {
|
|
2843
3494
|
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
2844
3495
|
}
|
|
2845
3496
|
const stackSetNames = new Set(
|
|
2846
3497
|
permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
|
|
2847
3498
|
);
|
|
2848
|
-
for (const [i, book] of
|
|
3499
|
+
for (const [i, book] of asArray20(stack.books).entries()) {
|
|
2849
3500
|
const audience = book.audience;
|
|
2850
3501
|
if (!audience || typeof audience !== "object") continue;
|
|
2851
3502
|
const setName = audience.permissionSet;
|
|
@@ -2923,7 +3574,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
2923
3574
|
}
|
|
2924
3575
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
2925
3576
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
2926
|
-
for (const [i, seed] of
|
|
3577
|
+
for (const [i, seed] of asArray20(stack.data).entries()) {
|
|
2927
3578
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
2928
3579
|
if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
|
|
2929
3580
|
const records = Array.isArray(seed.records) ? seed.records : [];
|
|
@@ -2967,7 +3618,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
2967
3618
|
var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
|
|
2968
3619
|
var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
|
|
2969
3620
|
var ORG_PARENT_FIELD = "parent_organization_id";
|
|
2970
|
-
function
|
|
3621
|
+
function asArray21(v) {
|
|
2971
3622
|
if (Array.isArray(v)) return v;
|
|
2972
3623
|
if (v && typeof v === "object") {
|
|
2973
3624
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -2988,9 +3639,9 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
|
|
|
2988
3639
|
function validateOrgAxisRedLines(stack) {
|
|
2989
3640
|
const findings = [];
|
|
2990
3641
|
const cfg = stack ?? {};
|
|
2991
|
-
const permissionSets =
|
|
3642
|
+
const permissionSets = asArray21(cfg.permissions ?? cfg.permissionSets);
|
|
2992
3643
|
permissionSets.forEach((ps, psIndex) => {
|
|
2993
|
-
|
|
3644
|
+
asArray21(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
2994
3645
|
for (const clause of ["using", "check"]) {
|
|
2995
3646
|
if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
|
|
2996
3647
|
findings.push({
|
|
@@ -3004,10 +3655,10 @@ function validateOrgAxisRedLines(stack) {
|
|
|
3004
3655
|
}
|
|
3005
3656
|
});
|
|
3006
3657
|
});
|
|
3007
|
-
const objects =
|
|
3658
|
+
const objects = asArray21(cfg.objects);
|
|
3008
3659
|
objects.forEach((object, oIndex) => {
|
|
3009
3660
|
const objectName = str(object.name) || String(oIndex);
|
|
3010
|
-
|
|
3661
|
+
asArray21(object.rowLevelSecurity ?? object.rls).forEach((policy, pIndex) => {
|
|
3011
3662
|
for (const clause of ["using", "check"]) {
|
|
3012
3663
|
if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
|
|
3013
3664
|
findings.push({
|
|
@@ -3021,7 +3672,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
3021
3672
|
}
|
|
3022
3673
|
});
|
|
3023
3674
|
});
|
|
3024
|
-
|
|
3675
|
+
asArray21(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
|
|
3025
3676
|
const criteria = JSON.stringify(rule.criteria ?? rule.filter ?? "");
|
|
3026
3677
|
const sharedTo = JSON.stringify(rule.sharedTo ?? rule.recipient ?? "");
|
|
3027
3678
|
if (criteria.includes(ORG_PARENT_FIELD) || sharedTo.includes(ORG_PARENT_FIELD)) {
|
|
@@ -3038,7 +3689,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
3038
3689
|
const tenancyDisabledObjects = new Set(
|
|
3039
3690
|
objects.filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
|
|
3040
3691
|
);
|
|
3041
|
-
|
|
3692
|
+
asArray21(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
|
|
3042
3693
|
const target = str(rule.object ?? rule.objectName);
|
|
3043
3694
|
if (!target || !tenancyDisabledObjects.has(target)) return;
|
|
3044
3695
|
const sharedTo = rule.sharedTo ?? rule.recipient;
|
|
@@ -3059,14 +3710,14 @@ function validateOrgAxisRedLines(stack) {
|
|
|
3059
3710
|
// src/validate-dashboard-action-refs.ts
|
|
3060
3711
|
var DASHBOARD_ACTION_TARGET_UNDEFINED = "dashboard-action-target-undefined";
|
|
3061
3712
|
var DASHBOARD_ACTION_ROUTE_UNRESOLVED = "dashboard-action-route-unresolved";
|
|
3062
|
-
function
|
|
3713
|
+
function asArray22(v) {
|
|
3063
3714
|
if (Array.isArray(v)) return v;
|
|
3064
3715
|
if (v && typeof v === "object") {
|
|
3065
3716
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3066
3717
|
}
|
|
3067
3718
|
return [];
|
|
3068
3719
|
}
|
|
3069
|
-
function
|
|
3720
|
+
function strName5(v) {
|
|
3070
3721
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3071
3722
|
}
|
|
3072
3723
|
var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
|
|
@@ -3083,7 +3734,7 @@ var URL_COLLECTION_TO_STACK_KEY = {
|
|
|
3083
3734
|
views: "views"
|
|
3084
3735
|
};
|
|
3085
3736
|
function viewContainerName(item) {
|
|
3086
|
-
return
|
|
3737
|
+
return strName5(item.name) ?? strName5(item.id) ?? strName5(item.object) ?? strName5(item.list?.data && item.list.data.object) ?? strName5(item.form?.data && item.form.data.object);
|
|
3087
3738
|
}
|
|
3088
3739
|
function collectKnownTargets(stack) {
|
|
3089
3740
|
const actions = /* @__PURE__ */ new Set();
|
|
@@ -3093,22 +3744,22 @@ function collectKnownTargets(stack) {
|
|
|
3093
3744
|
const pages = /* @__PURE__ */ new Set();
|
|
3094
3745
|
const views = /* @__PURE__ */ new Set();
|
|
3095
3746
|
const collectNames = (v, into, name) => {
|
|
3096
|
-
for (const item of
|
|
3747
|
+
for (const item of asArray22(v)) {
|
|
3097
3748
|
if (!item || typeof item !== "object") continue;
|
|
3098
3749
|
const n = name(item);
|
|
3099
3750
|
if (n) into.add(n);
|
|
3100
3751
|
}
|
|
3101
3752
|
};
|
|
3102
|
-
collectNames(stack.actions, actions, (a) =>
|
|
3103
|
-
for (const obj of
|
|
3753
|
+
collectNames(stack.actions, actions, (a) => strName5(a.name));
|
|
3754
|
+
for (const obj of asArray22(stack.objects)) {
|
|
3104
3755
|
if (!obj || typeof obj !== "object") continue;
|
|
3105
|
-
const n =
|
|
3756
|
+
const n = strName5(obj.name);
|
|
3106
3757
|
if (n) objects.add(n);
|
|
3107
|
-
collectNames(obj.actions, actions, (a) =>
|
|
3758
|
+
collectNames(obj.actions, actions, (a) => strName5(a.name));
|
|
3108
3759
|
}
|
|
3109
|
-
collectNames(stack.reports, reports, (r) =>
|
|
3110
|
-
collectNames(stack.dashboards, dashboards, (d) =>
|
|
3111
|
-
collectNames(stack.pages, pages, (p) =>
|
|
3760
|
+
collectNames(stack.reports, reports, (r) => strName5(r.name));
|
|
3761
|
+
collectNames(stack.dashboards, dashboards, (d) => strName5(d.name));
|
|
3762
|
+
collectNames(stack.pages, pages, (p) => strName5(p.name));
|
|
3112
3763
|
collectNames(stack.views, views, viewContainerName);
|
|
3113
3764
|
for (const o of objects) views.add(o);
|
|
3114
3765
|
return { actions, objects, reports, dashboards, pages, views };
|
|
@@ -3140,14 +3791,14 @@ function resolveUrlRoute(target, known) {
|
|
|
3140
3791
|
function validateDashboardActionRefs(stack) {
|
|
3141
3792
|
const findings = [];
|
|
3142
3793
|
if (!stack || typeof stack !== "object") return findings;
|
|
3143
|
-
const dashboards =
|
|
3794
|
+
const dashboards = asArray22(stack.dashboards);
|
|
3144
3795
|
if (dashboards.length === 0) return findings;
|
|
3145
3796
|
const known = collectKnownTargets(stack);
|
|
3146
3797
|
const checkOne = (action, where, path) => {
|
|
3147
|
-
const target =
|
|
3798
|
+
const target = strName5(action.actionUrl);
|
|
3148
3799
|
if (!target) return;
|
|
3149
3800
|
if (target.includes("${")) return;
|
|
3150
|
-
const actionType =
|
|
3801
|
+
const actionType = strName5(action.actionType) ?? "url";
|
|
3151
3802
|
if (actionType === "script" || actionType === "modal") {
|
|
3152
3803
|
if (resolveActionTarget(actionType, target, known)) return;
|
|
3153
3804
|
const kindWord = actionType === "script" ? "script" : "modal";
|
|
@@ -3178,25 +3829,25 @@ function validateDashboardActionRefs(stack) {
|
|
|
3178
3829
|
for (let di = 0; di < dashboards.length; di++) {
|
|
3179
3830
|
const dash = dashboards[di];
|
|
3180
3831
|
if (!dash || typeof dash !== "object") continue;
|
|
3181
|
-
const dashName =
|
|
3832
|
+
const dashName = strName5(dash.name) ?? `(dashboard ${di})`;
|
|
3182
3833
|
const dashPath = `dashboards[${di}]`;
|
|
3183
|
-
const headerActions =
|
|
3834
|
+
const headerActions = asArray22(dash.header?.actions);
|
|
3184
3835
|
for (let ai = 0; ai < headerActions.length; ai++) {
|
|
3185
3836
|
const action = headerActions[ai];
|
|
3186
3837
|
if (!action || typeof action !== "object") continue;
|
|
3187
|
-
const label2 =
|
|
3838
|
+
const label2 = strName5(action.label) ?? strName5(action.actionUrl) ?? `#${ai}`;
|
|
3188
3839
|
checkOne(
|
|
3189
3840
|
action,
|
|
3190
3841
|
`dashboard "${dashName}" \xB7 header action "${label2}"`,
|
|
3191
3842
|
`${dashPath}.header.actions[${ai}].actionUrl`
|
|
3192
3843
|
);
|
|
3193
3844
|
}
|
|
3194
|
-
const widgets =
|
|
3845
|
+
const widgets = asArray22(dash.widgets);
|
|
3195
3846
|
for (let wi = 0; wi < widgets.length; wi++) {
|
|
3196
3847
|
const widget = widgets[wi];
|
|
3197
3848
|
if (!widget || typeof widget !== "object") continue;
|
|
3198
|
-
if (!
|
|
3199
|
-
const widgetId =
|
|
3849
|
+
if (!strName5(widget.actionUrl)) continue;
|
|
3850
|
+
const widgetId = strName5(widget.id) ?? `#${wi}`;
|
|
3200
3851
|
checkOne(
|
|
3201
3852
|
{ actionType: widget.actionType, actionUrl: widget.actionUrl },
|
|
3202
3853
|
`dashboard "${dashName}" \xB7 widget "${widgetId}" action`,
|
|
@@ -3208,10 +3859,10 @@ function validateDashboardActionRefs(stack) {
|
|
|
3208
3859
|
}
|
|
3209
3860
|
|
|
3210
3861
|
// src/validate-filter-tokens.ts
|
|
3211
|
-
var
|
|
3862
|
+
var import_data5 = require("@objectstack/spec/data");
|
|
3212
3863
|
var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
|
|
3213
3864
|
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
3214
|
-
function
|
|
3865
|
+
function asArray23(v) {
|
|
3215
3866
|
if (Array.isArray(v)) return v;
|
|
3216
3867
|
if (v && typeof v === "object") {
|
|
3217
3868
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -3221,11 +3872,11 @@ function asArray21(v) {
|
|
|
3221
3872
|
function label(v, fallback) {
|
|
3222
3873
|
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
3223
3874
|
}
|
|
3224
|
-
var KNOWN_LIST =
|
|
3875
|
+
var KNOWN_LIST = import_data5.CONTEXT_TOKENS.join("}, {");
|
|
3225
3876
|
function walkFilterValues(node, path, where, out, seen) {
|
|
3226
3877
|
if (node === null || node === void 0) return;
|
|
3227
3878
|
if (typeof node === "string") {
|
|
3228
|
-
const cls = (0,
|
|
3879
|
+
const cls = (0, import_data5.classifyFilterToken)(node);
|
|
3229
3880
|
if (cls?.kind === "unknown") {
|
|
3230
3881
|
const suggestion = cls.suggestion;
|
|
3231
3882
|
out.push({
|
|
@@ -3280,7 +3931,7 @@ function validateFilterTokens(stack) {
|
|
|
3280
3931
|
["apps", "app"]
|
|
3281
3932
|
];
|
|
3282
3933
|
for (const [key, kind] of surfaces) {
|
|
3283
|
-
const items =
|
|
3934
|
+
const items = asArray23(stack[key]);
|
|
3284
3935
|
items.forEach((item, i) => {
|
|
3285
3936
|
const name = label(item.name ?? item.id, `#${i}`);
|
|
3286
3937
|
if (kind === "dashboard") {
|
|
@@ -3306,18 +3957,18 @@ function validateFilterTokens(stack) {
|
|
|
3306
3957
|
}
|
|
3307
3958
|
|
|
3308
3959
|
// src/validate-object-references.ts
|
|
3309
|
-
var
|
|
3310
|
-
var PLATFORM_NAMES = [...
|
|
3960
|
+
var import_system2 = require("@objectstack/spec/system");
|
|
3961
|
+
var PLATFORM_NAMES = [...import_system2.PLATFORM_PROVIDED_OBJECT_NAMES];
|
|
3311
3962
|
var OBJECT_REFERENCE_UNKNOWN = "object-reference-unknown";
|
|
3312
3963
|
var OBJECT_REFERENCE_UNREGISTERED_PLATFORM = "object-reference-unregistered-platform";
|
|
3313
|
-
function
|
|
3964
|
+
function asArray24(v) {
|
|
3314
3965
|
if (Array.isArray(v)) return v;
|
|
3315
3966
|
if (v && typeof v === "object") {
|
|
3316
3967
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3317
3968
|
}
|
|
3318
3969
|
return [];
|
|
3319
3970
|
}
|
|
3320
|
-
function
|
|
3971
|
+
function strName6(v) {
|
|
3321
3972
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3322
3973
|
}
|
|
3323
3974
|
function isInterpolated(target) {
|
|
@@ -3325,11 +3976,11 @@ function isInterpolated(target) {
|
|
|
3325
3976
|
const open = target.indexOf("{");
|
|
3326
3977
|
return open !== -1 && target.indexOf("}", open + 2) !== -1;
|
|
3327
3978
|
}
|
|
3328
|
-
function
|
|
3979
|
+
function suggest3(target, known) {
|
|
3329
3980
|
let best;
|
|
3330
3981
|
let bestScore = Infinity;
|
|
3331
3982
|
for (const candidate of known) {
|
|
3332
|
-
const d =
|
|
3983
|
+
const d = distance2(target, candidate);
|
|
3333
3984
|
if (d < bestScore) {
|
|
3334
3985
|
bestScore = d;
|
|
3335
3986
|
best = candidate;
|
|
@@ -3338,7 +3989,7 @@ function suggest2(target, known) {
|
|
|
3338
3989
|
const limit = Math.max(2, Math.floor(target.length / 3));
|
|
3339
3990
|
return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
|
|
3340
3991
|
}
|
|
3341
|
-
function
|
|
3992
|
+
function distance2(a, b) {
|
|
3342
3993
|
const m = a.length;
|
|
3343
3994
|
const n = b.length;
|
|
3344
3995
|
if (m === 0) return n;
|
|
@@ -3357,25 +4008,25 @@ function distance(a, b) {
|
|
|
3357
4008
|
function validateObjectReferences(stack) {
|
|
3358
4009
|
const findings = [];
|
|
3359
4010
|
if (!stack || typeof stack !== "object") return findings;
|
|
3360
|
-
const objects =
|
|
4011
|
+
const objects = asArray24(stack.objects);
|
|
3361
4012
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
3362
4013
|
for (const obj of objects) {
|
|
3363
|
-
const n =
|
|
4014
|
+
const n = strName6(obj.name);
|
|
3364
4015
|
if (n) ownObjects.add(n);
|
|
3365
4016
|
}
|
|
3366
4017
|
const check = (target, where, path, subject, fix) => {
|
|
3367
|
-
const name =
|
|
4018
|
+
const name = strName6(target);
|
|
3368
4019
|
if (!name) return;
|
|
3369
4020
|
if (isInterpolated(name)) return;
|
|
3370
4021
|
if (ownObjects.has(name)) return;
|
|
3371
|
-
if ((0,
|
|
3372
|
-
if ((0,
|
|
4022
|
+
if ((0, import_system2.isPlatformProvidedObjectName)(name)) return;
|
|
4023
|
+
if ((0, import_system2.hasPlatformObjectPrefix)(name)) {
|
|
3373
4024
|
findings.push({
|
|
3374
4025
|
severity: "warning",
|
|
3375
4026
|
rule: OBJECT_REFERENCE_UNREGISTERED_PLATFORM,
|
|
3376
4027
|
where,
|
|
3377
4028
|
path,
|
|
3378
|
-
message: `${subject} "${name}" carries a platform namespace prefix, but no platform package, official plugin, or cloud runtime object registers that name \u2014 and this stack does not define it either. If nothing provides it at runtime the reference resolves to nothing and fails silently.` +
|
|
4029
|
+
message: `${subject} "${name}" carries a platform namespace prefix, but no platform package, official plugin, or cloud runtime object registers that name \u2014 and this stack does not define it either. If nothing provides it at runtime the reference resolves to nothing and fails silently.` + suggest3(name, PLATFORM_NAMES),
|
|
3379
4030
|
hint: `Check the spelling against the object the providing package actually registers (e.g. "sys_approval_request", not "sys_approval_process" \u2014 the process object was removed when approval became a flow node, ADR-0019). If a third-party package genuinely provides it, this warning is expected. ${fix}`
|
|
3380
4031
|
});
|
|
3381
4032
|
return;
|
|
@@ -3385,26 +4036,26 @@ function validateObjectReferences(stack) {
|
|
|
3385
4036
|
rule: OBJECT_REFERENCE_UNKNOWN,
|
|
3386
4037
|
where,
|
|
3387
4038
|
path,
|
|
3388
|
-
message: `${subject} "${name}" resolves to no object defined in this stack. The reference is inert at runtime \u2014 nothing reports the miss.` +
|
|
4039
|
+
message: `${subject} "${name}" resolves to no object defined in this stack. The reference is inert at runtime \u2014 nothing reports the miss.` + suggest3(name, ownObjects),
|
|
3389
4040
|
hint: `Point it at one of this stack's objects, or at a platform object by its full name (the platform user object is "sys_user", not "user"). ${fix}` + (ownObjects.size > 0 ? ` Defined objects: ${[...ownObjects].sort().join(", ")}.` : "")
|
|
3390
4041
|
});
|
|
3391
4042
|
};
|
|
3392
4043
|
const checkActionParams2 = (action, actionPath, actionLabel) => {
|
|
3393
|
-
const params =
|
|
4044
|
+
const params = asArray24(action.params);
|
|
3394
4045
|
for (let pi = 0; pi < params.length; pi++) {
|
|
3395
4046
|
const param = params[pi];
|
|
3396
4047
|
if (!param || typeof param !== "object") continue;
|
|
3397
|
-
const paramLabel =
|
|
4048
|
+
const paramLabel = strName6(param.name) ?? strName6(param.field) ?? `#${pi}`;
|
|
3398
4049
|
const where = `${actionLabel} \xB7 param "${paramLabel}"`;
|
|
3399
4050
|
check(
|
|
3400
|
-
|
|
4051
|
+
strName6(param.reference),
|
|
3401
4052
|
where,
|
|
3402
4053
|
`${actionPath}.params[${pi}].reference`,
|
|
3403
4054
|
"record-picker target",
|
|
3404
4055
|
"Without a resolvable target the picker degrades to a raw record-id text input."
|
|
3405
4056
|
);
|
|
3406
4057
|
check(
|
|
3407
|
-
|
|
4058
|
+
strName6(param.objectOverride),
|
|
3408
4059
|
where,
|
|
3409
4060
|
`${actionPath}.params[${pi}].objectOverride`,
|
|
3410
4061
|
"field-backed param object",
|
|
@@ -3412,70 +4063,70 @@ function validateObjectReferences(stack) {
|
|
|
3412
4063
|
);
|
|
3413
4064
|
}
|
|
3414
4065
|
};
|
|
3415
|
-
const globalActions =
|
|
4066
|
+
const globalActions = asArray24(stack.actions);
|
|
3416
4067
|
for (let ai = 0; ai < globalActions.length; ai++) {
|
|
3417
4068
|
const action = globalActions[ai];
|
|
3418
4069
|
if (!action || typeof action !== "object") continue;
|
|
3419
|
-
checkActionParams2(action, `actions[${ai}]`, `action "${
|
|
4070
|
+
checkActionParams2(action, `actions[${ai}]`, `action "${strName6(action.name) ?? `#${ai}`}"`);
|
|
3420
4071
|
}
|
|
3421
4072
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
3422
4073
|
const obj = objects[oi];
|
|
3423
4074
|
if (!obj || typeof obj !== "object") continue;
|
|
3424
|
-
const objName =
|
|
3425
|
-
const objActions =
|
|
4075
|
+
const objName = strName6(obj.name) ?? `#${oi}`;
|
|
4076
|
+
const objActions = asArray24(obj.actions);
|
|
3426
4077
|
for (let ai = 0; ai < objActions.length; ai++) {
|
|
3427
4078
|
const action = objActions[ai];
|
|
3428
4079
|
if (!action || typeof action !== "object") continue;
|
|
3429
4080
|
checkActionParams2(
|
|
3430
4081
|
action,
|
|
3431
4082
|
`objects[${oi}].actions[${ai}]`,
|
|
3432
|
-
`object "${objName}" \xB7 action "${
|
|
4083
|
+
`object "${objName}" \xB7 action "${strName6(action.name) ?? `#${ai}`}"`
|
|
3433
4084
|
);
|
|
3434
4085
|
}
|
|
3435
4086
|
}
|
|
3436
|
-
const dashboards =
|
|
4087
|
+
const dashboards = asArray24(stack.dashboards);
|
|
3437
4088
|
for (let di = 0; di < dashboards.length; di++) {
|
|
3438
4089
|
const dash = dashboards[di];
|
|
3439
4090
|
if (!dash || typeof dash !== "object") continue;
|
|
3440
|
-
const dashName =
|
|
3441
|
-
const filters =
|
|
4091
|
+
const dashName = strName6(dash.name) ?? `#${di}`;
|
|
4092
|
+
const filters = asArray24(dash.globalFilters);
|
|
3442
4093
|
for (let fi = 0; fi < filters.length; fi++) {
|
|
3443
4094
|
const filter = filters[fi];
|
|
3444
4095
|
if (!filter || typeof filter !== "object") continue;
|
|
3445
4096
|
const optionsFrom = filter.optionsFrom;
|
|
3446
4097
|
if (!optionsFrom || typeof optionsFrom !== "object") continue;
|
|
3447
4098
|
check(
|
|
3448
|
-
|
|
3449
|
-
`dashboard "${dashName}" \xB7 filter "${
|
|
4099
|
+
strName6(optionsFrom.object),
|
|
4100
|
+
`dashboard "${dashName}" \xB7 filter "${strName6(filter.name) ?? `#${fi}`}"`,
|
|
3450
4101
|
`dashboards[${di}].globalFilters[${fi}].optionsFrom.object`,
|
|
3451
4102
|
"filter options source",
|
|
3452
4103
|
"The dropdown fetches its options from this object; an unknown one renders an always-empty filter."
|
|
3453
4104
|
);
|
|
3454
4105
|
}
|
|
3455
4106
|
}
|
|
3456
|
-
const apps =
|
|
4107
|
+
const apps = asArray24(stack.apps);
|
|
3457
4108
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
3458
4109
|
const app = apps[ai];
|
|
3459
4110
|
if (!app || typeof app !== "object") continue;
|
|
3460
|
-
const appName =
|
|
4111
|
+
const appName = strName6(app.name) ?? `#${ai}`;
|
|
3461
4112
|
const walkNav = (items, basePath) => {
|
|
3462
|
-
const navItems =
|
|
4113
|
+
const navItems = asArray24(items);
|
|
3463
4114
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
3464
4115
|
const nav = navItems[ni];
|
|
3465
4116
|
if (!nav || typeof nav !== "object") continue;
|
|
3466
|
-
const navId =
|
|
4117
|
+
const navId = strName6(nav.id) ?? `#${ni}`;
|
|
3467
4118
|
const where = `app "${appName}" \xB7 nav "${navId}"`;
|
|
3468
4119
|
const navPath = `${basePath}[${ni}]`;
|
|
3469
4120
|
check(
|
|
3470
|
-
|
|
4121
|
+
strName6(nav.requiresObject),
|
|
3471
4122
|
where,
|
|
3472
4123
|
`${navPath}.requiresObject`,
|
|
3473
4124
|
"capability gate object",
|
|
3474
4125
|
"The entry is hidden unless this object is registered, so a typo hides it permanently \u2014 and it suppresses the nav cross-reference check that would have caught the target."
|
|
3475
4126
|
);
|
|
3476
|
-
if (nav.requiresObject &&
|
|
4127
|
+
if (nav.requiresObject && strName6(nav.objectName)) {
|
|
3477
4128
|
check(
|
|
3478
|
-
|
|
4129
|
+
strName6(nav.objectName),
|
|
3479
4130
|
where,
|
|
3480
4131
|
`${navPath}.objectName`,
|
|
3481
4132
|
"navigation target",
|
|
@@ -3486,7 +4137,7 @@ function validateObjectReferences(stack) {
|
|
|
3486
4137
|
}
|
|
3487
4138
|
};
|
|
3488
4139
|
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
3489
|
-
const areas =
|
|
4140
|
+
const areas = asArray24(app.areas);
|
|
3490
4141
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
3491
4142
|
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
3492
4143
|
}
|
|
@@ -3494,88 +4145,22 @@ function validateObjectReferences(stack) {
|
|
|
3494
4145
|
return findings;
|
|
3495
4146
|
}
|
|
3496
4147
|
|
|
3497
|
-
// src/page-walk.ts
|
|
3498
|
-
function isRec2(v) {
|
|
3499
|
-
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3500
|
-
}
|
|
3501
|
-
function strName3(v) {
|
|
3502
|
-
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3503
|
-
}
|
|
3504
|
-
var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
|
|
3505
|
-
function isSourceAuthoredPage(page) {
|
|
3506
|
-
const kind = strName3(page.kind);
|
|
3507
|
-
return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
|
|
3508
|
-
}
|
|
3509
|
-
function walkPageComponents(page, pagePath) {
|
|
3510
|
-
const out = [];
|
|
3511
|
-
if (!isRec2(page) || isSourceAuthoredPage(page)) return out;
|
|
3512
|
-
const pageObject = strName3(page.object);
|
|
3513
|
-
const visit = (node, path, inheritedObject) => {
|
|
3514
|
-
if (!isRec2(node)) return;
|
|
3515
|
-
const props = isRec2(node.properties) ? node.properties : void 0;
|
|
3516
|
-
const dataSource = isRec2(node.dataSource) ? node.dataSource : void 0;
|
|
3517
|
-
const objectName = strName3(dataSource?.object) ?? strName3(props?.object) ?? inheritedObject;
|
|
3518
|
-
out.push({ component: node, path, objectName });
|
|
3519
|
-
if (!props) return;
|
|
3520
|
-
if (Array.isArray(props.items)) {
|
|
3521
|
-
for (let i = 0; i < props.items.length; i++) {
|
|
3522
|
-
const item = props.items[i];
|
|
3523
|
-
if (!isRec2(item) || !Array.isArray(item.children)) continue;
|
|
3524
|
-
for (let c = 0; c < item.children.length; c++) {
|
|
3525
|
-
visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
|
|
3526
|
-
}
|
|
3527
|
-
}
|
|
3528
|
-
}
|
|
3529
|
-
if (Array.isArray(props.children)) {
|
|
3530
|
-
for (let i = 0; i < props.children.length; i++) {
|
|
3531
|
-
visit(props.children[i], `${path}.properties.children[${i}]`, objectName);
|
|
3532
|
-
}
|
|
3533
|
-
}
|
|
3534
|
-
for (const key of ["body", "footer"]) {
|
|
3535
|
-
const slotList = props[key];
|
|
3536
|
-
if (!Array.isArray(slotList)) continue;
|
|
3537
|
-
for (let i = 0; i < slotList.length; i++) {
|
|
3538
|
-
visit(slotList[i], `${path}.properties.${key}[${i}]`, objectName);
|
|
3539
|
-
}
|
|
3540
|
-
}
|
|
3541
|
-
};
|
|
3542
|
-
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
3543
|
-
for (let r = 0; r < regions.length; r++) {
|
|
3544
|
-
const region = regions[r];
|
|
3545
|
-
if (!isRec2(region) || !Array.isArray(region.components)) continue;
|
|
3546
|
-
for (let c = 0; c < region.components.length; c++) {
|
|
3547
|
-
visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
|
|
3548
|
-
}
|
|
3549
|
-
}
|
|
3550
|
-
const slots = isRec2(page.slots) ? page.slots : void 0;
|
|
3551
|
-
if (slots) {
|
|
3552
|
-
for (const [slot, value] of Object.entries(slots)) {
|
|
3553
|
-
const list3 = Array.isArray(value) ? value : [value];
|
|
3554
|
-
const indexed = Array.isArray(value);
|
|
3555
|
-
for (let i = 0; i < list3.length; i++) {
|
|
3556
|
-
visit(list3[i], `${pagePath}.slots.${slot}${indexed ? `[${i}]` : ""}`, pageObject);
|
|
3557
|
-
}
|
|
3558
|
-
}
|
|
3559
|
-
}
|
|
3560
|
-
return out;
|
|
3561
|
-
}
|
|
3562
|
-
|
|
3563
4148
|
// src/validate-action-name-refs.ts
|
|
3564
4149
|
var ACTION_NAME_UNDEFINED = "action-name-undefined";
|
|
3565
|
-
function
|
|
4150
|
+
function asArray25(v) {
|
|
3566
4151
|
if (Array.isArray(v)) return v;
|
|
3567
4152
|
if (v && typeof v === "object") {
|
|
3568
4153
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3569
4154
|
}
|
|
3570
4155
|
return [];
|
|
3571
4156
|
}
|
|
3572
|
-
function
|
|
4157
|
+
function strName7(v) {
|
|
3573
4158
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3574
4159
|
}
|
|
3575
4160
|
function strList(v) {
|
|
3576
4161
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
3577
4162
|
}
|
|
3578
|
-
function
|
|
4163
|
+
function distance3(a, b) {
|
|
3579
4164
|
const m = a.length;
|
|
3580
4165
|
const n = b.length;
|
|
3581
4166
|
if (m === 0) return n;
|
|
@@ -3591,11 +4176,11 @@ function distance2(a, b) {
|
|
|
3591
4176
|
}
|
|
3592
4177
|
return prev[n];
|
|
3593
4178
|
}
|
|
3594
|
-
function
|
|
4179
|
+
function suggest4(target, known) {
|
|
3595
4180
|
let best;
|
|
3596
4181
|
let bestScore = Infinity;
|
|
3597
4182
|
for (const candidate of known) {
|
|
3598
|
-
const d =
|
|
4183
|
+
const d = distance3(target, candidate);
|
|
3599
4184
|
if (d < bestScore) {
|
|
3600
4185
|
bestScore = d;
|
|
3601
4186
|
best = candidate;
|
|
@@ -3606,14 +4191,14 @@ function suggest3(target, known) {
|
|
|
3606
4191
|
}
|
|
3607
4192
|
function collectActionNames(stack) {
|
|
3608
4193
|
const names = /* @__PURE__ */ new Set();
|
|
3609
|
-
for (const action of
|
|
3610
|
-
const n =
|
|
4194
|
+
for (const action of asArray25(stack.actions)) {
|
|
4195
|
+
const n = strName7(action?.name);
|
|
3611
4196
|
if (n) names.add(n);
|
|
3612
4197
|
}
|
|
3613
|
-
for (const obj of
|
|
4198
|
+
for (const obj of asArray25(stack.objects)) {
|
|
3614
4199
|
if (!obj || typeof obj !== "object") continue;
|
|
3615
|
-
for (const action of
|
|
3616
|
-
const n =
|
|
4200
|
+
for (const action of asArray25(obj.actions)) {
|
|
4201
|
+
const n = strName7(action?.name);
|
|
3617
4202
|
if (n) names.add(n);
|
|
3618
4203
|
}
|
|
3619
4204
|
}
|
|
@@ -3630,15 +4215,15 @@ function validateActionNameRefs(stack) {
|
|
|
3630
4215
|
rule: ACTION_NAME_UNDEFINED,
|
|
3631
4216
|
where,
|
|
3632
4217
|
path,
|
|
3633
|
-
message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` +
|
|
4218
|
+
message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` + suggest4(name, known),
|
|
3634
4219
|
hint: `Define an action named "${name}" (in \`stack.actions\` or the object's \`actions\`) with the location this surface needs, remove the reference, or ignore this if the action is contributed by another installed package.` + (known.size > 0 ? ` Defined actions: ${[...known].sort().join(", ")}.` : "")
|
|
3635
4220
|
});
|
|
3636
4221
|
};
|
|
3637
|
-
const views =
|
|
4222
|
+
const views = asArray25(stack.views);
|
|
3638
4223
|
for (let vi = 0; vi < views.length; vi++) {
|
|
3639
4224
|
const view = views[vi];
|
|
3640
4225
|
if (!view || typeof view !== "object") continue;
|
|
3641
|
-
const viewName =
|
|
4226
|
+
const viewName = strName7(view.name) ?? strName7(view.object) ?? `#${vi}`;
|
|
3642
4227
|
const checkListContainer = (container, label2, path) => {
|
|
3643
4228
|
if (!container || typeof container !== "object") return;
|
|
3644
4229
|
const list3 = container;
|
|
@@ -3649,238 +4234,66 @@ function validateActionNameRefs(stack) {
|
|
|
3649
4234
|
names[ai],
|
|
3650
4235
|
`view "${viewName}" \xB7 ${label2} \xB7 ${key}`,
|
|
3651
4236
|
`${path}.${key}[${ai}]`,
|
|
3652
|
-
key === "bulkActions" ? "Bulk-action menu" : "Row-action menu"
|
|
3653
|
-
);
|
|
3654
|
-
}
|
|
3655
|
-
}
|
|
3656
|
-
};
|
|
3657
|
-
checkListContainer(view.list, "list", `views[${vi}].list`);
|
|
3658
|
-
const listViews = view.listViews;
|
|
3659
|
-
if (listViews && typeof listViews === "object" && !Array.isArray(listViews)) {
|
|
3660
|
-
for (const [key, lv] of Object.entries(listViews)) {
|
|
3661
|
-
checkListContainer(lv, `listViews.${key}`, `views[${vi}].listViews.${key}`);
|
|
3662
|
-
}
|
|
3663
|
-
}
|
|
3664
|
-
}
|
|
3665
|
-
const pages = asArray23(stack.pages);
|
|
3666
|
-
for (let pi = 0; pi < pages.length; pi++) {
|
|
3667
|
-
const page = pages[pi];
|
|
3668
|
-
if (!page || typeof page !== "object") continue;
|
|
3669
|
-
const pageName = strName4(page.name) ?? `#${pi}`;
|
|
3670
|
-
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
3671
|
-
const props = component.properties;
|
|
3672
|
-
if (!props || typeof props !== "object") continue;
|
|
3673
|
-
const names = strList(props.actionNames);
|
|
3674
|
-
for (let ai = 0; ai < names.length; ai++) {
|
|
3675
|
-
check(
|
|
3676
|
-
names[ai],
|
|
3677
|
-
`page "${pageName}" \xB7 component "${strName4(component.type) ?? "?"}"`,
|
|
3678
|
-
`${path}.properties.actionNames[${ai}]`,
|
|
3679
|
-
"Quick-actions bar"
|
|
3680
|
-
);
|
|
3681
|
-
}
|
|
3682
|
-
}
|
|
3683
|
-
}
|
|
3684
|
-
const apps = asArray23(stack.apps);
|
|
3685
|
-
for (let ai = 0; ai < apps.length; ai++) {
|
|
3686
|
-
const app = apps[ai];
|
|
3687
|
-
if (!app || typeof app !== "object") continue;
|
|
3688
|
-
const appName = strName4(app.name) ?? `#${ai}`;
|
|
3689
|
-
const walkNav = (items, basePath) => {
|
|
3690
|
-
const navItems = asArray23(items);
|
|
3691
|
-
for (let ni = 0; ni < navItems.length; ni++) {
|
|
3692
|
-
const nav = navItems[ni];
|
|
3693
|
-
if (!nav || typeof nav !== "object") continue;
|
|
3694
|
-
const navPath = `${basePath}[${ni}]`;
|
|
3695
|
-
const actionDef = nav.actionDef;
|
|
3696
|
-
const actionName = strName4(actionDef?.actionName);
|
|
3697
|
-
if (nav.type === "action" && actionName) {
|
|
3698
|
-
check(
|
|
3699
|
-
actionName,
|
|
3700
|
-
`app "${appName}" \xB7 nav "${strName4(nav.id) ?? `#${ni}`}"`,
|
|
3701
|
-
`${navPath}.actionDef.actionName`,
|
|
3702
|
-
"Navigation action item"
|
|
3703
|
-
);
|
|
3704
|
-
}
|
|
3705
|
-
if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
|
|
3706
|
-
}
|
|
3707
|
-
};
|
|
3708
|
-
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
3709
|
-
const areas = asArray23(app.areas);
|
|
3710
|
-
for (let ri = 0; ri < areas.length; ri++) {
|
|
3711
|
-
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
3712
|
-
}
|
|
3713
|
-
}
|
|
3714
|
-
return findings;
|
|
3715
|
-
}
|
|
3716
|
-
|
|
3717
|
-
// src/validate-page-field-bindings.ts
|
|
3718
|
-
var PAGE_FIELD_UNKNOWN = "page-field-unknown";
|
|
3719
|
-
var SYSTEM_FIELDS4 = /* @__PURE__ */ new Set([
|
|
3720
|
-
"id",
|
|
3721
|
-
"created_at",
|
|
3722
|
-
"created_by",
|
|
3723
|
-
"updated_at",
|
|
3724
|
-
"updated_by",
|
|
3725
|
-
"owner_id",
|
|
3726
|
-
"organization_id",
|
|
3727
|
-
"tenant_id",
|
|
3728
|
-
"user_id",
|
|
3729
|
-
"deleted_at"
|
|
3730
|
-
]);
|
|
3731
|
-
function asArray24(v) {
|
|
3732
|
-
if (Array.isArray(v)) return v;
|
|
3733
|
-
if (v && typeof v === "object") {
|
|
3734
|
-
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3735
|
-
}
|
|
3736
|
-
return [];
|
|
3737
|
-
}
|
|
3738
|
-
function strName5(v) {
|
|
3739
|
-
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3740
|
-
}
|
|
3741
|
-
function isRec3(v) {
|
|
3742
|
-
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3743
|
-
}
|
|
3744
|
-
function fieldRefsFrom(value, basePath) {
|
|
3745
|
-
const out = [];
|
|
3746
|
-
const one = (v, path) => {
|
|
3747
|
-
const bare = strName5(v);
|
|
3748
|
-
if (bare) {
|
|
3749
|
-
out.push({ name: bare, path });
|
|
3750
|
-
return;
|
|
3751
|
-
}
|
|
3752
|
-
if (!isRec3(v)) return;
|
|
3753
|
-
const named = strName5(v.field) ?? strName5(v.name);
|
|
3754
|
-
if (named) out.push({ name: named, path: `${path}.${strName5(v.field) ? "field" : "name"}` });
|
|
3755
|
-
};
|
|
3756
|
-
if (Array.isArray(value)) {
|
|
3757
|
-
for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
|
|
3758
|
-
} else {
|
|
3759
|
-
one(value, basePath);
|
|
3760
|
-
}
|
|
3761
|
-
return out;
|
|
3762
|
-
}
|
|
3763
|
-
var COMPONENT_FIELD_SPECS = {
|
|
3764
|
-
"record:highlights": { props: ["fields"] },
|
|
3765
|
-
// `sections`/`hideFields` are not in RecordDetailsProps, but every real page
|
|
3766
|
-
// authors them (they survive because `properties` is unvalidated).
|
|
3767
|
-
"record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
|
|
3768
|
-
"record:path": { props: ["statusField"] },
|
|
3769
|
-
"element:number": { props: ["field"] },
|
|
3770
|
-
"element:filter": { props: ["fields"] },
|
|
3771
|
-
"element:form": { props: ["fields"] },
|
|
3772
|
-
// The schema says `displayField`; real pages author `labelField`. Accept both.
|
|
3773
|
-
"element:record_picker": { props: ["displayField", "labelField", "searchFields"] }
|
|
3774
|
-
};
|
|
3775
|
-
var RELATED_LIST_TYPE = "record:related_list";
|
|
3776
|
-
function validatePageFieldBindings(stack) {
|
|
3777
|
-
const findings = [];
|
|
3778
|
-
if (!stack || typeof stack !== "object") return findings;
|
|
3779
|
-
const objectFields = /* @__PURE__ */ new Map();
|
|
3780
|
-
for (const obj of asArray24(stack.objects)) {
|
|
3781
|
-
const name = strName5(obj.name);
|
|
3782
|
-
if (!name) continue;
|
|
3783
|
-
const names = /* @__PURE__ */ new Set();
|
|
3784
|
-
for (const f of asArray24(obj.fields)) {
|
|
3785
|
-
const fn = strName5(f.name);
|
|
3786
|
-
if (fn) names.add(fn);
|
|
3787
|
-
}
|
|
3788
|
-
objectFields.set(name, names);
|
|
3789
|
-
}
|
|
3790
|
-
const pages = asArray24(stack.pages);
|
|
3791
|
-
for (let pi = 0; pi < pages.length; pi++) {
|
|
3792
|
-
const page = pages[pi];
|
|
3793
|
-
if (!page || typeof page !== "object") continue;
|
|
3794
|
-
const pageName = strName5(page.name) ?? `#${pi}`;
|
|
3795
|
-
const checkRefs = (refs, objectName, where) => {
|
|
3796
|
-
if (!objectName) return;
|
|
3797
|
-
const known = objectFields.get(objectName);
|
|
3798
|
-
if (!known) return;
|
|
3799
|
-
for (const ref of refs) {
|
|
3800
|
-
if (ref.name.includes(".")) continue;
|
|
3801
|
-
if (known.has(ref.name) || SYSTEM_FIELDS4.has(ref.name)) continue;
|
|
3802
|
-
findings.push({
|
|
3803
|
-
severity: "warning",
|
|
3804
|
-
rule: PAGE_FIELD_UNKNOWN,
|
|
3805
|
-
where,
|
|
3806
|
-
path: ref.path,
|
|
3807
|
-
message: `field "${ref.name}" is not a field on object "${objectName}" \u2014 the component silently skips it, so it never renders.`,
|
|
3808
|
-
hint: `Fix the field name, or add "${ref.name}" to ${objectName}. References must match the object's field names exactly.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
|
|
3809
|
-
});
|
|
3810
|
-
}
|
|
3811
|
-
};
|
|
3812
|
-
for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
3813
|
-
const type = strName5(component.type);
|
|
3814
|
-
const props = isRec3(component.properties) ? component.properties : void 0;
|
|
3815
|
-
if (!type || !props) continue;
|
|
3816
|
-
const where = `page "${pageName}" \xB7 ${type}`;
|
|
3817
|
-
if (type === RELATED_LIST_TYPE) {
|
|
3818
|
-
const relatedObject = strName5(props.objectName);
|
|
3819
|
-
const relatedRefs = [
|
|
3820
|
-
...fieldRefsFrom(props.columns, `${path}.properties.columns`),
|
|
3821
|
-
...fieldRefsFrom(props.sort, `${path}.properties.sort`),
|
|
3822
|
-
...fieldRefsFrom(props.filter, `${path}.properties.filter`),
|
|
3823
|
-
...fieldRefsFrom(props.relationshipField, `${path}.properties.relationshipField`)
|
|
3824
|
-
];
|
|
3825
|
-
checkRefs(relatedRefs, relatedObject, where);
|
|
3826
|
-
checkRefs(
|
|
3827
|
-
fieldRefsFrom(props.relationshipValueField, `${path}.properties.relationshipValueField`),
|
|
3828
|
-
objectName,
|
|
3829
|
-
where
|
|
3830
|
-
);
|
|
3831
|
-
const add = isRec3(props.add) ? props.add : void 0;
|
|
3832
|
-
const picker = add && isRec3(add.picker) ? add.picker : void 0;
|
|
3833
|
-
if (picker) {
|
|
3834
|
-
checkRefs(
|
|
3835
|
-
[
|
|
3836
|
-
...fieldRefsFrom(picker.valueField, `${path}.properties.add.picker.valueField`),
|
|
3837
|
-
...fieldRefsFrom(picker.labelField, `${path}.properties.add.picker.labelField`)
|
|
3838
|
-
],
|
|
3839
|
-
strName5(picker.object),
|
|
3840
|
-
where
|
|
3841
|
-
);
|
|
3842
|
-
}
|
|
3843
|
-
if (add) {
|
|
3844
|
-
checkRefs(
|
|
3845
|
-
fieldRefsFrom(add.linkField, `${path}.properties.add.linkField`),
|
|
3846
|
-
relatedObject,
|
|
3847
|
-
where
|
|
4237
|
+
key === "bulkActions" ? "Bulk-action menu" : "Row-action menu"
|
|
3848
4238
|
);
|
|
3849
4239
|
}
|
|
3850
|
-
continue;
|
|
3851
4240
|
}
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
4241
|
+
};
|
|
4242
|
+
checkListContainer(view.list, "list", `views[${vi}].list`);
|
|
4243
|
+
const listViews = view.listViews;
|
|
4244
|
+
if (listViews && typeof listViews === "object" && !Array.isArray(listViews)) {
|
|
4245
|
+
for (const [key, lv] of Object.entries(listViews)) {
|
|
4246
|
+
checkListContainer(lv, `listViews.${key}`, `views[${vi}].listViews.${key}`);
|
|
3857
4247
|
}
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
const pages = asArray25(stack.pages);
|
|
4251
|
+
for (let pi = 0; pi < pages.length; pi++) {
|
|
4252
|
+
const page = pages[pi];
|
|
4253
|
+
if (!page || typeof page !== "object") continue;
|
|
4254
|
+
const pageName = strName7(page.name) ?? `#${pi}`;
|
|
4255
|
+
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
4256
|
+
const props = component.properties;
|
|
4257
|
+
if (!props || typeof props !== "object") continue;
|
|
4258
|
+
const names = strList(props.actionNames);
|
|
4259
|
+
for (let ai = 0; ai < names.length; ai++) {
|
|
4260
|
+
check(
|
|
4261
|
+
names[ai],
|
|
4262
|
+
`page "${pageName}" \xB7 component "${strName7(component.type) ?? "?"}"`,
|
|
4263
|
+
`${path}.properties.actionNames[${ai}]`,
|
|
4264
|
+
"Quick-actions bar"
|
|
4265
|
+
);
|
|
3867
4266
|
}
|
|
3868
|
-
checkRefs(refs, objectName, where);
|
|
3869
4267
|
}
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
4268
|
+
}
|
|
4269
|
+
const apps = asArray25(stack.apps);
|
|
4270
|
+
for (let ai = 0; ai < apps.length; ai++) {
|
|
4271
|
+
const app = apps[ai];
|
|
4272
|
+
if (!app || typeof app !== "object") continue;
|
|
4273
|
+
const appName = strName7(app.name) ?? `#${ai}`;
|
|
4274
|
+
const walkNav = (items, basePath) => {
|
|
4275
|
+
const navItems = asArray25(items);
|
|
4276
|
+
for (let ni = 0; ni < navItems.length; ni++) {
|
|
4277
|
+
const nav = navItems[ni];
|
|
4278
|
+
if (!nav || typeof nav !== "object") continue;
|
|
4279
|
+
const navPath = `${basePath}[${ni}]`;
|
|
4280
|
+
const actionDef = nav.actionDef;
|
|
4281
|
+
const actionName = strName7(actionDef?.actionName);
|
|
4282
|
+
if (nav.type === "action" && actionName) {
|
|
4283
|
+
check(
|
|
4284
|
+
actionName,
|
|
4285
|
+
`app "${appName}" \xB7 nav "${strName7(nav.id) ?? `#${ni}`}"`,
|
|
4286
|
+
`${navPath}.actionDef.actionName`,
|
|
4287
|
+
"Navigation action item"
|
|
4288
|
+
);
|
|
4289
|
+
}
|
|
4290
|
+
if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
|
|
3882
4291
|
}
|
|
3883
|
-
|
|
4292
|
+
};
|
|
4293
|
+
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
4294
|
+
const areas = asArray25(app.areas);
|
|
4295
|
+
for (let ri = 0; ri < areas.length; ri++) {
|
|
4296
|
+
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
3884
4297
|
}
|
|
3885
4298
|
}
|
|
3886
4299
|
return findings;
|
|
@@ -3891,23 +4304,23 @@ var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
|
|
|
3891
4304
|
var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
|
|
3892
4305
|
var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
|
|
3893
4306
|
var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
|
|
3894
|
-
function
|
|
4307
|
+
function asArray26(v) {
|
|
3895
4308
|
if (Array.isArray(v)) return v;
|
|
3896
4309
|
if (v && typeof v === "object") {
|
|
3897
4310
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3898
4311
|
}
|
|
3899
4312
|
return [];
|
|
3900
4313
|
}
|
|
3901
|
-
function
|
|
4314
|
+
function strName8(v) {
|
|
3902
4315
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3903
4316
|
}
|
|
3904
4317
|
function strList2(v) {
|
|
3905
4318
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
3906
4319
|
}
|
|
3907
|
-
function
|
|
4320
|
+
function isRec6(v) {
|
|
3908
4321
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3909
4322
|
}
|
|
3910
|
-
function
|
|
4323
|
+
function distance4(a, b) {
|
|
3911
4324
|
const m = a.length;
|
|
3912
4325
|
const n = b.length;
|
|
3913
4326
|
if (m === 0) return n;
|
|
@@ -3923,11 +4336,11 @@ function distance3(a, b) {
|
|
|
3923
4336
|
}
|
|
3924
4337
|
return prev[n];
|
|
3925
4338
|
}
|
|
3926
|
-
function
|
|
4339
|
+
function suggest5(target, known) {
|
|
3927
4340
|
let best;
|
|
3928
4341
|
let bestScore = Infinity;
|
|
3929
4342
|
for (const c of known) {
|
|
3930
|
-
const d =
|
|
4343
|
+
const d = distance4(target, c);
|
|
3931
4344
|
if (d < bestScore) {
|
|
3932
4345
|
bestScore = d;
|
|
3933
4346
|
best = c;
|
|
@@ -3942,17 +4355,17 @@ function list2(names) {
|
|
|
3942
4355
|
}
|
|
3943
4356
|
function indexDatasets(stack) {
|
|
3944
4357
|
const out = /* @__PURE__ */ new Map();
|
|
3945
|
-
for (const ds of
|
|
3946
|
-
const name =
|
|
4358
|
+
for (const ds of asArray26(stack.datasets)) {
|
|
4359
|
+
const name = strName8(ds.name);
|
|
3947
4360
|
if (!name) continue;
|
|
3948
4361
|
const dimensions = /* @__PURE__ */ new Set();
|
|
3949
|
-
for (const d of
|
|
3950
|
-
const n =
|
|
4362
|
+
for (const d of asArray26(ds.dimensions)) {
|
|
4363
|
+
const n = strName8(d.name);
|
|
3951
4364
|
if (n) dimensions.add(n);
|
|
3952
4365
|
}
|
|
3953
4366
|
const measures = /* @__PURE__ */ new Set();
|
|
3954
|
-
for (const m of
|
|
3955
|
-
const n =
|
|
4367
|
+
for (const m of asArray26(ds.measures)) {
|
|
4368
|
+
const n = strName8(m.name);
|
|
3956
4369
|
if (n) measures.add(n);
|
|
3957
4370
|
}
|
|
3958
4371
|
out.set(name, { dimensions, measures });
|
|
@@ -3975,7 +4388,7 @@ function validateChartBindings(stack) {
|
|
|
3975
4388
|
where: binding.where,
|
|
3976
4389
|
path: `${binding.path}.dataset`,
|
|
3977
4390
|
message: `binds dataset "${dsName}", which resolves to no declared dataset \u2014 the chart has no data to render.`,
|
|
3978
|
-
hint: `Declared datasets: ${list2(datasets.keys())}.${
|
|
4391
|
+
hint: `Declared datasets: ${list2(datasets.keys())}.${suggest5(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
|
|
3979
4392
|
});
|
|
3980
4393
|
return;
|
|
3981
4394
|
}
|
|
@@ -3987,7 +4400,7 @@ function validateChartBindings(stack) {
|
|
|
3987
4400
|
where: binding.where,
|
|
3988
4401
|
path,
|
|
3989
4402
|
message: `"${name}" is not a dimension declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by DIMENSION NAME, not the base field, so this axis renders with no categories.`,
|
|
3990
|
-
hint: `Dataset dimensions: ${list2(ds.dimensions)}.${
|
|
4403
|
+
hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest5(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
|
|
3991
4404
|
});
|
|
3992
4405
|
};
|
|
3993
4406
|
const measureRef = (name, path, selected2) => {
|
|
@@ -3998,7 +4411,7 @@ function validateChartBindings(stack) {
|
|
|
3998
4411
|
where: binding.where,
|
|
3999
4412
|
path,
|
|
4000
4413
|
message: `"${name}" is not a measure declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by MEASURE NAME (e.g. "sum_amount"), not the base field (e.g. "amount"), so this series comes back empty.`,
|
|
4001
|
-
hint: `Dataset measures: ${list2(ds.measures)}.${
|
|
4414
|
+
hint: `Dataset measures: ${list2(ds.measures)}.${suggest5(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
|
|
4002
4415
|
});
|
|
4003
4416
|
return;
|
|
4004
4417
|
}
|
|
@@ -4030,29 +4443,29 @@ function validateChartBindings(stack) {
|
|
|
4030
4443
|
if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
|
|
4031
4444
|
for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
|
|
4032
4445
|
};
|
|
4033
|
-
const reports =
|
|
4446
|
+
const reports = asArray26(stack.reports);
|
|
4034
4447
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
4035
4448
|
const report = reports[ri];
|
|
4036
|
-
if (!
|
|
4037
|
-
const reportName =
|
|
4449
|
+
if (!isRec6(report)) continue;
|
|
4450
|
+
const reportName = strName8(report.name) ?? `#${ri}`;
|
|
4038
4451
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
4039
|
-
if (!
|
|
4452
|
+
if (!isRec6(chart)) return;
|
|
4040
4453
|
check({
|
|
4041
4454
|
dataset,
|
|
4042
4455
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
4043
4456
|
// it in lets the yAxis "declared but not selected" check work without
|
|
4044
4457
|
// reporting the selection itself twice.
|
|
4045
4458
|
values: { names: values, path: `${path}.values` },
|
|
4046
|
-
xAxis:
|
|
4047
|
-
yAxis:
|
|
4048
|
-
series:
|
|
4459
|
+
xAxis: strName8(chart.xAxis) ? { name: strName8(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
|
|
4460
|
+
yAxis: strName8(chart.yAxis) ? { name: strName8(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
|
|
4461
|
+
series: asArray26(chart.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
|
|
4049
4462
|
where,
|
|
4050
4463
|
path: `${path}.chart`
|
|
4051
4464
|
});
|
|
4052
4465
|
};
|
|
4053
4466
|
checkReportChart(
|
|
4054
4467
|
report.chart,
|
|
4055
|
-
|
|
4468
|
+
strName8(report.dataset),
|
|
4056
4469
|
strList2(report.values),
|
|
4057
4470
|
`report "${reportName}" \xB7 chart`,
|
|
4058
4471
|
`reports[${ri}]`
|
|
@@ -4060,45 +4473,45 @@ function validateChartBindings(stack) {
|
|
|
4060
4473
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
4061
4474
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
4062
4475
|
const block = blocks[bi];
|
|
4063
|
-
if (!
|
|
4476
|
+
if (!isRec6(block)) continue;
|
|
4064
4477
|
checkReportChart(
|
|
4065
4478
|
block.chart,
|
|
4066
|
-
|
|
4479
|
+
strName8(block.dataset),
|
|
4067
4480
|
strList2(block.values),
|
|
4068
|
-
`report "${reportName}" \xB7 block "${
|
|
4481
|
+
`report "${reportName}" \xB7 block "${strName8(block.name) ?? `#${bi}`}" chart`,
|
|
4069
4482
|
`reports[${ri}].blocks[${bi}]`
|
|
4070
4483
|
);
|
|
4071
4484
|
}
|
|
4072
4485
|
}
|
|
4073
4486
|
const checkListChart = (container, where, path) => {
|
|
4074
|
-
if (!
|
|
4487
|
+
if (!isRec6(container)) return;
|
|
4075
4488
|
const chart = container.chart;
|
|
4076
|
-
if (!
|
|
4489
|
+
if (!isRec6(chart)) return;
|
|
4077
4490
|
check({
|
|
4078
|
-
dataset:
|
|
4491
|
+
dataset: strName8(chart.dataset),
|
|
4079
4492
|
dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
4080
4493
|
values: { names: strList2(chart.values), path: `${path}.chart.values` },
|
|
4081
4494
|
where,
|
|
4082
4495
|
path: `${path}.chart`
|
|
4083
4496
|
});
|
|
4084
4497
|
};
|
|
4085
|
-
const views =
|
|
4498
|
+
const views = asArray26(stack.views);
|
|
4086
4499
|
for (let vi = 0; vi < views.length; vi++) {
|
|
4087
4500
|
const view = views[vi];
|
|
4088
|
-
if (!
|
|
4089
|
-
const viewName =
|
|
4501
|
+
if (!isRec6(view)) continue;
|
|
4502
|
+
const viewName = strName8(view.name) ?? strName8(view.objectName) ?? `#${vi}`;
|
|
4090
4503
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
4091
|
-
if (
|
|
4504
|
+
if (isRec6(view.listViews)) {
|
|
4092
4505
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
4093
4506
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
4094
4507
|
}
|
|
4095
4508
|
}
|
|
4096
4509
|
}
|
|
4097
|
-
const objects =
|
|
4510
|
+
const objects = asArray26(stack.objects);
|
|
4098
4511
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
4099
4512
|
const obj = objects[oi];
|
|
4100
|
-
if (!
|
|
4101
|
-
const objName =
|
|
4513
|
+
if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
|
|
4514
|
+
const objName = strName8(obj.name) ?? `#${oi}`;
|
|
4102
4515
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
4103
4516
|
checkListChart(
|
|
4104
4517
|
lv,
|
|
@@ -4107,22 +4520,22 @@ function validateChartBindings(stack) {
|
|
|
4107
4520
|
);
|
|
4108
4521
|
}
|
|
4109
4522
|
}
|
|
4110
|
-
const pages =
|
|
4523
|
+
const pages = asArray26(stack.pages);
|
|
4111
4524
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4112
4525
|
const page = pages[pi];
|
|
4113
|
-
if (!
|
|
4114
|
-
const pageName =
|
|
4526
|
+
if (!isRec6(page)) continue;
|
|
4527
|
+
const pageName = strName8(page.name) ?? `#${pi}`;
|
|
4115
4528
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
4116
|
-
const props =
|
|
4117
|
-
if (!props || !
|
|
4118
|
-
const axisRefs =
|
|
4119
|
-
const seriesRefs =
|
|
4529
|
+
const props = isRec6(component.properties) ? component.properties : void 0;
|
|
4530
|
+
if (!props || !strName8(props.dataset)) continue;
|
|
4531
|
+
const axisRefs = asArray26(props.yAxis).map((a, ai) => ({ name: strName8(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
4532
|
+
const seriesRefs = asArray26(props.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
4120
4533
|
check({
|
|
4121
|
-
dataset:
|
|
4534
|
+
dataset: strName8(props.dataset),
|
|
4122
4535
|
dimensions: { names: strList2(props.dimensions), path: `${path}.properties.dimensions` },
|
|
4123
4536
|
values: { names: strList2(props.values), path: `${path}.properties.values` },
|
|
4124
4537
|
series: [...axisRefs, ...seriesRefs],
|
|
4125
|
-
where: `page "${pageName}" \xB7 ${
|
|
4538
|
+
where: `page "${pageName}" \xB7 ${strName8(component.type) ?? "chart"}`,
|
|
4126
4539
|
path: `${path}.properties`
|
|
4127
4540
|
});
|
|
4128
4541
|
}
|
|
@@ -4131,10 +4544,10 @@ function validateChartBindings(stack) {
|
|
|
4131
4544
|
}
|
|
4132
4545
|
|
|
4133
4546
|
// src/validate-nav-access.ts
|
|
4134
|
-
var
|
|
4547
|
+
var import_system3 = require("@objectstack/spec/system");
|
|
4135
4548
|
|
|
4136
4549
|
// src/build-access-matrix.ts
|
|
4137
|
-
function
|
|
4550
|
+
function asArray27(v) {
|
|
4138
4551
|
if (Array.isArray(v)) return v;
|
|
4139
4552
|
if (v && typeof v === "object") {
|
|
4140
4553
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4145,13 +4558,13 @@ function buildAccessMatrix(stack) {
|
|
|
4145
4558
|
const entries = [];
|
|
4146
4559
|
if (!stack || typeof stack !== "object") return { version: 1, entries };
|
|
4147
4560
|
const owdByObject = /* @__PURE__ */ new Map();
|
|
4148
|
-
for (const obj of
|
|
4561
|
+
for (const obj of asArray27(stack.objects)) {
|
|
4149
4562
|
const name = typeof obj.name === "string" ? obj.name : "";
|
|
4150
4563
|
if (!name) continue;
|
|
4151
4564
|
const owd = obj.sharingModel ?? obj.security?.sharingModel;
|
|
4152
4565
|
if (typeof owd === "string") owdByObject.set(name, owd);
|
|
4153
4566
|
}
|
|
4154
|
-
for (const ps of
|
|
4567
|
+
for (const ps of asArray27(stack.permissions)) {
|
|
4155
4568
|
const psName = typeof ps.name === "string" ? ps.name : "";
|
|
4156
4569
|
if (!psName) continue;
|
|
4157
4570
|
const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
|
|
@@ -4224,34 +4637,34 @@ function diffAccessMatrix(before, after) {
|
|
|
4224
4637
|
|
|
4225
4638
|
// src/validate-nav-access.ts
|
|
4226
4639
|
var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
|
|
4227
|
-
function
|
|
4640
|
+
function asArray28(v) {
|
|
4228
4641
|
if (Array.isArray(v)) return v;
|
|
4229
4642
|
if (v && typeof v === "object") {
|
|
4230
4643
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4231
4644
|
}
|
|
4232
4645
|
return [];
|
|
4233
4646
|
}
|
|
4234
|
-
function
|
|
4647
|
+
function strName9(v) {
|
|
4235
4648
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4236
4649
|
}
|
|
4237
4650
|
function collectNavExposures(stack) {
|
|
4238
4651
|
const out = [];
|
|
4239
|
-
const apps =
|
|
4652
|
+
const apps = asArray28(stack.apps);
|
|
4240
4653
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
4241
4654
|
const app = apps[ai];
|
|
4242
4655
|
if (!app || typeof app !== "object") continue;
|
|
4243
|
-
const appName =
|
|
4656
|
+
const appName = strName9(app.name) ?? `#${ai}`;
|
|
4244
4657
|
const walk = (items, basePath) => {
|
|
4245
|
-
const navItems =
|
|
4658
|
+
const navItems = asArray28(items);
|
|
4246
4659
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
4247
4660
|
const nav = navItems[ni];
|
|
4248
4661
|
if (!nav || typeof nav !== "object") continue;
|
|
4249
4662
|
const navPath = `${basePath}[${ni}]`;
|
|
4250
|
-
const objectName =
|
|
4663
|
+
const objectName = strName9(nav.objectName);
|
|
4251
4664
|
if (nav.type === "object" && objectName) {
|
|
4252
4665
|
out.push({
|
|
4253
4666
|
objectName,
|
|
4254
|
-
where: `app "${appName}" \xB7 nav "${
|
|
4667
|
+
where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? `#${ni}`}"`,
|
|
4255
4668
|
path: `${navPath}.objectName`
|
|
4256
4669
|
});
|
|
4257
4670
|
}
|
|
@@ -4259,7 +4672,7 @@ function collectNavExposures(stack) {
|
|
|
4259
4672
|
}
|
|
4260
4673
|
};
|
|
4261
4674
|
walk(app.navigation, `apps[${ai}].navigation`);
|
|
4262
|
-
const areas =
|
|
4675
|
+
const areas = asArray28(app.areas);
|
|
4263
4676
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
4264
4677
|
walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
4265
4678
|
}
|
|
@@ -4269,13 +4682,13 @@ function collectNavExposures(stack) {
|
|
|
4269
4682
|
function validateNavAccess(stack) {
|
|
4270
4683
|
const findings = [];
|
|
4271
4684
|
if (!stack || typeof stack !== "object") return findings;
|
|
4272
|
-
const permissionSets =
|
|
4685
|
+
const permissionSets = asArray28(stack.permissions);
|
|
4273
4686
|
if (permissionSets.length === 0) return findings;
|
|
4274
4687
|
const exposures = collectNavExposures(stack);
|
|
4275
4688
|
if (exposures.length === 0) return findings;
|
|
4276
4689
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
4277
|
-
for (const obj of
|
|
4278
|
-
const n =
|
|
4690
|
+
for (const obj of asArray28(stack.objects)) {
|
|
4691
|
+
const n = strName9(obj.name);
|
|
4279
4692
|
if (n) ownObjects.add(n);
|
|
4280
4693
|
}
|
|
4281
4694
|
const readable = /* @__PURE__ */ new Set();
|
|
@@ -4287,7 +4700,7 @@ function validateNavAccess(stack) {
|
|
|
4287
4700
|
for (const exposure of exposures) {
|
|
4288
4701
|
const { objectName } = exposure;
|
|
4289
4702
|
if (reported.has(objectName)) continue;
|
|
4290
|
-
if ((0,
|
|
4703
|
+
if ((0, import_system3.isPlatformProvidedObjectName)(objectName)) continue;
|
|
4291
4704
|
if (!ownObjects.has(objectName)) continue;
|
|
4292
4705
|
if (readable.has(objectName)) continue;
|
|
4293
4706
|
reported.add(objectName);
|
|
@@ -4304,21 +4717,21 @@ function validateNavAccess(stack) {
|
|
|
4304
4717
|
}
|
|
4305
4718
|
|
|
4306
4719
|
// src/validate-translation-references.ts
|
|
4307
|
-
var
|
|
4720
|
+
var import_system4 = require("@objectstack/spec/system");
|
|
4308
4721
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
4309
4722
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
4310
|
-
function
|
|
4723
|
+
function isRec7(v) {
|
|
4311
4724
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4312
4725
|
}
|
|
4313
|
-
function
|
|
4726
|
+
function asArray29(v) {
|
|
4314
4727
|
if (Array.isArray(v)) return v;
|
|
4315
|
-
if (
|
|
4728
|
+
if (isRec7(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec7(def) ? def : {} }));
|
|
4316
4729
|
return [];
|
|
4317
4730
|
}
|
|
4318
|
-
function
|
|
4731
|
+
function strName10(v) {
|
|
4319
4732
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4320
4733
|
}
|
|
4321
|
-
function
|
|
4734
|
+
function distance5(a, b) {
|
|
4322
4735
|
const m = a.length;
|
|
4323
4736
|
const n = b.length;
|
|
4324
4737
|
if (m === 0) return n;
|
|
@@ -4334,7 +4747,7 @@ function distance4(a, b) {
|
|
|
4334
4747
|
}
|
|
4335
4748
|
return prev[n];
|
|
4336
4749
|
}
|
|
4337
|
-
function
|
|
4750
|
+
function suggest6(target, known) {
|
|
4338
4751
|
const names = [...known];
|
|
4339
4752
|
const segmentMatch = names.find(
|
|
4340
4753
|
(candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
|
|
@@ -4343,7 +4756,7 @@ function suggest5(target, known) {
|
|
|
4343
4756
|
let best;
|
|
4344
4757
|
let bestScore = Infinity;
|
|
4345
4758
|
for (const candidate of names) {
|
|
4346
|
-
const d =
|
|
4759
|
+
const d = distance5(target, candidate);
|
|
4347
4760
|
if (d < bestScore) {
|
|
4348
4761
|
bestScore = d;
|
|
4349
4762
|
best = candidate;
|
|
@@ -4358,20 +4771,10 @@ function listNames(names, max = 12) {
|
|
|
4358
4771
|
const shown = all.slice(0, max).join(", ");
|
|
4359
4772
|
return all.length > max ? `${shown}, \u2026 (${all.length} total)` : shown;
|
|
4360
4773
|
}
|
|
4361
|
-
var
|
|
4362
|
-
|
|
4774
|
+
var IMPLICIT_FIELDS = /* @__PURE__ */ new Set([
|
|
4775
|
+
...SYSTEM_FIELDS,
|
|
4363
4776
|
"_id",
|
|
4364
4777
|
"name",
|
|
4365
|
-
"created_at",
|
|
4366
|
-
"created_by",
|
|
4367
|
-
"updated_at",
|
|
4368
|
-
"updated_by",
|
|
4369
|
-
"owner_id",
|
|
4370
|
-
"organization_id",
|
|
4371
|
-
"tenant_id",
|
|
4372
|
-
"user_id",
|
|
4373
|
-
"is_deleted",
|
|
4374
|
-
"deleted_at",
|
|
4375
4778
|
"space"
|
|
4376
4779
|
]);
|
|
4377
4780
|
function emptyFacts() {
|
|
@@ -4383,20 +4786,20 @@ function collectViewRecord(view, factsFor) {
|
|
|
4383
4786
|
const addView = (objectName, name) => {
|
|
4384
4787
|
if (objectName && name) factsFor(objectName).views.add(name);
|
|
4385
4788
|
};
|
|
4386
|
-
const listBinding =
|
|
4387
|
-
if (
|
|
4388
|
-
addView(recordObject ?? listBinding,
|
|
4789
|
+
const listBinding = isRec7(view.list) ? bindingOf(view.list) : void 0;
|
|
4790
|
+
if (isRec7(view.list)) addView(listBinding, strName10(view.list.name));
|
|
4791
|
+
addView(recordObject ?? listBinding, strName10(view.name));
|
|
4389
4792
|
for (const key of ["listViews", "formViews"]) {
|
|
4390
4793
|
const container = view[key];
|
|
4391
|
-
if (!
|
|
4794
|
+
if (!isRec7(container)) continue;
|
|
4392
4795
|
for (const [subKey, sub] of Object.entries(container)) {
|
|
4393
|
-
if (!
|
|
4796
|
+
if (!isRec7(sub)) continue;
|
|
4394
4797
|
const binding = bindingOf(sub) ?? listBinding;
|
|
4395
4798
|
addView(binding, subKey);
|
|
4396
|
-
addView(binding,
|
|
4799
|
+
addView(binding, strName10(sub.name));
|
|
4397
4800
|
if (binding) {
|
|
4398
|
-
for (const section of
|
|
4399
|
-
const sectionName =
|
|
4801
|
+
for (const section of asArray29(sub.sections)) {
|
|
4802
|
+
const sectionName = strName10(section.name);
|
|
4400
4803
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
4401
4804
|
}
|
|
4402
4805
|
}
|
|
@@ -4404,14 +4807,14 @@ function collectViewRecord(view, factsFor) {
|
|
|
4404
4807
|
}
|
|
4405
4808
|
const sectionBinding = recordObject ?? listBinding;
|
|
4406
4809
|
if (sectionBinding) {
|
|
4407
|
-
for (const section of
|
|
4408
|
-
const sectionName =
|
|
4810
|
+
for (const section of asArray29(view.sections)) {
|
|
4811
|
+
const sectionName = strName10(section.name);
|
|
4409
4812
|
if (sectionName) factsFor(sectionBinding).sections.add(sectionName);
|
|
4410
4813
|
}
|
|
4411
4814
|
}
|
|
4412
4815
|
}
|
|
4413
4816
|
function viewObjectName(view) {
|
|
4414
|
-
return
|
|
4817
|
+
return strName10(view.objectName) ?? strName10(view.object) ?? (isRec7(view.data) ? strName10(view.data.object) : void 0);
|
|
4415
4818
|
}
|
|
4416
4819
|
function readOptions(field) {
|
|
4417
4820
|
const raw = field.options;
|
|
@@ -4423,14 +4826,14 @@ function readOptions(field) {
|
|
|
4423
4826
|
values.add(opt);
|
|
4424
4827
|
continue;
|
|
4425
4828
|
}
|
|
4426
|
-
if (!
|
|
4427
|
-
const value =
|
|
4829
|
+
if (!isRec7(opt)) continue;
|
|
4830
|
+
const value = strName10(opt.value);
|
|
4428
4831
|
if (!value) continue;
|
|
4429
4832
|
values.add(value);
|
|
4430
|
-
const label2 =
|
|
4833
|
+
const label2 = strName10(opt.label);
|
|
4431
4834
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
4432
4835
|
}
|
|
4433
|
-
} else if (
|
|
4836
|
+
} else if (isRec7(raw)) {
|
|
4434
4837
|
for (const [value, label2] of Object.entries(raw)) {
|
|
4435
4838
|
values.add(value);
|
|
4436
4839
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -4450,48 +4853,48 @@ function buildUniverse(stack) {
|
|
|
4450
4853
|
}
|
|
4451
4854
|
return facts;
|
|
4452
4855
|
};
|
|
4453
|
-
for (const obj of
|
|
4454
|
-
const objectName =
|
|
4856
|
+
for (const obj of asArray29(stack.objects)) {
|
|
4857
|
+
const objectName = strName10(obj.name);
|
|
4455
4858
|
if (!objectName) continue;
|
|
4456
4859
|
const facts = factsFor(objectName);
|
|
4457
|
-
for (const field of
|
|
4458
|
-
const fieldName =
|
|
4860
|
+
for (const field of asArray29(obj.fields)) {
|
|
4861
|
+
const fieldName = strName10(field.name);
|
|
4459
4862
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
4460
4863
|
}
|
|
4461
|
-
for (const action of
|
|
4462
|
-
const actionName =
|
|
4864
|
+
for (const action of asArray29(obj.actions)) {
|
|
4865
|
+
const actionName = strName10(action.name);
|
|
4463
4866
|
if (actionName) facts.actions.set(actionName, action);
|
|
4464
4867
|
}
|
|
4465
|
-
for (const view of
|
|
4466
|
-
collectViewRecord({ ...view, object:
|
|
4868
|
+
for (const view of asArray29(obj.views)) {
|
|
4869
|
+
collectViewRecord({ ...view, object: strName10(view.object) ?? objectName }, factsFor);
|
|
4467
4870
|
}
|
|
4468
4871
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
4469
|
-
for (const group of
|
|
4470
|
-
const key =
|
|
4872
|
+
for (const group of asArray29(obj.fieldGroups)) {
|
|
4873
|
+
const key = strName10(group.key) ?? strName10(group.name);
|
|
4471
4874
|
if (key) facts.sections.add(key);
|
|
4472
4875
|
}
|
|
4473
4876
|
}
|
|
4474
|
-
for (const view of
|
|
4877
|
+
for (const view of asArray29(stack.views)) {
|
|
4475
4878
|
collectViewRecord(view, factsFor);
|
|
4476
4879
|
}
|
|
4477
|
-
const pages =
|
|
4880
|
+
const pages = asArray29(stack.pages);
|
|
4478
4881
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4479
4882
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
4480
4883
|
if (!walked.objectName) continue;
|
|
4481
|
-
const props =
|
|
4884
|
+
const props = isRec7(walked.component.properties) ? walked.component.properties : void 0;
|
|
4482
4885
|
if (!props) continue;
|
|
4483
|
-
for (const section of
|
|
4484
|
-
const sectionName =
|
|
4886
|
+
for (const section of asArray29(props.sections)) {
|
|
4887
|
+
const sectionName = strName10(section.name);
|
|
4485
4888
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
4486
4889
|
}
|
|
4487
4890
|
}
|
|
4488
4891
|
}
|
|
4489
4892
|
const globalActions = /* @__PURE__ */ new Map();
|
|
4490
4893
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
4491
|
-
for (const action of
|
|
4492
|
-
const actionName =
|
|
4894
|
+
for (const action of asArray29(stack.actions)) {
|
|
4895
|
+
const actionName = strName10(action.name);
|
|
4493
4896
|
if (!actionName) continue;
|
|
4494
|
-
const owner =
|
|
4897
|
+
const owner = strName10(action.objectName) ?? strName10(action.object);
|
|
4495
4898
|
if (owner) {
|
|
4496
4899
|
factsFor(owner).actions.set(actionName, action);
|
|
4497
4900
|
actionOwners.set(actionName, owner);
|
|
@@ -4505,41 +4908,41 @@ function buildUniverse(stack) {
|
|
|
4505
4908
|
}
|
|
4506
4909
|
}
|
|
4507
4910
|
const apps = /* @__PURE__ */ new Map();
|
|
4508
|
-
for (const app of
|
|
4509
|
-
const appName =
|
|
4911
|
+
for (const app of asArray29(stack.apps)) {
|
|
4912
|
+
const appName = strName10(app.name);
|
|
4510
4913
|
if (!appName) continue;
|
|
4511
4914
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
4512
4915
|
const walkNav = (items) => {
|
|
4513
|
-
for (const item of
|
|
4514
|
-
const id =
|
|
4916
|
+
for (const item of asArray29(items)) {
|
|
4917
|
+
const id = strName10(item.id);
|
|
4515
4918
|
if (id) navIds.add(id);
|
|
4516
4919
|
if (item.children) walkNav(item.children);
|
|
4517
4920
|
}
|
|
4518
4921
|
};
|
|
4519
4922
|
walkNav(app.navigation);
|
|
4520
|
-
for (const area of
|
|
4521
|
-
const areaId =
|
|
4923
|
+
for (const area of asArray29(app.areas)) {
|
|
4924
|
+
const areaId = strName10(area.id);
|
|
4522
4925
|
if (areaId) navIds.add(areaId);
|
|
4523
4926
|
walkNav(area.navigation);
|
|
4524
4927
|
}
|
|
4525
4928
|
apps.set(appName, navIds);
|
|
4526
4929
|
}
|
|
4527
4930
|
const dashboards = /* @__PURE__ */ new Map();
|
|
4528
|
-
for (const dash of
|
|
4529
|
-
const dashName =
|
|
4931
|
+
for (const dash of asArray29(stack.dashboards)) {
|
|
4932
|
+
const dashName = strName10(dash.name);
|
|
4530
4933
|
if (!dashName) continue;
|
|
4531
4934
|
const widgets = /* @__PURE__ */ new Set();
|
|
4532
|
-
for (const widget of
|
|
4533
|
-
const id =
|
|
4935
|
+
for (const widget of asArray29(dash.widgets)) {
|
|
4936
|
+
const id = strName10(widget.id) ?? strName10(widget.name);
|
|
4534
4937
|
if (id) widgets.add(id);
|
|
4535
4938
|
}
|
|
4536
4939
|
const actions = /* @__PURE__ */ new Set();
|
|
4537
4940
|
const headerActions = [
|
|
4538
|
-
...
|
|
4539
|
-
...
|
|
4941
|
+
...asArray29(isRec7(dash.header) ? dash.header.actions : void 0),
|
|
4942
|
+
...asArray29(dash.actions)
|
|
4540
4943
|
];
|
|
4541
4944
|
for (const action of headerActions) {
|
|
4542
|
-
const key =
|
|
4945
|
+
const key = strName10(action.actionUrl) ?? strName10(action.url) ?? strName10(action.name);
|
|
4543
4946
|
if (key) actions.add(key);
|
|
4544
4947
|
}
|
|
4545
4948
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -4551,7 +4954,7 @@ function localePath(bundleIndex, locale) {
|
|
|
4551
4954
|
}
|
|
4552
4955
|
function validateTranslationReferences(stack) {
|
|
4553
4956
|
const findings = [];
|
|
4554
|
-
if (!
|
|
4957
|
+
if (!isRec7(stack)) return findings;
|
|
4555
4958
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
4556
4959
|
if (bundles.length === 0) return findings;
|
|
4557
4960
|
const universe = buildUniverse(stack);
|
|
@@ -4560,21 +4963,21 @@ function validateTranslationReferences(stack) {
|
|
|
4560
4963
|
};
|
|
4561
4964
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
4562
4965
|
const bundle = bundles[bi];
|
|
4563
|
-
if (!
|
|
4966
|
+
if (!isRec7(bundle)) continue;
|
|
4564
4967
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
4565
|
-
if (!
|
|
4968
|
+
if (!isRec7(rawData)) continue;
|
|
4566
4969
|
const base = localePath(bi, locale);
|
|
4567
4970
|
const inLocale = `locale "${locale}"`;
|
|
4568
4971
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
4569
|
-
if (!
|
|
4972
|
+
if (!isRec7(rawNode)) continue;
|
|
4570
4973
|
const objPath = `${base}.objects.${objectName}`;
|
|
4571
4974
|
const facts = universe.objects.get(objectName);
|
|
4572
4975
|
if (!facts) {
|
|
4573
|
-
if ((0,
|
|
4976
|
+
if ((0, import_system4.isPlatformProvidedObjectName)(objectName)) continue;
|
|
4574
4977
|
orphan(
|
|
4575
4978
|
`${inLocale} \xB7 object "${objectName}"`,
|
|
4576
4979
|
objPath,
|
|
4577
|
-
(0,
|
|
4980
|
+
(0, import_system4.hasPlatformObjectPrefix)(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` + suggest6(objectName, universe.objects.keys()) : `Translations are keyed to "${objectName}", which no object in this stack defines. The resolver looks up keys derived from the metadata, so this whole subtree is dead weight \u2014 every label it carries renders untranslated.` + suggest6(objectName, universe.objects.keys()),
|
|
4578
4981
|
`Rename the key to the object it was written for, drop it, or ignore this if the object is contributed by another installed package.` + (universe.objects.size > 0 ? ` Defined objects: ${listNames(universe.objects.keys())}.` : "")
|
|
4579
4982
|
);
|
|
4580
4983
|
continue;
|
|
@@ -4583,16 +4986,16 @@ function validateTranslationReferences(stack) {
|
|
|
4583
4986
|
const fieldPath = `${objPath}.fields.${fieldName}`;
|
|
4584
4987
|
const field = facts.fields.get(fieldName);
|
|
4585
4988
|
if (!field) {
|
|
4586
|
-
if (
|
|
4989
|
+
if (IMPLICIT_FIELDS.has(fieldName)) continue;
|
|
4587
4990
|
orphan(
|
|
4588
4991
|
`${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
|
|
4589
4992
|
fieldPath,
|
|
4590
|
-
`Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` +
|
|
4993
|
+
`Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` + suggest6(fieldName, facts.fields.keys()),
|
|
4591
4994
|
`Point the key at a declared field, or drop it if the field was removed or renamed.` + (facts.fields.size > 0 ? ` Declared fields: ${listNames(facts.fields.keys())}.` : "")
|
|
4592
4995
|
);
|
|
4593
4996
|
continue;
|
|
4594
4997
|
}
|
|
4595
|
-
if (!
|
|
4998
|
+
if (!isRec7(rawField)) continue;
|
|
4596
4999
|
checkOptionKeys(findings, {
|
|
4597
5000
|
optionMap: rawField.options,
|
|
4598
5001
|
field,
|
|
@@ -4607,7 +5010,7 @@ function validateTranslationReferences(stack) {
|
|
|
4607
5010
|
orphan(
|
|
4608
5011
|
`${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
|
|
4609
5012
|
`${objPath}._views.${viewName}`,
|
|
4610
|
-
`Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` +
|
|
5013
|
+
`Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` + suggest6(viewName, facts.views),
|
|
4611
5014
|
`Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
|
|
4612
5015
|
);
|
|
4613
5016
|
}
|
|
@@ -4616,7 +5019,7 @@ function validateTranslationReferences(stack) {
|
|
|
4616
5019
|
orphan(
|
|
4617
5020
|
`${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
|
|
4618
5021
|
`${objPath}._sections.${sectionName}`,
|
|
4619
|
-
`Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` +
|
|
5022
|
+
`Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` + suggest6(sectionName, facts.sections),
|
|
4620
5023
|
`Sections are translatable only through a STABLE NAME: give the group/section a \`key\`/\`name\` and use it here, or drop the translation.` + (facts.sections.size > 0 ? ` Declared sections: ${listNames(facts.sections)}.` : ` Object "${objectName}" declares no named section at all.`)
|
|
4621
5024
|
);
|
|
4622
5025
|
}
|
|
@@ -4627,7 +5030,7 @@ function validateTranslationReferences(stack) {
|
|
|
4627
5030
|
orphan(
|
|
4628
5031
|
`${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
|
|
4629
5032
|
actionPath,
|
|
4630
|
-
`Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` +
|
|
5033
|
+
`Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` + suggest6(actionName, facts.actions.keys()),
|
|
4631
5034
|
`Match the key to a defined action name, move it under the object that owns the action, or drop it.` + (facts.actions.size > 0 ? ` Actions on this object: ${listNames(facts.actions.keys())}.` : "")
|
|
4632
5035
|
);
|
|
4633
5036
|
continue;
|
|
@@ -4649,7 +5052,7 @@ function validateTranslationReferences(stack) {
|
|
|
4649
5052
|
orphan(
|
|
4650
5053
|
`${inLocale} \xB7 global action "${actionName}"`,
|
|
4651
5054
|
actionPath,
|
|
4652
|
-
owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` +
|
|
5055
|
+
owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` + suggest6(actionName, universe.globalActions.keys()),
|
|
4653
5056
|
owner ? `Move these keys under \`objects.${owner}._actions.${actionName}\`.` : `Match the key to an object-less action's name, or drop it.` + (universe.globalActions.size > 0 ? ` Object-less actions: ${listNames(universe.globalActions.keys())}.` : "")
|
|
4654
5057
|
);
|
|
4655
5058
|
continue;
|
|
@@ -4669,18 +5072,18 @@ function validateTranslationReferences(stack) {
|
|
|
4669
5072
|
orphan(
|
|
4670
5073
|
`${inLocale} \xB7 app "${appName}"`,
|
|
4671
5074
|
appPath,
|
|
4672
|
-
`Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` +
|
|
5075
|
+
`Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` + suggest6(appName, universe.apps.keys()),
|
|
4673
5076
|
`Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
|
|
4674
5077
|
);
|
|
4675
5078
|
continue;
|
|
4676
5079
|
}
|
|
4677
|
-
if (!
|
|
5080
|
+
if (!isRec7(rawApp)) continue;
|
|
4678
5081
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
4679
5082
|
if (navIds.has(navId)) continue;
|
|
4680
5083
|
orphan(
|
|
4681
5084
|
`${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
|
|
4682
5085
|
`${appPath}.navigation.${navId}`,
|
|
4683
|
-
`Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` +
|
|
5086
|
+
`Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest6(navId, navIds),
|
|
4684
5087
|
`Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
|
|
4685
5088
|
);
|
|
4686
5089
|
}
|
|
@@ -4692,18 +5095,18 @@ function validateTranslationReferences(stack) {
|
|
|
4692
5095
|
orphan(
|
|
4693
5096
|
`${inLocale} \xB7 dashboard "${dashName}"`,
|
|
4694
5097
|
dashPath,
|
|
4695
|
-
`Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` +
|
|
5098
|
+
`Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` + suggest6(dashName, universe.dashboards.keys()),
|
|
4696
5099
|
`Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
|
|
4697
5100
|
);
|
|
4698
5101
|
continue;
|
|
4699
5102
|
}
|
|
4700
|
-
if (!
|
|
5103
|
+
if (!isRec7(rawDash)) continue;
|
|
4701
5104
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
4702
5105
|
if (dash.widgets.has(widgetId)) continue;
|
|
4703
5106
|
orphan(
|
|
4704
5107
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
|
|
4705
5108
|
`${dashPath}.widgets.${widgetId}`,
|
|
4706
|
-
`Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` +
|
|
5109
|
+
`Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest6(widgetId, dash.widgets),
|
|
4707
5110
|
`Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
|
|
4708
5111
|
);
|
|
4709
5112
|
}
|
|
@@ -4712,7 +5115,7 @@ function validateTranslationReferences(stack) {
|
|
|
4712
5115
|
orphan(
|
|
4713
5116
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
|
|
4714
5117
|
`${dashPath}.actions.${actionKey}`,
|
|
4715
|
-
`Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` +
|
|
5118
|
+
`Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest6(actionKey, dash.actions),
|
|
4716
5119
|
`Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
|
|
4717
5120
|
);
|
|
4718
5121
|
}
|
|
@@ -4722,7 +5125,7 @@ function validateTranslationReferences(stack) {
|
|
|
4722
5125
|
return findings;
|
|
4723
5126
|
}
|
|
4724
5127
|
function asRecord(v) {
|
|
4725
|
-
return
|
|
5128
|
+
return isRec7(v) ? v : {};
|
|
4726
5129
|
}
|
|
4727
5130
|
function checkOptionKeys(findings, ctx) {
|
|
4728
5131
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -4734,7 +5137,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
4734
5137
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
4735
5138
|
where: ctx.where,
|
|
4736
5139
|
path: ctx.path,
|
|
4737
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
5140
|
+
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName10(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
|
|
4738
5141
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
4739
5142
|
});
|
|
4740
5143
|
return;
|
|
@@ -4747,17 +5150,17 @@ function checkOptionKeys(findings, ctx) {
|
|
|
4747
5150
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
4748
5151
|
where: ctx.where,
|
|
4749
5152
|
path: `${ctx.path}.${key}`,
|
|
4750
|
-
message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` +
|
|
5153
|
+
message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` + suggest6(key, declared.values),
|
|
4751
5154
|
hint: byLabel ? `Rename the key to "${byLabel}".` : `Option keys are the stored \`value\`, not the label and not a variant spelling (\`direct_mail\`, not \`direct-mail\`). Declared values: ${listNames(declared.values)}.`
|
|
4752
5155
|
});
|
|
4753
5156
|
}
|
|
4754
5157
|
}
|
|
4755
5158
|
function checkActionParams(findings, ctx) {
|
|
4756
|
-
const rawParams = Object.keys(asRecord(
|
|
5159
|
+
const rawParams = Object.keys(asRecord(isRec7(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
4757
5160
|
if (rawParams.length === 0) return;
|
|
4758
5161
|
const declared = /* @__PURE__ */ new Set();
|
|
4759
|
-
for (const param of
|
|
4760
|
-
const name =
|
|
5162
|
+
for (const param of asArray29(ctx.action.params)) {
|
|
5163
|
+
const name = strName10(param.name) ?? strName10(param.field);
|
|
4761
5164
|
if (name) declared.add(name);
|
|
4762
5165
|
}
|
|
4763
5166
|
for (const paramName of rawParams) {
|
|
@@ -4767,7 +5170,7 @@ function checkActionParams(findings, ctx) {
|
|
|
4767
5170
|
rule: TRANSLATION_TARGET_UNKNOWN,
|
|
4768
5171
|
where: `${ctx.where} \xB7 param "${paramName}"`,
|
|
4769
5172
|
path: `${ctx.path}.params.${paramName}`,
|
|
4770
|
-
message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` +
|
|
5173
|
+
message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` + suggest6(paramName, declared),
|
|
4771
5174
|
hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
|
|
4772
5175
|
});
|
|
4773
5176
|
}
|
|
@@ -4775,14 +5178,14 @@ function checkActionParams(findings, ctx) {
|
|
|
4775
5178
|
|
|
4776
5179
|
// src/validate-ai-surface-affinity.ts
|
|
4777
5180
|
var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
|
|
4778
|
-
function
|
|
5181
|
+
function asArray30(v) {
|
|
4779
5182
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
4780
5183
|
if (v && typeof v === "object") {
|
|
4781
5184
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4782
5185
|
}
|
|
4783
5186
|
return [];
|
|
4784
5187
|
}
|
|
4785
|
-
function
|
|
5188
|
+
function strName11(v) {
|
|
4786
5189
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4787
5190
|
}
|
|
4788
5191
|
function surfaceOf(v) {
|
|
@@ -4792,18 +5195,18 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
4792
5195
|
const findings = [];
|
|
4793
5196
|
if (!stack || typeof stack !== "object") return findings;
|
|
4794
5197
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
4795
|
-
for (const skill of
|
|
4796
|
-
const n =
|
|
5198
|
+
for (const skill of asArray30(stack.skills)) {
|
|
5199
|
+
const n = strName11(skill.name);
|
|
4797
5200
|
if (n) skillsByName.set(n, skill);
|
|
4798
5201
|
}
|
|
4799
|
-
const agents =
|
|
5202
|
+
const agents = asArray30(stack.agents);
|
|
4800
5203
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
4801
5204
|
const agent = agents[ai];
|
|
4802
|
-
const agentName =
|
|
5205
|
+
const agentName = strName11(agent.name) ?? `#${ai}`;
|
|
4803
5206
|
const agentSurface = surfaceOf(agent.surface);
|
|
4804
5207
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
4805
5208
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
4806
|
-
const ref =
|
|
5209
|
+
const ref = strName11(skillRefs[si]);
|
|
4807
5210
|
if (!ref) continue;
|
|
4808
5211
|
const skill = skillsByName.get(ref);
|
|
4809
5212
|
if (!skill) continue;
|
|
@@ -4823,19 +5226,19 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
4823
5226
|
}
|
|
4824
5227
|
|
|
4825
5228
|
// src/validate-ai-tool-references.ts
|
|
4826
|
-
var
|
|
5229
|
+
var import_system5 = require("@objectstack/spec/system");
|
|
4827
5230
|
var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
|
|
4828
|
-
function
|
|
5231
|
+
function asArray31(v) {
|
|
4829
5232
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
4830
5233
|
if (v && typeof v === "object") {
|
|
4831
5234
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4832
5235
|
}
|
|
4833
5236
|
return [];
|
|
4834
5237
|
}
|
|
4835
|
-
function
|
|
5238
|
+
function strName12(v) {
|
|
4836
5239
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4837
5240
|
}
|
|
4838
|
-
function
|
|
5241
|
+
function distance6(a, b) {
|
|
4839
5242
|
const m = a.length;
|
|
4840
5243
|
const n = b.length;
|
|
4841
5244
|
if (m === 0) return n;
|
|
@@ -4851,14 +5254,14 @@ function distance5(a, b) {
|
|
|
4851
5254
|
}
|
|
4852
5255
|
return prev[n];
|
|
4853
5256
|
}
|
|
4854
|
-
function
|
|
4855
|
-
for (const prefix of
|
|
5257
|
+
function suggest7(target, known) {
|
|
5258
|
+
for (const prefix of import_system5.PLATFORM_TOOL_FAMILY_PREFIXES) {
|
|
4856
5259
|
if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
|
|
4857
5260
|
}
|
|
4858
5261
|
let best;
|
|
4859
5262
|
let bestScore = Infinity;
|
|
4860
5263
|
for (const candidate of known) {
|
|
4861
|
-
const d =
|
|
5264
|
+
const d = distance6(target, candidate);
|
|
4862
5265
|
if (d < bestScore) {
|
|
4863
5266
|
bestScore = d;
|
|
4864
5267
|
best = candidate;
|
|
@@ -4873,26 +5276,26 @@ function materialisesAsTool(action) {
|
|
|
4873
5276
|
if (!ai || typeof ai !== "object") return false;
|
|
4874
5277
|
const aiRec = ai;
|
|
4875
5278
|
if (aiRec.exposed !== true) return false;
|
|
4876
|
-
if (!
|
|
4877
|
-
const type =
|
|
5279
|
+
if (!strName12(aiRec.description)) return false;
|
|
5280
|
+
const type = strName12(action.type);
|
|
4878
5281
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
4879
5282
|
if (type === "script") return Boolean(action.target || action.body);
|
|
4880
5283
|
return Boolean(action.target);
|
|
4881
5284
|
}
|
|
4882
5285
|
function collectToolUniverse(stack) {
|
|
4883
|
-
const universe = new Set(
|
|
4884
|
-
for (const tool of
|
|
4885
|
-
const n =
|
|
5286
|
+
const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
|
|
5287
|
+
for (const tool of asArray31(stack.tools)) {
|
|
5288
|
+
const n = strName12(tool.name);
|
|
4886
5289
|
if (n) universe.add(n);
|
|
4887
5290
|
}
|
|
4888
5291
|
const addActionFamily = (actions) => {
|
|
4889
|
-
for (const action of
|
|
4890
|
-
const n =
|
|
5292
|
+
for (const action of asArray31(actions)) {
|
|
5293
|
+
const n = strName12(action.name);
|
|
4891
5294
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
4892
5295
|
}
|
|
4893
5296
|
};
|
|
4894
5297
|
addActionFamily(stack.actions);
|
|
4895
|
-
for (const obj of
|
|
5298
|
+
for (const obj of asArray31(stack.objects)) {
|
|
4896
5299
|
addActionFamily(obj.actions);
|
|
4897
5300
|
}
|
|
4898
5301
|
return universe;
|
|
@@ -4900,13 +5303,13 @@ function collectToolUniverse(stack) {
|
|
|
4900
5303
|
function collectUnexposedActionNames(stack) {
|
|
4901
5304
|
const names = /* @__PURE__ */ new Set();
|
|
4902
5305
|
const scan = (actions) => {
|
|
4903
|
-
for (const action of
|
|
4904
|
-
const n =
|
|
5306
|
+
for (const action of asArray31(actions)) {
|
|
5307
|
+
const n = strName12(action.name);
|
|
4905
5308
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
4906
5309
|
}
|
|
4907
5310
|
};
|
|
4908
5311
|
scan(stack.actions);
|
|
4909
|
-
for (const obj of
|
|
5312
|
+
for (const obj of asArray31(stack.objects)) scan(obj.actions);
|
|
4910
5313
|
return names;
|
|
4911
5314
|
}
|
|
4912
5315
|
function validateAiToolReferences(stack) {
|
|
@@ -4924,13 +5327,13 @@ function validateAiToolReferences(stack) {
|
|
|
4924
5327
|
}
|
|
4925
5328
|
return universe.has(ref);
|
|
4926
5329
|
};
|
|
4927
|
-
const skills =
|
|
5330
|
+
const skills = asArray31(stack.skills);
|
|
4928
5331
|
for (let si = 0; si < skills.length; si++) {
|
|
4929
5332
|
const skill = skills[si];
|
|
4930
|
-
const skillName =
|
|
5333
|
+
const skillName = strName12(skill.name) ?? `#${si}`;
|
|
4931
5334
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
4932
5335
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
4933
|
-
const ref =
|
|
5336
|
+
const ref = strName12(refs[ti]);
|
|
4934
5337
|
if (!ref || resolves(ref)) continue;
|
|
4935
5338
|
const isPattern = ref.endsWith("*");
|
|
4936
5339
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -4939,8 +5342,8 @@ function validateAiToolReferences(stack) {
|
|
|
4939
5342
|
rule: AI_SKILL_TOOL_UNRESOLVED,
|
|
4940
5343
|
where: `skill "${skillName}" \xB7 tools`,
|
|
4941
5344
|
path: `skills[${si}].tools[${ti}]`,
|
|
4942
|
-
message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` +
|
|
4943
|
-
hint: unexposed ? `Either opt "${unexposed}" in \u2014 set \`ai: { exposed: true, description: '\u2026' }\` (\u226540 chars, LLM-facing) and give it a headless type \u2014 or drop the reference and have the skill's instructions recommend the UI action instead. A \`modal\`/\`form\`/\`url\` action stays human-driven by design; that is a legitimate answer, not a gap.` : `Back "${ref}" with a real executable: declare a declarative action (or flow), opt it in with \`ai.exposed: true\` + \`ai.description\`, and reference its materialised tool (\`action_<name>\` \u2014 the ADR-0109 default path, no tool record needed); or reference a platform tool by its registered name; or remove the reference and the instructions that mention it. Ignore this only if a runtime plugin outside the platform registry provides "${ref}". Family prefixes materialised by the runtime: ${
|
|
5345
|
+
message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` + suggest7(ref, universe),
|
|
5346
|
+
hint: unexposed ? `Either opt "${unexposed}" in \u2014 set \`ai: { exposed: true, description: '\u2026' }\` (\u226540 chars, LLM-facing) and give it a headless type \u2014 or drop the reference and have the skill's instructions recommend the UI action instead. A \`modal\`/\`form\`/\`url\` action stays human-driven by design; that is a legitimate answer, not a gap.` : `Back "${ref}" with a real executable: declare a declarative action (or flow), opt it in with \`ai.exposed: true\` + \`ai.description\`, and reference its materialised tool (\`action_<name>\` \u2014 the ADR-0109 default path, no tool record needed); or reference a platform tool by its registered name; or remove the reference and the instructions that mention it. Ignore this only if a runtime plugin outside the platform registry provides "${ref}". Family prefixes materialised by the runtime: ${import_system5.PLATFORM_TOOL_FAMILY_PREFIXES.join(", ")}.`
|
|
4944
5347
|
});
|
|
4945
5348
|
}
|
|
4946
5349
|
}
|
|
@@ -4949,24 +5352,24 @@ function validateAiToolReferences(stack) {
|
|
|
4949
5352
|
|
|
4950
5353
|
// src/validate-ai-agent-authoring.ts
|
|
4951
5354
|
var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
|
|
4952
|
-
function
|
|
5355
|
+
function asArray32(v) {
|
|
4953
5356
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
4954
5357
|
if (v && typeof v === "object") {
|
|
4955
5358
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4956
5359
|
}
|
|
4957
5360
|
return [];
|
|
4958
5361
|
}
|
|
4959
|
-
function
|
|
5362
|
+
function strName13(v) {
|
|
4960
5363
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4961
5364
|
}
|
|
4962
5365
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
4963
5366
|
function validateAiAgentAuthoring(stack) {
|
|
4964
5367
|
const findings = [];
|
|
4965
5368
|
if (!stack || typeof stack !== "object") return findings;
|
|
4966
|
-
const agents =
|
|
5369
|
+
const agents = asArray32(stack.agents);
|
|
4967
5370
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
4968
5371
|
const agent = agents[ai];
|
|
4969
|
-
const name =
|
|
5372
|
+
const name = strName13(agent.name) ?? `#${ai}`;
|
|
4970
5373
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
4971
5374
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
4972
5375
|
findings.push({
|
|
@@ -4981,9 +5384,495 @@ function validateAiAgentAuthoring(stack) {
|
|
|
4981
5384
|
return findings;
|
|
4982
5385
|
}
|
|
4983
5386
|
|
|
5387
|
+
// src/validate-hook-body-writes.ts
|
|
5388
|
+
var import_node_module3 = require("module");
|
|
5389
|
+
var import_shared = require("@objectstack/spec/shared");
|
|
5390
|
+
var import_meta3 = {};
|
|
5391
|
+
var cachedTs2 = null;
|
|
5392
|
+
function loadTypeScript2() {
|
|
5393
|
+
if (cachedTs2) return cachedTs2;
|
|
5394
|
+
const anchor = typeof import_meta3 !== "undefined" && import_meta3.url ? import_meta3.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
5395
|
+
try {
|
|
5396
|
+
cachedTs2 = (0, import_node_module3.createRequire)(anchor)("typescript");
|
|
5397
|
+
} catch (err) {
|
|
5398
|
+
throw new Error(
|
|
5399
|
+
`@objectstack/lint: checking an L2 (language:'js') hook body requires the "typescript" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "typescript" in the image; it is only loaded when a hook with a JS body is validated.`
|
|
5400
|
+
);
|
|
5401
|
+
}
|
|
5402
|
+
return cachedTs2;
|
|
5403
|
+
}
|
|
5404
|
+
var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
|
|
5405
|
+
var HOOK_BODY_WRITE_PATTERNS = [
|
|
5406
|
+
{
|
|
5407
|
+
id: "input-property-assign",
|
|
5408
|
+
syntax: "ctx.input.<field> = \u2026 | ctx.input['<field>'] \u27E8op\u27E9= \u2026",
|
|
5409
|
+
example: {
|
|
5410
|
+
// Compound (`+=`) and logical (`??=`) assignment operators write their
|
|
5411
|
+
// LHS exactly like `=` does — the example pins the whole operator range.
|
|
5412
|
+
source: "ctx.input.total = 0; ctx.input['status'] ??= 'open'; ctx.input.retries += 1;",
|
|
5413
|
+
writes: [{ field: "total" }, { field: "status" }, { field: "retries" }]
|
|
5414
|
+
}
|
|
5415
|
+
},
|
|
5416
|
+
{
|
|
5417
|
+
id: "input-object-assign",
|
|
5418
|
+
syntax: "Object.assign(ctx.input, { <field>: \u2026 })",
|
|
5419
|
+
example: {
|
|
5420
|
+
source: "Object.assign(ctx.input, { total: 5, 'status': 'open', discount });",
|
|
5421
|
+
writes: [{ field: "total" }, { field: "status" }, { field: "discount" }]
|
|
5422
|
+
}
|
|
5423
|
+
},
|
|
5424
|
+
{
|
|
5425
|
+
// ACTION-only shape (the hook sandbox context has no `ctx.record` at all).
|
|
5426
|
+
// Declared here because this ledger is the extractor's shape inventory, not
|
|
5427
|
+
// any one rule's; every consumer declares which shapes it consumes.
|
|
5428
|
+
id: "record-property-assign",
|
|
5429
|
+
syntax: "ctx.record.<field> = \u2026 | ctx.record['<field>'] \u27E8op\u27E9= \u2026",
|
|
5430
|
+
example: {
|
|
5431
|
+
source: "ctx.record.stage = 'won'; ctx.record['amount'] += 1;",
|
|
5432
|
+
writes: [{ field: "stage" }, { field: "amount" }]
|
|
5433
|
+
}
|
|
5434
|
+
},
|
|
5435
|
+
{
|
|
5436
|
+
id: "api-crud-literal",
|
|
5437
|
+
syntax: "ctx.api.object('<object>').insert({\u2026}) | .create({\u2026}) | .update({\u2026}) | .updateById(id, {\u2026})",
|
|
5438
|
+
example: {
|
|
5439
|
+
// Real ObjectRepository signatures: the record payload is argument 0 for
|
|
5440
|
+
// insert/create/update and argument 1 for updateById. (`update(data)` —
|
|
5441
|
+
// NOT `update(id, data)`; the id travels inside the payload/options.)
|
|
5442
|
+
source: "await ctx.api.object('audit_log').insert({ event: 'won' }); await ctx.api.object('crm_deal').updateById(id, { stage: 'won' });",
|
|
5443
|
+
writes: [
|
|
5444
|
+
{ field: "event", object: "audit_log" },
|
|
5445
|
+
{ field: "stage", object: "crm_deal" }
|
|
5446
|
+
]
|
|
5447
|
+
}
|
|
5448
|
+
}
|
|
5449
|
+
];
|
|
5450
|
+
var HOOK_BODY_WRITE_PATTERN_IDS = [
|
|
5451
|
+
"input-property-assign",
|
|
5452
|
+
"input-object-assign",
|
|
5453
|
+
"api-crud-literal"
|
|
5454
|
+
];
|
|
5455
|
+
var HOOK_BODY_WRITE_EXCLUSIONS = [
|
|
5456
|
+
{
|
|
5457
|
+
id: "record-property-assign",
|
|
5458
|
+
reason: "a hook sandbox context has no `ctx.record` at all \u2014 `buildSandboxContext` never sets it (a hook\u2019s record IS `ctx.input`), so the expression throws at run time rather than silently no-op\u2019ing. A loud failure the author sees on the first run is not this advisory rule\u2019s business"
|
|
5459
|
+
}
|
|
5460
|
+
];
|
|
5461
|
+
var HOOK_APPLICABLE_IDS = new Set(HOOK_BODY_WRITE_PATTERN_IDS);
|
|
5462
|
+
var API_WRITE_METHODS = /* @__PURE__ */ new Map([
|
|
5463
|
+
["insert", 0],
|
|
5464
|
+
["create", 0],
|
|
5465
|
+
["update", 0],
|
|
5466
|
+
["updateById", 1]
|
|
5467
|
+
]);
|
|
5468
|
+
var INPUT_ENVELOPE_KEYS = /* @__PURE__ */ new Set(["id", "options", "ast", "data"]);
|
|
5469
|
+
var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
5470
|
+
...SYSTEM_FIELDS,
|
|
5471
|
+
"_id",
|
|
5472
|
+
"name",
|
|
5473
|
+
"space",
|
|
5474
|
+
"owner",
|
|
5475
|
+
"record_type"
|
|
5476
|
+
]);
|
|
5477
|
+
var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5478
|
+
function asArray33(v) {
|
|
5479
|
+
if (Array.isArray(v)) return v.filter((x) => isRec8(x));
|
|
5480
|
+
if (isRec8(v)) {
|
|
5481
|
+
return Object.entries(v).map(([name, def]) => ({
|
|
5482
|
+
name,
|
|
5483
|
+
...isRec8(def) ? def : {}
|
|
5484
|
+
}));
|
|
5485
|
+
}
|
|
5486
|
+
return [];
|
|
5487
|
+
}
|
|
5488
|
+
function indexObjectFields2(stack) {
|
|
5489
|
+
const out = /* @__PURE__ */ new Map();
|
|
5490
|
+
for (const obj of asArray33(stack.objects)) {
|
|
5491
|
+
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
5492
|
+
if (!name) continue;
|
|
5493
|
+
const names = /* @__PURE__ */ new Set();
|
|
5494
|
+
for (const f of asArray33(obj.fields)) {
|
|
5495
|
+
if (typeof f.name === "string" && f.name) names.add(f.name);
|
|
5496
|
+
}
|
|
5497
|
+
out.set(name, names);
|
|
5498
|
+
}
|
|
5499
|
+
return out;
|
|
5500
|
+
}
|
|
5501
|
+
function judgeableFieldsOf(index, objectName) {
|
|
5502
|
+
const declared = index.get(objectName);
|
|
5503
|
+
if (!declared || declared.size === 0) return void 0;
|
|
5504
|
+
return declared;
|
|
5505
|
+
}
|
|
5506
|
+
function extractHookBodyWrites(source) {
|
|
5507
|
+
return extractHookBodyWriteSet(source).writes;
|
|
5508
|
+
}
|
|
5509
|
+
function extractHookBodyWriteSet(source) {
|
|
5510
|
+
if (!/\bctx\b/.test(source) && !/\bObject\b/.test(source)) {
|
|
5511
|
+
return { writes: [], ctxRecordEscapes: false };
|
|
5512
|
+
}
|
|
5513
|
+
const tsc = loadTypeScript2();
|
|
5514
|
+
const sf = tsc.createSourceFile(
|
|
5515
|
+
"hook-body.ts",
|
|
5516
|
+
`async function __body(ctx) {
|
|
5517
|
+
${source}
|
|
5518
|
+
}`,
|
|
5519
|
+
tsc.ScriptTarget.Latest,
|
|
5520
|
+
/* setParentNodes */
|
|
5521
|
+
false,
|
|
5522
|
+
tsc.ScriptKind.TS
|
|
5523
|
+
);
|
|
5524
|
+
const writes = [];
|
|
5525
|
+
const recordRefs = [];
|
|
5526
|
+
const consumedRecordRefs = /* @__PURE__ */ new Set();
|
|
5527
|
+
const isCtxDot = (node, prop) => tsc.isPropertyAccessExpression(node) && tsc.isIdentifier(node.expression) && node.expression.text === "ctx" && node.name.text === prop;
|
|
5528
|
+
const fieldOfCtxLhs = (lhs, prop) => {
|
|
5529
|
+
if (tsc.isPropertyAccessExpression(lhs) && tsc.isIdentifier(lhs.name) && isCtxDot(lhs.expression, prop)) {
|
|
5530
|
+
return lhs.name.text;
|
|
5531
|
+
}
|
|
5532
|
+
if (tsc.isElementAccessExpression(lhs) && isCtxDot(lhs.expression, prop)) {
|
|
5533
|
+
const arg = lhs.argumentExpression;
|
|
5534
|
+
if (tsc.isStringLiteral(arg) || tsc.isNoSubstitutionTemplateLiteral(arg)) return arg.text;
|
|
5535
|
+
}
|
|
5536
|
+
return void 0;
|
|
5537
|
+
};
|
|
5538
|
+
const literalObjectKeys = (node) => {
|
|
5539
|
+
if (!tsc.isObjectLiteralExpression(node)) return [];
|
|
5540
|
+
const keys = [];
|
|
5541
|
+
for (const p of node.properties) {
|
|
5542
|
+
if (tsc.isPropertyAssignment(p)) {
|
|
5543
|
+
if (tsc.isIdentifier(p.name) || tsc.isStringLiteral(p.name)) keys.push(p.name.text);
|
|
5544
|
+
} else if (tsc.isShorthandPropertyAssignment(p)) {
|
|
5545
|
+
keys.push(p.name.text);
|
|
5546
|
+
}
|
|
5547
|
+
}
|
|
5548
|
+
return keys;
|
|
5549
|
+
};
|
|
5550
|
+
const visit = (node) => {
|
|
5551
|
+
if (tsc.isBinaryExpression(node) && node.operatorToken.kind >= tsc.SyntaxKind.FirstAssignment && node.operatorToken.kind <= tsc.SyntaxKind.LastAssignment) {
|
|
5552
|
+
const inputField = fieldOfCtxLhs(node.left, "input");
|
|
5553
|
+
if (inputField !== void 0 && !INPUT_ENVELOPE_KEYS.has(inputField)) {
|
|
5554
|
+
writes.push({ patternId: "input-property-assign", field: inputField });
|
|
5555
|
+
}
|
|
5556
|
+
const recordField = fieldOfCtxLhs(node.left, "record");
|
|
5557
|
+
if (recordField !== void 0) {
|
|
5558
|
+
writes.push({ patternId: "record-property-assign", field: recordField });
|
|
5559
|
+
}
|
|
5560
|
+
}
|
|
5561
|
+
if (tsc.isPropertyAccessExpression(node) || tsc.isElementAccessExpression(node)) {
|
|
5562
|
+
if (isCtxDot(node.expression, "record")) consumedRecordRefs.add(node.expression);
|
|
5563
|
+
}
|
|
5564
|
+
if (tsc.isBinaryExpression(node)) {
|
|
5565
|
+
const op = node.operatorToken.kind;
|
|
5566
|
+
if ((op === tsc.SyntaxKind.AmpersandAmpersandToken || op === tsc.SyntaxKind.BarBarToken || op === tsc.SyntaxKind.QuestionQuestionToken) && isCtxDot(node.left, "record")) {
|
|
5567
|
+
consumedRecordRefs.add(node.left);
|
|
5568
|
+
}
|
|
5569
|
+
}
|
|
5570
|
+
if (tsc.isPrefixUnaryExpression(node) && node.operator === tsc.SyntaxKind.ExclamationToken) {
|
|
5571
|
+
if (isCtxDot(node.operand, "record")) consumedRecordRefs.add(node.operand);
|
|
5572
|
+
}
|
|
5573
|
+
if (tsc.isTypeOfExpression(node) && isCtxDot(node.expression, "record")) {
|
|
5574
|
+
consumedRecordRefs.add(node.expression);
|
|
5575
|
+
}
|
|
5576
|
+
if ((tsc.isIfStatement(node) || tsc.isWhileStatement(node) || tsc.isDoStatement(node)) && isCtxDot(node.expression, "record")) {
|
|
5577
|
+
consumedRecordRefs.add(node.expression);
|
|
5578
|
+
}
|
|
5579
|
+
if (tsc.isConditionalExpression(node) && isCtxDot(node.condition, "record")) {
|
|
5580
|
+
consumedRecordRefs.add(node.condition);
|
|
5581
|
+
}
|
|
5582
|
+
if (isCtxDot(node, "record")) recordRefs.push(node);
|
|
5583
|
+
if (tsc.isCallExpression(node)) {
|
|
5584
|
+
const callee = node.expression;
|
|
5585
|
+
if (tsc.isPropertyAccessExpression(callee) && tsc.isIdentifier(callee.expression) && callee.expression.text === "Object" && callee.name.text === "assign" && node.arguments.length >= 2 && isCtxDot(node.arguments[0], "input")) {
|
|
5586
|
+
for (const arg of node.arguments.slice(1)) {
|
|
5587
|
+
for (const field of literalObjectKeys(arg)) {
|
|
5588
|
+
if (!INPUT_ENVELOPE_KEYS.has(field)) {
|
|
5589
|
+
writes.push({ patternId: "input-object-assign", field });
|
|
5590
|
+
}
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
if (tsc.isPropertyAccessExpression(callee) && tsc.isIdentifier(callee.name)) {
|
|
5595
|
+
const payloadIndex = API_WRITE_METHODS.get(callee.name.text);
|
|
5596
|
+
const recv = callee.expression;
|
|
5597
|
+
if (payloadIndex !== void 0 && tsc.isCallExpression(recv) && tsc.isPropertyAccessExpression(recv.expression) && recv.expression.name.text === "object" && isCtxDot(recv.expression.expression, "api") && recv.arguments.length === 1) {
|
|
5598
|
+
const objArg = recv.arguments[0];
|
|
5599
|
+
const objectName = tsc.isStringLiteral(objArg) || tsc.isNoSubstitutionTemplateLiteral(objArg) ? objArg.text : void 0;
|
|
5600
|
+
const payload = node.arguments[payloadIndex];
|
|
5601
|
+
if (objectName && payload !== void 0) {
|
|
5602
|
+
for (const field of literalObjectKeys(payload)) {
|
|
5603
|
+
writes.push({
|
|
5604
|
+
patternId: "api-crud-literal",
|
|
5605
|
+
object: objectName,
|
|
5606
|
+
method: callee.name.text,
|
|
5607
|
+
field
|
|
5608
|
+
});
|
|
5609
|
+
}
|
|
5610
|
+
}
|
|
5611
|
+
}
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
tsc.forEachChild(node, visit);
|
|
5615
|
+
};
|
|
5616
|
+
visit(sf);
|
|
5617
|
+
return {
|
|
5618
|
+
writes,
|
|
5619
|
+
ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref))
|
|
5620
|
+
};
|
|
5621
|
+
}
|
|
5622
|
+
function validateHookBodyWrites(stack) {
|
|
5623
|
+
const findings = [];
|
|
5624
|
+
const hooks = asArray33(stack.hooks);
|
|
5625
|
+
if (hooks.length === 0) return findings;
|
|
5626
|
+
let objectFields = null;
|
|
5627
|
+
hooks.forEach((hook, hookIndex) => {
|
|
5628
|
+
const body = hook.body;
|
|
5629
|
+
if (!isRec8(body) || body.language !== "js") return;
|
|
5630
|
+
const source = body.source;
|
|
5631
|
+
if (typeof source !== "string" || source.trim() === "") return;
|
|
5632
|
+
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
5633
|
+
if (writes.length === 0) return;
|
|
5634
|
+
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
5635
|
+
const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
|
|
5636
|
+
const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
|
|
5637
|
+
(o) => typeof o === "string" && o.trim() !== ""
|
|
5638
|
+
);
|
|
5639
|
+
const targetSets = targets.map((t) => judgeableFieldsOf(objectFields, t));
|
|
5640
|
+
const inputJudgeable = targets.length > 0 && !targets.includes("*") && targetSets.every((s) => s !== void 0);
|
|
5641
|
+
const where = `hook "${hookName}" \u203A body`;
|
|
5642
|
+
const path = `hooks[${hookIndex}].body.source`;
|
|
5643
|
+
const reported = /* @__PURE__ */ new Set();
|
|
5644
|
+
for (const w of writes) {
|
|
5645
|
+
const dedupeKey = `${w.object ?? ""}\0${w.field}`;
|
|
5646
|
+
if (reported.has(dedupeKey)) continue;
|
|
5647
|
+
if (w.object === void 0) {
|
|
5648
|
+
if (!inputJudgeable) continue;
|
|
5649
|
+
if (IMPLICIT_FIELDS2.has(w.field)) continue;
|
|
5650
|
+
if (targetSets.some((s) => s.has(w.field))) continue;
|
|
5651
|
+
reported.add(dedupeKey);
|
|
5652
|
+
const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
|
|
5653
|
+
const declares = targets.length === 1 ? "declares no such field" : "declare that field";
|
|
5654
|
+
findings.push({
|
|
5655
|
+
severity: "warning",
|
|
5656
|
+
rule: HOOK_BODY_WRITE_UNKNOWN_FIELD,
|
|
5657
|
+
where,
|
|
5658
|
+
path,
|
|
5659
|
+
message: `body writes '${w.field}' to its input, but ${objDesc} ${declares}. The sandboxed script runs clean and the value is copied back onto the record payload unfiltered \u2014 on a SQL driver the stray column then fails the WHOLE write with a driver-level error far from here; on a schemaless driver (memory, MongoDB) it is persisted as an undeclared key (#4271).`,
|
|
5660
|
+
hint: fixHint(w.field, unionCandidates(targetSets))
|
|
5661
|
+
});
|
|
5662
|
+
} else {
|
|
5663
|
+
const known = judgeableFieldsOf(objectFields, w.object);
|
|
5664
|
+
if (!known) continue;
|
|
5665
|
+
if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
|
|
5666
|
+
reported.add(dedupeKey);
|
|
5667
|
+
findings.push({
|
|
5668
|
+
severity: "warning",
|
|
5669
|
+
rule: HOOK_BODY_WRITE_UNKNOWN_FIELD,
|
|
5670
|
+
where,
|
|
5671
|
+
path,
|
|
5672
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', but object '${w.object}' declares no such field. The write-path validator skips the unknown key \u2014 on a SQL driver the whole call then fails with a driver-level error far from here; on a schemaless driver (memory, MongoDB) the stray key is persisted (#4271).`,
|
|
5673
|
+
hint: fixHint(w.field, [...known])
|
|
5674
|
+
});
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5677
|
+
});
|
|
5678
|
+
return findings;
|
|
5679
|
+
}
|
|
5680
|
+
function unionCandidates(targetSets) {
|
|
5681
|
+
const out = /* @__PURE__ */ new Set();
|
|
5682
|
+
for (const s of targetSets) for (const f of s ?? []) out.add(f);
|
|
5683
|
+
return [...out];
|
|
5684
|
+
}
|
|
5685
|
+
function fixHint(field, declared) {
|
|
5686
|
+
const suggestion = (0, import_shared.formatSuggestion)((0, import_shared.findClosestMatches)(field, [...declared, ...IMPLICIT_FIELDS2]));
|
|
5687
|
+
return (suggestion ? `${suggestion} ` : "") + `Fix the field name, or declare '${field}' on the object. Only the literal write patterns in HOOK_BODY_WRITE_PATTERNS are checked \u2014 computed keys, spreads and aliased input are not \u2014 and this warning never blocks a build.`;
|
|
5688
|
+
}
|
|
5689
|
+
|
|
5690
|
+
// src/validate-action-body-writes.ts
|
|
5691
|
+
var import_shared2 = require("@objectstack/spec/shared");
|
|
5692
|
+
var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
|
|
5693
|
+
var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
|
|
5694
|
+
var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
|
|
5695
|
+
var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
|
|
5696
|
+
var ACTION_BODY_WRITE_EXCLUSIONS = [
|
|
5697
|
+
{
|
|
5698
|
+
id: "input-property-assign",
|
|
5699
|
+
reason: "an action's ctx.input is its params bag (`input: unwrapProxyToPlain(actionCtx?.params)`), not a record \u2014 `ctx.input.<name>` writes a declared PARAMETER, which object fields cannot judge"
|
|
5700
|
+
},
|
|
5701
|
+
{
|
|
5702
|
+
id: "input-object-assign",
|
|
5703
|
+
reason: "same surface as input-property-assign \u2014 Object.assign(ctx.input, \u2026) targets the params bag"
|
|
5704
|
+
}
|
|
5705
|
+
];
|
|
5706
|
+
var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_BODY_WRITE_PATTERN_IDS.includes(p.id));
|
|
5707
|
+
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
5708
|
+
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
5709
|
+
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
5710
|
+
var isRec9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5711
|
+
function asArray34(v) {
|
|
5712
|
+
if (Array.isArray(v)) return v.filter((x) => isRec9(x));
|
|
5713
|
+
if (isRec9(v)) {
|
|
5714
|
+
return Object.entries(v).map(([name, def]) => ({
|
|
5715
|
+
name,
|
|
5716
|
+
...isRec9(def) ? def : {}
|
|
5717
|
+
}));
|
|
5718
|
+
}
|
|
5719
|
+
return [];
|
|
5720
|
+
}
|
|
5721
|
+
function actionObjectBinding(action, parentObject) {
|
|
5722
|
+
if (typeof action.object === "string" && action.object) return action.object;
|
|
5723
|
+
if (typeof action.objectName === "string" && action.objectName) return action.objectName;
|
|
5724
|
+
return parentObject;
|
|
5725
|
+
}
|
|
5726
|
+
function collectActionBodies(stack) {
|
|
5727
|
+
const sites = [];
|
|
5728
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5729
|
+
const collect = (actions, pathPrefix, parentObject) => {
|
|
5730
|
+
asArray34(actions).forEach((action, index) => {
|
|
5731
|
+
const body = action.body;
|
|
5732
|
+
if (!isRec9(body) || body.language !== "js") return;
|
|
5733
|
+
const source = body.source;
|
|
5734
|
+
if (typeof source !== "string" || source.trim() === "") return;
|
|
5735
|
+
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
5736
|
+
const key = `${actionObjectBinding(action, parentObject) ?? ""}\0${name}\0${source}`;
|
|
5737
|
+
if (seen.has(key)) return;
|
|
5738
|
+
seen.add(key);
|
|
5739
|
+
sites.push({ name, source, path: `${pathPrefix}[${index}].body.source` });
|
|
5740
|
+
});
|
|
5741
|
+
};
|
|
5742
|
+
collect(stack.actions, "actions");
|
|
5743
|
+
asArray34(stack.objects).forEach((obj, objIndex) => {
|
|
5744
|
+
const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
|
|
5745
|
+
collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
|
|
5746
|
+
});
|
|
5747
|
+
return sites;
|
|
5748
|
+
}
|
|
5749
|
+
function validateActionBodyWrites(stack) {
|
|
5750
|
+
const findings = [];
|
|
5751
|
+
if (!isRec9(stack)) return findings;
|
|
5752
|
+
const sites = collectActionBodies(stack);
|
|
5753
|
+
if (sites.length === 0) return findings;
|
|
5754
|
+
let objectFields = null;
|
|
5755
|
+
for (const site of sites) {
|
|
5756
|
+
if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
|
|
5757
|
+
const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
|
|
5758
|
+
const writes = allWrites.filter((w) => APPLICABLE_IDS.has(w.patternId));
|
|
5759
|
+
const recordWrites = allWrites.filter((w) => RECORD_WRITE_IDS.has(w.patternId));
|
|
5760
|
+
if (writes.length === 0 && recordWrites.length === 0) continue;
|
|
5761
|
+
const where = `action "${site.name}" \u203A body`;
|
|
5762
|
+
if (recordWrites.length > 0 && !ctxRecordEscapes) {
|
|
5763
|
+
const reportedFields = /* @__PURE__ */ new Set();
|
|
5764
|
+
for (const w of recordWrites) {
|
|
5765
|
+
if (reportedFields.has(w.field)) continue;
|
|
5766
|
+
reportedFields.add(w.field);
|
|
5767
|
+
findings.push({
|
|
5768
|
+
severity: "warning",
|
|
5769
|
+
rule: ACTION_RECORD_WRITE_DISCARDED,
|
|
5770
|
+
where,
|
|
5771
|
+
path: site.path,
|
|
5772
|
+
message: `body assigns ctx.record.${w.field}, but an action's ctx.record is a plain snapshot the runtime never writes back \u2014 the action returns success and the assignment is discarded, whether or not '${w.field}' is a declared field (#4345).`,
|
|
5773
|
+
hint: `To persist it, write through the API: ctx.api.object('<object>').updateById(ctx.recordId, { ${w.field}: \u2026 }). Reported only because ctx.record is never passed anywhere in this body \u2014 mutating the snapshot and then handing it to an API write is a live payload and is not flagged. This warning never blocks a build.`
|
|
5774
|
+
});
|
|
5775
|
+
}
|
|
5776
|
+
}
|
|
5777
|
+
if (writes.length === 0) continue;
|
|
5778
|
+
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
5779
|
+
const reported = /* @__PURE__ */ new Set();
|
|
5780
|
+
for (const w of writes) {
|
|
5781
|
+
if (w.object === void 0) continue;
|
|
5782
|
+
const dedupeKey = `${w.object}\0${w.field}`;
|
|
5783
|
+
if (reported.has(dedupeKey)) continue;
|
|
5784
|
+
const known = judgeableFieldsOf(objectFields, w.object);
|
|
5785
|
+
if (!known) continue;
|
|
5786
|
+
if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
|
|
5787
|
+
reported.add(dedupeKey);
|
|
5788
|
+
findings.push({
|
|
5789
|
+
severity: "warning",
|
|
5790
|
+
rule: ACTION_BODY_WRITE_UNKNOWN_FIELD,
|
|
5791
|
+
where,
|
|
5792
|
+
path: site.path,
|
|
5793
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', but object '${w.object}' declares no such field. The write-path validator skips the unknown key \u2014 on a SQL driver the whole action then fails with a driver-level error far from here; on a schemaless driver (memory, MongoDB) the stray key is persisted (#4271).`,
|
|
5794
|
+
hint: fixHint2(w.field, [...known])
|
|
5795
|
+
});
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
return findings;
|
|
5799
|
+
}
|
|
5800
|
+
function fixHint2(field, declared) {
|
|
5801
|
+
const suggestion = (0, import_shared2.formatSuggestion)((0, import_shared2.findClosestMatches)(field, [...declared, ...IMPLICIT_FIELDS2]));
|
|
5802
|
+
return (suggestion ? `${suggestion} ` : "") + `Fix the field name, or declare '${field}' on the object. Only the literal write patterns in ACTION_BODY_WRITE_PATTERNS are checked \u2014 an action's ctx.input is its params bag, so it is not a record-write surface and is never resolved against fields \u2014 and this warning never blocks a build.`;
|
|
5803
|
+
}
|
|
5804
|
+
|
|
5805
|
+
// src/validate-flow-node-writes.ts
|
|
5806
|
+
var import_shared3 = require("@objectstack/spec/shared");
|
|
5807
|
+
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
5808
|
+
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
5809
|
+
var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
|
|
5810
|
+
var isRec10 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5811
|
+
function asArray35(v) {
|
|
5812
|
+
if (Array.isArray(v)) return v.filter((x) => isRec10(x));
|
|
5813
|
+
if (isRec10(v)) {
|
|
5814
|
+
return Object.entries(v).map(([name, def]) => ({
|
|
5815
|
+
name,
|
|
5816
|
+
...isRec10(def) ? def : {}
|
|
5817
|
+
}));
|
|
5818
|
+
}
|
|
5819
|
+
return [];
|
|
5820
|
+
}
|
|
5821
|
+
function readLiteralObjectName2(config) {
|
|
5822
|
+
const raw = config.objectName ?? config.object;
|
|
5823
|
+
if (typeof raw !== "string" || raw.includes("{")) return void 0;
|
|
5824
|
+
return raw || void 0;
|
|
5825
|
+
}
|
|
5826
|
+
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
5827
|
+
function validateFlowNodeWrites(stack) {
|
|
5828
|
+
const findings = [];
|
|
5829
|
+
if (!isRec10(stack)) return findings;
|
|
5830
|
+
const flows = asArray35(stack.flows);
|
|
5831
|
+
if (flows.length === 0) return findings;
|
|
5832
|
+
let objectFields = null;
|
|
5833
|
+
flows.forEach((flow, flowIndex) => {
|
|
5834
|
+
const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
|
|
5835
|
+
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
5836
|
+
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
5837
|
+
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
5838
|
+
const config = isRec10(node.config) ? node.config : void 0;
|
|
5839
|
+
if (!config) return;
|
|
5840
|
+
const fields = config.fields;
|
|
5841
|
+
if (!isRec10(fields)) return;
|
|
5842
|
+
const written = Object.keys(fields);
|
|
5843
|
+
if (written.length === 0) return;
|
|
5844
|
+
const objectName = readLiteralObjectName2(config);
|
|
5845
|
+
if (!objectName) return;
|
|
5846
|
+
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
5847
|
+
const known = judgeableFieldsOf(objectFields, objectName);
|
|
5848
|
+
if (!known) return;
|
|
5849
|
+
const nodeName = flowNodeLabel(node, walkIndex);
|
|
5850
|
+
const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
|
|
5851
|
+
for (const fieldName of written) {
|
|
5852
|
+
if (known.has(fieldName) || IMPLICIT_FIELDS2.has(fieldName)) continue;
|
|
5853
|
+
if (fieldName.includes(".")) continue;
|
|
5854
|
+
findings.push({
|
|
5855
|
+
severity: "error",
|
|
5856
|
+
rule: FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
5857
|
+
where: `flow "${flowName}" \u203A ${nodeWhere}`,
|
|
5858
|
+
path: `${nodePath}.config.fields.${fieldName}`,
|
|
5859
|
+
message: `${node.type} writes '${fieldName}', but object '${objectName}' declares no such field. Nothing between the node and storage removes the key: on a SQL datasource the driver rejects the whole statement ('no such column'), so the correctly named fields in this same payload never land either${node.type === "create_record" ? " and the record is never created at all" : ""}; on a schemaless one the stray key is persisted into a column no read surface returns.`,
|
|
5860
|
+
hint: fixHint3(fieldName, [...known])
|
|
5861
|
+
});
|
|
5862
|
+
}
|
|
5863
|
+
});
|
|
5864
|
+
});
|
|
5865
|
+
return findings;
|
|
5866
|
+
}
|
|
5867
|
+
function fixHint3(field, declared) {
|
|
5868
|
+
const suggestion = (0, import_shared3.formatSuggestion)((0, import_shared3.findClosestMatches)(field, [...declared, ...IMPLICIT_FIELDS2]));
|
|
5869
|
+
return (suggestion ? `${suggestion} ` : "") + `Fix the field name, or declare '${field}' on the object. This gates the build rather than warning: the key is literal and so is the object, so unlike the hook/action body rules there is nothing here that could have been mis-extracted.`;
|
|
5870
|
+
}
|
|
5871
|
+
|
|
4984
5872
|
// src/reference-integrity-suite.ts
|
|
4985
5873
|
var REFERENCE_INTEGRITY_RULES = [
|
|
4986
5874
|
{ name: "validateObjectReferences", run: validateObjectReferences },
|
|
5875
|
+
{ name: "validateSearchableFields", run: validateSearchableFields },
|
|
4987
5876
|
{ name: "validateActionNameRefs", run: validateActionNameRefs },
|
|
4988
5877
|
{ name: "validatePageFieldBindings", run: validatePageFieldBindings },
|
|
4989
5878
|
{ name: "validateChartBindings", run: validateChartBindings },
|
|
@@ -4992,7 +5881,70 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
4992
5881
|
{ name: "validateFlowTemplatePaths", run: validateFlowTemplatePaths },
|
|
4993
5882
|
{ name: "validateAiSurfaceAffinity", run: validateAiSurfaceAffinity },
|
|
4994
5883
|
{ name: "validateAiToolReferences", run: validateAiToolReferences },
|
|
4995
|
-
{ name: "validateAiAgentAuthoring", run: validateAiAgentAuthoring }
|
|
5884
|
+
{ name: "validateAiAgentAuthoring", run: validateAiAgentAuthoring },
|
|
5885
|
+
// Field names WRITTEN by an L2 hook body (`ctx.input.x = …`,
|
|
5886
|
+
// `ctx.api.object('y').update({ x })`), resolved against the target object's
|
|
5887
|
+
// declared fields — the write-side counterpart of validateFlowTemplatePaths'
|
|
5888
|
+
// read-side membership (#4271). Lazy: only a hook that actually carries a
|
|
5889
|
+
// `language:'js'` body loads the TypeScript parser.
|
|
5890
|
+
{ name: "validateHookBodyWrites", run: validateHookBodyWrites },
|
|
5891
|
+
// The same check on the other surface that carries a `HookBodySchema` body:
|
|
5892
|
+
// action bodies, run by the same sandbox. Only the `ctx.api` write family
|
|
5893
|
+
// carries over — an action's `ctx.input` is its params bag, not a record
|
|
5894
|
+
// (see that module's ledger). Lazy on the same terms.
|
|
5895
|
+
//
|
|
5896
|
+
// The first member here to emit more than one rule id (`validateReactPageProps`
|
|
5897
|
+
// below is the other, and carries the most). Besides resolving `ctx.api`
|
|
5898
|
+
// writes against declared fields (`action-body-write-unknown-field`), it
|
|
5899
|
+
// reports a `ctx.record` write that can reach nothing
|
|
5900
|
+
// (`action-record-write-discarded`, #4345) — not a resolution question, so
|
|
5901
|
+
// by the charter above it does not belong in the suite. It rides along
|
|
5902
|
+
// anyway because it falls out of the SAME parse of the SAME source: a
|
|
5903
|
+
// separate member would parse every action body twice to say two things
|
|
5904
|
+
// about one walk, and hand-wiring it into the CLI instead is exactly the
|
|
5905
|
+
// drift this suite exists to end — which `validateReadonlyFlowWrites` was
|
|
5906
|
+
// the standing proof of, until it joined the suite below.
|
|
5907
|
+
{ name: "validateActionBodyWrites", run: validateActionBodyWrites },
|
|
5908
|
+
// The third surface that writes a record field set: a flow `update_record`
|
|
5909
|
+
// node's `config.fields`. Same question as the two rules above, but the map
|
|
5910
|
+
// is structural metadata rather than parsed JS, so a finding is a certainty
|
|
5911
|
+
// and gates (`error`) — see that module for why, and why the docs' long-
|
|
5912
|
+
// standing "prefer a flow node, it's checked" advice was the least true of
|
|
5913
|
+
// the three until it landed.
|
|
5914
|
+
{ name: "validateFlowNodeWrites", run: validateFlowNodeWrites },
|
|
5915
|
+
// The OTHER question about that same `config.fields` map: not "does this
|
|
5916
|
+
// field exist?" but "is it writable?" — a `runAs:'user'` update_record
|
|
5917
|
+
// writing a static-`readonly` field is stripped by the engine and the step
|
|
5918
|
+
// still reports success (#2948/#3425). It walks the identical map the rule
|
|
5919
|
+
// above walks, so the two splitting call sites was never defensible: hand-
|
|
5920
|
+
// wired into `validate` and `compile` only, it left `os lint` PASSING a flow
|
|
5921
|
+
// `os validate` refuses — and this one gates, so the divergence shipped a
|
|
5922
|
+
// build the other command would have stopped. Joining the suite is the whole
|
|
5923
|
+
// fix; the two hand-wired call sites are deleted with it (#4345 follow-up).
|
|
5924
|
+
{ name: "validateReadonlyFlowWrites", run: validateReadonlyFlowWrites },
|
|
5925
|
+
// The `kind:'react'` page surface. Every prop a react block binds BY FIELD
|
|
5926
|
+
// NAME is resolved against the object it names (#4340) — `<ListView columns>`,
|
|
5927
|
+
// `<ObjectForm fields>`, the `record:*` family through the SAME
|
|
5928
|
+
// `COMPONENT_FIELD_SPECS` table `validatePageFieldBindings` walks one surface
|
|
5929
|
+
// over, plus `<ObjectChart>`'s aggregate/axes (#3701/#3729) and
|
|
5930
|
+
// `searchableFields` (#4329). Squarely the charter's question, on the surface
|
|
5931
|
+
// where it had no answer at all.
|
|
5932
|
+
//
|
|
5933
|
+
// It was hand-wired into `os validate` ALONE, so `os lint` and `os compile`
|
|
5934
|
+
// accepted a react page whose every field binding was stale — including the
|
|
5935
|
+
// gating ones (a missing required binding, a filter position naming no field:
|
|
5936
|
+
// the predicate can never match and the list comes back empty). That is
|
|
5937
|
+
// `validateReadonlyFlowWrites`' divergence again, one surface over, and it is
|
|
5938
|
+
// the reason this entry exists rather than a fourth hand-wiring.
|
|
5939
|
+
//
|
|
5940
|
+
// Like `validateActionBodyWrites` above, it emits ids that are not resolution
|
|
5941
|
+
// questions — `react-prop-missing-required` and `react-prop-typo` are shape,
|
|
5942
|
+
// and by the charter belong outside. They ride along for the same reason: they
|
|
5943
|
+
// fall out of the SAME TypeScript parse of the SAME page source, and splitting
|
|
5944
|
+
// them into a second member would parse every react page twice to say two
|
|
5945
|
+
// things about one walk. Lazy on the same terms as the hook/action body rules
|
|
5946
|
+
// — only a page that is actually `kind:'react'` loads the compiler.
|
|
5947
|
+
{ name: "validateReactPageProps", run: validateReactPageProps }
|
|
4996
5948
|
];
|
|
4997
5949
|
function validateReferenceIntegrity(stack) {
|
|
4998
5950
|
const findings = [];
|
|
@@ -5003,7 +5955,14 @@ function validateReferenceIntegrity(stack) {
|
|
|
5003
5955
|
}
|
|
5004
5956
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5005
5957
|
0 && (module.exports = {
|
|
5958
|
+
ACTION_BODY_WRITE_EXCLUSIONS,
|
|
5959
|
+
ACTION_BODY_WRITE_PATTERNS,
|
|
5960
|
+
ACTION_BODY_WRITE_PATTERN_IDS,
|
|
5961
|
+
ACTION_BODY_WRITE_UNKNOWN_FIELD,
|
|
5006
5962
|
ACTION_NAME_UNDEFINED,
|
|
5963
|
+
ACTION_RECORD_WRITE_DISCARDED,
|
|
5964
|
+
ACTION_RECORD_WRITE_PATTERNS,
|
|
5965
|
+
ACTION_RECORD_WRITE_PATTERN_IDS,
|
|
5007
5966
|
AGENT_AUTHORING_WITHDRAWN,
|
|
5008
5967
|
AI_SKILL_SURFACE_MISMATCH,
|
|
5009
5968
|
AI_SKILL_TOOL_UNRESOLVED,
|
|
@@ -5030,13 +5989,20 @@ function validateReferenceIntegrity(stack) {
|
|
|
5030
5989
|
FIELD_GROUP_UNDECLARED,
|
|
5031
5990
|
FILTER_TOKEN_UNKNOWN,
|
|
5032
5991
|
FLOW_DRAFT_STATUS_AMBIGUOUS,
|
|
5992
|
+
FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
5033
5993
|
FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
|
|
5034
5994
|
FLOW_TEMPLATE_UNKNOWN_FIELD,
|
|
5035
5995
|
FLOW_TRIGGER_UNKNOWN_OBJECT,
|
|
5036
5996
|
FLOW_UPDATE_READONLY_FIELD,
|
|
5037
5997
|
FLOW_UPDATE_READONLY_WHEN_FIELD,
|
|
5998
|
+
FLOW_WRITE_NODE_TYPES,
|
|
5999
|
+
FLOW_WRITE_NODE_TYPES_DEFERRED,
|
|
5038
6000
|
FORM_COLSPAN_ABSOLUTE,
|
|
5039
6001
|
FORM_FIELD_UNKNOWN,
|
|
6002
|
+
HOOK_BODY_WRITE_EXCLUSIONS,
|
|
6003
|
+
HOOK_BODY_WRITE_PATTERNS,
|
|
6004
|
+
HOOK_BODY_WRITE_PATTERN_IDS,
|
|
6005
|
+
HOOK_BODY_WRITE_UNKNOWN_FIELD,
|
|
5040
6006
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
5041
6007
|
MEASURE_AGGREGATE_INCOHERENT,
|
|
5042
6008
|
NAV_OBJECT_UNGRANTED,
|
|
@@ -5050,6 +6016,7 @@ function validateReferenceIntegrity(stack) {
|
|
|
5050
6016
|
REACT_CHART_AXIS_UNKNOWN,
|
|
5051
6017
|
REACT_CHART_FIELD_UNKNOWN,
|
|
5052
6018
|
REFERENCE_INTEGRITY_RULES,
|
|
6019
|
+
SEARCHABLE_FIELD_UNKNOWN,
|
|
5053
6020
|
SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
5054
6021
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
5055
6022
|
SECURITY_DELEGATION_MISSING_REASON,
|
|
@@ -5082,6 +6049,9 @@ function validateReferenceIntegrity(stack) {
|
|
|
5082
6049
|
WIDGET_MEASURE_UNKNOWN,
|
|
5083
6050
|
buildAccessMatrix,
|
|
5084
6051
|
diffAccessMatrix,
|
|
6052
|
+
extractHookBodyWriteSet,
|
|
6053
|
+
extractHookBodyWrites,
|
|
6054
|
+
validateActionBodyWrites,
|
|
5085
6055
|
validateActionNameRefs,
|
|
5086
6056
|
validateAiAgentAuthoring,
|
|
5087
6057
|
validateAiSurfaceAffinity,
|
|
@@ -5091,9 +6061,11 @@ function validateReferenceIntegrity(stack) {
|
|
|
5091
6061
|
validateChartBindings,
|
|
5092
6062
|
validateDashboardActionRefs,
|
|
5093
6063
|
validateFilterTokens,
|
|
6064
|
+
validateFlowNodeWrites,
|
|
5094
6065
|
validateFlowTemplatePaths,
|
|
5095
6066
|
validateFlowTriggerReadiness,
|
|
5096
6067
|
validateFormLayout,
|
|
6068
|
+
validateHookBodyWrites,
|
|
5097
6069
|
validateJsxPages,
|
|
5098
6070
|
validateListViewMode,
|
|
5099
6071
|
validateNavAccess,
|
|
@@ -5107,6 +6079,7 @@ function validateReferenceIntegrity(stack) {
|
|
|
5107
6079
|
validateRecordTitle,
|
|
5108
6080
|
validateReferenceIntegrity,
|
|
5109
6081
|
validateResponsiveStyles,
|
|
6082
|
+
validateSearchableFields,
|
|
5110
6083
|
validateSecurityPosture,
|
|
5111
6084
|
validateSeedReplaySafety,
|
|
5112
6085
|
validateSeedStateMachine,
|