@objectstack/lint 16.0.0-rc.1 → 16.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/index.cjs +224 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +124 -33
- package/dist/index.d.ts +124 -33
- package/dist/index.js +220 -3
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -14,8 +14,11 @@ It never depends on a runtime and is never bundled into a frontend.
|
|
|
14
14
|
## API
|
|
15
15
|
|
|
16
16
|
- `validateWidgetBindings(stack)` — dashboard widget → dataset → measure/dimension
|
|
17
|
-
reference integrity, chart-config bindings,
|
|
18
|
-
(e.g. SUM of a `percent` field is meaningless)
|
|
17
|
+
reference integrity, chart-config bindings, measure-aggregation coherence
|
|
18
|
+
(e.g. SUM of a `percent` field is meaningless), and dashboard-level filter
|
|
19
|
+
field existence — every `dateRange` / `globalFilters[]` field (after any
|
|
20
|
+
`filterBindings` re-target) must exist on each bound widget's object, else the
|
|
21
|
+
broadcast filter emits invalid SQL and the widget crashes at render (#3365).
|
|
19
22
|
- `validateStackExpressions(stack)` — CEL/predicate validity for field
|
|
20
23
|
conditionals, sharing rules, action `visible`/`disabled`, lifecycle hooks
|
|
21
24
|
(ADR-0032).
|
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,9 @@ __export(index_exports, {
|
|
|
27
27
|
CAPABILITY_REFERENCE_UNKNOWN: () => CAPABILITY_REFERENCE_UNKNOWN,
|
|
28
28
|
CHART_CONFIG_MISSING: () => CHART_CONFIG_MISSING,
|
|
29
29
|
CHART_FIELD_UNKNOWN: () => CHART_FIELD_UNKNOWN,
|
|
30
|
+
DASHBOARD_ACTION_ROUTE_UNRESOLVED: () => DASHBOARD_ACTION_ROUTE_UNRESOLVED,
|
|
31
|
+
DASHBOARD_ACTION_TARGET_UNDEFINED: () => DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
32
|
+
DASHBOARD_FILTER_FIELD_UNKNOWN: () => DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
30
33
|
FIELD_GROUP_EMPTY: () => FIELD_GROUP_EMPTY,
|
|
31
34
|
FIELD_GROUP_UNDECLARED: () => FIELD_GROUP_UNDECLARED,
|
|
32
35
|
FLOW_DRAFT_STATUS_AMBIGUOUS: () => FLOW_DRAFT_STATUS_AMBIGUOUS,
|
|
@@ -66,6 +69,7 @@ __export(index_exports, {
|
|
|
66
69
|
diffAccessMatrix: () => diffAccessMatrix,
|
|
67
70
|
validateApprovalApprovers: () => validateApprovalApprovers,
|
|
68
71
|
validateCapabilityReferences: () => validateCapabilityReferences,
|
|
72
|
+
validateDashboardActionRefs: () => validateDashboardActionRefs,
|
|
69
73
|
validateFlowTriggerReadiness: () => validateFlowTriggerReadiness,
|
|
70
74
|
validateFormLayout: () => validateFormLayout,
|
|
71
75
|
validateJsxPages: () => validateJsxPages,
|
|
@@ -95,6 +99,7 @@ var TABLE_COUNT_ONLY = "table-count-only";
|
|
|
95
99
|
var MEASURE_AGGREGATE_INCOHERENT = "measure-aggregate-incoherent";
|
|
96
100
|
var WIDGET_LEGACY_ANALYTICS_SHAPE = "widget-legacy-analytics-shape";
|
|
97
101
|
var WIDGET_LEGACY_ANALYTICS_UNRENDERABLE = "widget-legacy-analytics-unrenderable";
|
|
102
|
+
var DASHBOARD_FILTER_FIELD_UNKNOWN = "dashboard-filter-field-unknown";
|
|
98
103
|
var LEGACY_ANALYTICS_KEYS = [
|
|
99
104
|
"categoryField",
|
|
100
105
|
"valueField",
|
|
@@ -172,6 +177,47 @@ function list(names) {
|
|
|
172
177
|
const arr = [...names];
|
|
173
178
|
return arr.length > 0 ? arr.join(", ") : "(none)";
|
|
174
179
|
}
|
|
180
|
+
var DATE_RANGE_FILTER_NAME = "dateRange";
|
|
181
|
+
var DATE_RANGE_DEFAULT_FIELD = "created_at";
|
|
182
|
+
var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
183
|
+
"id",
|
|
184
|
+
"created_at",
|
|
185
|
+
"created_by",
|
|
186
|
+
"updated_at",
|
|
187
|
+
"updated_by",
|
|
188
|
+
"owner_id",
|
|
189
|
+
"organization_id",
|
|
190
|
+
"tenant_id",
|
|
191
|
+
"user_id",
|
|
192
|
+
"deleted_at"
|
|
193
|
+
]);
|
|
194
|
+
function dashboardFilterDefs(dash) {
|
|
195
|
+
const byName = /* @__PURE__ */ new Map();
|
|
196
|
+
const dateRange = dash.dateRange;
|
|
197
|
+
if (dateRange && typeof dateRange === "object") {
|
|
198
|
+
const declared = dateRange.field;
|
|
199
|
+
const field = typeof declared === "string" && declared ? declared : DATE_RANGE_DEFAULT_FIELD;
|
|
200
|
+
byName.set(DATE_RANGE_FILTER_NAME, { name: DATE_RANGE_FILTER_NAME, field });
|
|
201
|
+
}
|
|
202
|
+
for (const f of asArray(dash.globalFilters)) {
|
|
203
|
+
if (typeof f.field !== "string" || !f.field) continue;
|
|
204
|
+
const name = typeof f.name === "string" && f.name ? f.name : f.field;
|
|
205
|
+
const targetWidgets = Array.isArray(f.targetWidgets) ? f.targetWidgets.filter((w) => typeof w === "string") : void 0;
|
|
206
|
+
byName.set(name, { name, field: f.field, targetWidgets });
|
|
207
|
+
}
|
|
208
|
+
return [...byName.values()];
|
|
209
|
+
}
|
|
210
|
+
function effectiveFilterField(widget, def) {
|
|
211
|
+
const bindings = widget.filterBindings;
|
|
212
|
+
const binding = bindings && typeof bindings === "object" ? bindings[def.name] : void 0;
|
|
213
|
+
if (binding === false) return void 0;
|
|
214
|
+
if (typeof binding === "string" && binding) return { field: binding, explicit: true };
|
|
215
|
+
if (def.targetWidgets && def.targetWidgets.length > 0) {
|
|
216
|
+
const id = typeof widget.id === "string" ? widget.id : void 0;
|
|
217
|
+
if (!id || !def.targetWidgets.includes(id)) return void 0;
|
|
218
|
+
}
|
|
219
|
+
return { field: def.field, explicit: false };
|
|
220
|
+
}
|
|
175
221
|
function validateWidgetBindings(stack) {
|
|
176
222
|
const findings = [];
|
|
177
223
|
const datasets = /* @__PURE__ */ new Map();
|
|
@@ -216,6 +262,7 @@ function validateWidgetBindings(stack) {
|
|
|
216
262
|
const dash = dashboards[i];
|
|
217
263
|
const dashName = typeof dash.name === "string" ? dash.name : `(dashboard ${i})`;
|
|
218
264
|
const widgets = Array.isArray(dash.widgets) ? dash.widgets : [];
|
|
265
|
+
const dashFilterDefs = dashboardFilterDefs(dash);
|
|
219
266
|
for (let j = 0; j < widgets.length; j++) {
|
|
220
267
|
const w = widgets[j];
|
|
221
268
|
const widgetId = typeof w.id === "string" ? w.id : `(widget ${j})`;
|
|
@@ -260,6 +307,25 @@ function validateWidgetBindings(stack) {
|
|
|
260
307
|
});
|
|
261
308
|
}
|
|
262
309
|
if (!dataset) continue;
|
|
310
|
+
if (dashFilterDefs.length > 0) {
|
|
311
|
+
const datasetObject = typeof dataset.object === "string" ? dataset.object : void 0;
|
|
312
|
+
const objectFields = datasetObject ? objectFieldTypes.get(datasetObject) : void 0;
|
|
313
|
+
if (objectFields) {
|
|
314
|
+
for (const def of dashFilterDefs) {
|
|
315
|
+
const eff = effectiveFilterField(w, def);
|
|
316
|
+
if (!eff) continue;
|
|
317
|
+
const field = eff.field;
|
|
318
|
+
if (field.includes(".")) continue;
|
|
319
|
+
if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) continue;
|
|
320
|
+
push({
|
|
321
|
+
severity: "error",
|
|
322
|
+
rule: DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
323
|
+
message: eff.explicit ? `binds dashboard filter \`${def.name}\` to field \`${field}\` (via filterBindings), but object \`${datasetObject}\` (dataset "${dsName}") has no field \`${field}\`.` : `inherits dashboard filter \`${def.name}(${field})\`, but object \`${datasetObject}\` (dataset "${dsName}") has no field \`${field}\`.`,
|
|
324
|
+
hint: eff.explicit ? `Point filterBindings: { ${def.name}: '<field>' } at a field that exists on \`${datasetObject}\`, or opt out with filterBindings: { ${def.name}: false }.${suggest(field, objectFields.keys())} Object fields: ${list(objectFields.keys())}.` : `Set filterBindings: { ${def.name}: false } on this widget to opt out, or re-target to an existing field with filterBindings: { ${def.name}: '<field>' }.${suggest(field, objectFields.keys())} Object fields: ${list(objectFields.keys())}.`
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
263
329
|
const dimensionNames = /* @__PURE__ */ new Set();
|
|
264
330
|
for (const d of asArray(dataset.dimensions)) {
|
|
265
331
|
if (typeof d.name === "string") dimensionNames.add(d.name);
|
|
@@ -2115,7 +2181,9 @@ function validateSecurityPosture(stack, opts) {
|
|
|
2115
2181
|
return findings;
|
|
2116
2182
|
}
|
|
2117
2183
|
|
|
2118
|
-
// src/
|
|
2184
|
+
// src/validate-dashboard-action-refs.ts
|
|
2185
|
+
var DASHBOARD_ACTION_TARGET_UNDEFINED = "dashboard-action-target-undefined";
|
|
2186
|
+
var DASHBOARD_ACTION_ROUTE_UNRESOLVED = "dashboard-action-route-unresolved";
|
|
2119
2187
|
function asArray17(v) {
|
|
2120
2188
|
if (Array.isArray(v)) return v;
|
|
2121
2189
|
if (v && typeof v === "object") {
|
|
@@ -2123,17 +2191,166 @@ function asArray17(v) {
|
|
|
2123
2191
|
}
|
|
2124
2192
|
return [];
|
|
2125
2193
|
}
|
|
2194
|
+
function strName(v) {
|
|
2195
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2196
|
+
}
|
|
2197
|
+
var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
|
|
2198
|
+
var URL_COLLECTION_TO_STACK_KEY = {
|
|
2199
|
+
object: "objects",
|
|
2200
|
+
objects: "objects",
|
|
2201
|
+
report: "reports",
|
|
2202
|
+
reports: "reports",
|
|
2203
|
+
dashboard: "dashboards",
|
|
2204
|
+
dashboards: "dashboards",
|
|
2205
|
+
page: "pages",
|
|
2206
|
+
pages: "pages",
|
|
2207
|
+
view: "views",
|
|
2208
|
+
views: "views"
|
|
2209
|
+
};
|
|
2210
|
+
function viewContainerName(item) {
|
|
2211
|
+
return strName(item.name) ?? strName(item.id) ?? strName(item.object) ?? strName(item.list?.data && item.list.data.object) ?? strName(item.form?.data && item.form.data.object);
|
|
2212
|
+
}
|
|
2213
|
+
function collectKnownTargets(stack) {
|
|
2214
|
+
const actions = /* @__PURE__ */ new Set();
|
|
2215
|
+
const objects = /* @__PURE__ */ new Set();
|
|
2216
|
+
const reports = /* @__PURE__ */ new Set();
|
|
2217
|
+
const dashboards = /* @__PURE__ */ new Set();
|
|
2218
|
+
const pages = /* @__PURE__ */ new Set();
|
|
2219
|
+
const views = /* @__PURE__ */ new Set();
|
|
2220
|
+
const collectNames = (v, into, name) => {
|
|
2221
|
+
for (const item of asArray17(v)) {
|
|
2222
|
+
if (!item || typeof item !== "object") continue;
|
|
2223
|
+
const n = name(item);
|
|
2224
|
+
if (n) into.add(n);
|
|
2225
|
+
}
|
|
2226
|
+
};
|
|
2227
|
+
collectNames(stack.actions, actions, (a) => strName(a.name));
|
|
2228
|
+
for (const obj of asArray17(stack.objects)) {
|
|
2229
|
+
if (!obj || typeof obj !== "object") continue;
|
|
2230
|
+
const n = strName(obj.name);
|
|
2231
|
+
if (n) objects.add(n);
|
|
2232
|
+
collectNames(obj.actions, actions, (a) => strName(a.name));
|
|
2233
|
+
}
|
|
2234
|
+
collectNames(stack.reports, reports, (r) => strName(r.name));
|
|
2235
|
+
collectNames(stack.dashboards, dashboards, (d) => strName(d.name));
|
|
2236
|
+
collectNames(stack.pages, pages, (p) => strName(p.name));
|
|
2237
|
+
collectNames(stack.views, views, viewContainerName);
|
|
2238
|
+
for (const o of objects) views.add(o);
|
|
2239
|
+
return { actions, objects, reports, dashboards, pages, views };
|
|
2240
|
+
}
|
|
2241
|
+
function resolveActionTarget(actionType, target, known) {
|
|
2242
|
+
if (known.actions.has(target)) return true;
|
|
2243
|
+
if (actionType === "modal") {
|
|
2244
|
+
if (known.objects.has(target)) return true;
|
|
2245
|
+
const m = MODAL_VERB_RE.exec(target);
|
|
2246
|
+
if (m && known.objects.has(m[1])) return true;
|
|
2247
|
+
}
|
|
2248
|
+
return false;
|
|
2249
|
+
}
|
|
2250
|
+
function resolveUrlRoute(target, known) {
|
|
2251
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target) || target.startsWith("//")) return null;
|
|
2252
|
+
if (target.includes("${")) return null;
|
|
2253
|
+
if (!target.startsWith("/")) return null;
|
|
2254
|
+
const pathPart = target.split(/[?#]/, 1)[0];
|
|
2255
|
+
const segments = pathPart.split("/").filter(Boolean);
|
|
2256
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
2257
|
+
const stackKey = URL_COLLECTION_TO_STACK_KEY[segments[i]];
|
|
2258
|
+
if (!stackKey) continue;
|
|
2259
|
+
const name = segments[i + 1];
|
|
2260
|
+
if (known[stackKey].has(name)) return void 0;
|
|
2261
|
+
return { collection: segments[i], name };
|
|
2262
|
+
}
|
|
2263
|
+
return null;
|
|
2264
|
+
}
|
|
2265
|
+
function validateDashboardActionRefs(stack) {
|
|
2266
|
+
const findings = [];
|
|
2267
|
+
if (!stack || typeof stack !== "object") return findings;
|
|
2268
|
+
const dashboards = asArray17(stack.dashboards);
|
|
2269
|
+
if (dashboards.length === 0) return findings;
|
|
2270
|
+
const known = collectKnownTargets(stack);
|
|
2271
|
+
const checkOne = (action, where, path) => {
|
|
2272
|
+
const target = strName(action.actionUrl);
|
|
2273
|
+
if (!target) return;
|
|
2274
|
+
if (target.includes("${")) return;
|
|
2275
|
+
const actionType = strName(action.actionType) ?? "url";
|
|
2276
|
+
if (actionType === "script" || actionType === "modal") {
|
|
2277
|
+
if (resolveActionTarget(actionType, target, known)) return;
|
|
2278
|
+
const kindWord = actionType === "script" ? "script" : "modal";
|
|
2279
|
+
findings.push({
|
|
2280
|
+
severity: "error",
|
|
2281
|
+
rule: DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
2282
|
+
where,
|
|
2283
|
+
path,
|
|
2284
|
+
message: `${kindWord} action target "${target}" resolves to no defined action` + (actionType === "modal" ? " or object" : "") + `. The button renders but does nothing when clicked \u2014 a dangling reference the runtime cannot dispatch (ADR-0049: a declared reference must resolve).`,
|
|
2285
|
+
hint: actionType === "modal" ? `Define an action named "${target}" (stack.actions or the object's actions), use the "<verb>_<object>" convention against a real object (e.g. "create_<object>"), point actionUrl at an existing object, or remove the button.` : `Define a script action named "${target}" (stack.actions or the object's actions) with an inline body or a registered handler, or remove the button.`
|
|
2286
|
+
});
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
if (actionType === "url") {
|
|
2290
|
+
const route = resolveUrlRoute(target, known);
|
|
2291
|
+
if (!route) return;
|
|
2292
|
+
findings.push({
|
|
2293
|
+
severity: "warning",
|
|
2294
|
+
rule: DASHBOARD_ACTION_ROUTE_UNRESOLVED,
|
|
2295
|
+
where,
|
|
2296
|
+
path,
|
|
2297
|
+
message: `url action target "${target}" points at ${route.collection}/${route.name}, but no ${route.collection.replace(/s$/, "")} named "${route.name}" is registered in this stack \u2014 the button likely navigates to a dead route.`,
|
|
2298
|
+
hint: `Check the path for a typo, define the referenced ${route.collection.replace(/s$/, "")}, or ignore this if the route is served by another installed package or a host/console route.`
|
|
2299
|
+
});
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
for (let di = 0; di < dashboards.length; di++) {
|
|
2304
|
+
const dash = dashboards[di];
|
|
2305
|
+
if (!dash || typeof dash !== "object") continue;
|
|
2306
|
+
const dashName = strName(dash.name) ?? `(dashboard ${di})`;
|
|
2307
|
+
const dashPath = `dashboards[${di}]`;
|
|
2308
|
+
const headerActions = asArray17(dash.header?.actions);
|
|
2309
|
+
for (let ai = 0; ai < headerActions.length; ai++) {
|
|
2310
|
+
const action = headerActions[ai];
|
|
2311
|
+
if (!action || typeof action !== "object") continue;
|
|
2312
|
+
const label = strName(action.label) ?? strName(action.actionUrl) ?? `#${ai}`;
|
|
2313
|
+
checkOne(
|
|
2314
|
+
action,
|
|
2315
|
+
`dashboard "${dashName}" \xB7 header action "${label}"`,
|
|
2316
|
+
`${dashPath}.header.actions[${ai}].actionUrl`
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
const widgets = asArray17(dash.widgets);
|
|
2320
|
+
for (let wi = 0; wi < widgets.length; wi++) {
|
|
2321
|
+
const widget = widgets[wi];
|
|
2322
|
+
if (!widget || typeof widget !== "object") continue;
|
|
2323
|
+
if (!strName(widget.actionUrl)) continue;
|
|
2324
|
+
const widgetId = strName(widget.id) ?? `#${wi}`;
|
|
2325
|
+
checkOne(
|
|
2326
|
+
{ actionType: widget.actionType, actionUrl: widget.actionUrl },
|
|
2327
|
+
`dashboard "${dashName}" \xB7 widget "${widgetId}" action`,
|
|
2328
|
+
`${dashPath}.widgets[${wi}].actionUrl`
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
return findings;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// src/build-access-matrix.ts
|
|
2336
|
+
function asArray18(v) {
|
|
2337
|
+
if (Array.isArray(v)) return v;
|
|
2338
|
+
if (v && typeof v === "object") {
|
|
2339
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
2340
|
+
}
|
|
2341
|
+
return [];
|
|
2342
|
+
}
|
|
2126
2343
|
function buildAccessMatrix(stack) {
|
|
2127
2344
|
const entries = [];
|
|
2128
2345
|
if (!stack || typeof stack !== "object") return { version: 1, entries };
|
|
2129
2346
|
const owdByObject = /* @__PURE__ */ new Map();
|
|
2130
|
-
for (const obj of
|
|
2347
|
+
for (const obj of asArray18(stack.objects)) {
|
|
2131
2348
|
const name = typeof obj.name === "string" ? obj.name : "";
|
|
2132
2349
|
if (!name) continue;
|
|
2133
2350
|
const owd = obj.sharingModel ?? obj.security?.sharingModel;
|
|
2134
2351
|
if (typeof owd === "string") owdByObject.set(name, owd);
|
|
2135
2352
|
}
|
|
2136
|
-
for (const ps of
|
|
2353
|
+
for (const ps of asArray18(stack.permissions)) {
|
|
2137
2354
|
const psName = typeof ps.name === "string" ? ps.name : "";
|
|
2138
2355
|
if (!psName) continue;
|
|
2139
2356
|
const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
|
|
@@ -2212,6 +2429,9 @@ function diffAccessMatrix(before, after) {
|
|
|
2212
2429
|
CAPABILITY_REFERENCE_UNKNOWN,
|
|
2213
2430
|
CHART_CONFIG_MISSING,
|
|
2214
2431
|
CHART_FIELD_UNKNOWN,
|
|
2432
|
+
DASHBOARD_ACTION_ROUTE_UNRESOLVED,
|
|
2433
|
+
DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
2434
|
+
DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
2215
2435
|
FIELD_GROUP_EMPTY,
|
|
2216
2436
|
FIELD_GROUP_UNDECLARED,
|
|
2217
2437
|
FLOW_DRAFT_STATUS_AMBIGUOUS,
|
|
@@ -2251,6 +2471,7 @@ function diffAccessMatrix(before, after) {
|
|
|
2251
2471
|
diffAccessMatrix,
|
|
2252
2472
|
validateApprovalApprovers,
|
|
2253
2473
|
validateCapabilityReferences,
|
|
2474
|
+
validateDashboardActionRefs,
|
|
2254
2475
|
validateFlowTriggerReadiness,
|
|
2255
2476
|
validateFormLayout,
|
|
2256
2477
|
validateJsxPages,
|