@objectstack/lint 15.1.0 → 16.0.0-rc.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/dist/index.cjs +251 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -8
- package/dist/index.d.ts +61 -8
- package/dist/index.js +250 -69
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -7,6 +7,18 @@ var CHART_FIELD_UNKNOWN = "chart-field-unknown";
|
|
|
7
7
|
var CHART_CONFIG_MISSING = "chart-config-missing";
|
|
8
8
|
var TABLE_COUNT_ONLY = "table-count-only";
|
|
9
9
|
var MEASURE_AGGREGATE_INCOHERENT = "measure-aggregate-incoherent";
|
|
10
|
+
var WIDGET_LEGACY_ANALYTICS_SHAPE = "widget-legacy-analytics-shape";
|
|
11
|
+
var WIDGET_LEGACY_ANALYTICS_UNRENDERABLE = "widget-legacy-analytics-unrenderable";
|
|
12
|
+
var LEGACY_ANALYTICS_KEYS = [
|
|
13
|
+
"categoryField",
|
|
14
|
+
"valueField",
|
|
15
|
+
"xAxisField",
|
|
16
|
+
"yAxisFields",
|
|
17
|
+
"aggregate",
|
|
18
|
+
"aggregation",
|
|
19
|
+
"rowField",
|
|
20
|
+
"columnField"
|
|
21
|
+
];
|
|
10
22
|
function asArray(v) {
|
|
11
23
|
if (Array.isArray(v)) return v;
|
|
12
24
|
if (v && typeof v === "object") {
|
|
@@ -128,6 +140,29 @@ function validateWidgetBindings(stack) {
|
|
|
128
140
|
if (f.severity === "warning" && suppressed(f.rule)) return;
|
|
129
141
|
findings.push({ ...f, where, path });
|
|
130
142
|
};
|
|
143
|
+
const legacyUsed = LEGACY_ANALYTICS_KEYS.filter((k) => w[k] !== void 0);
|
|
144
|
+
if (legacyUsed.length > 0) {
|
|
145
|
+
const optionsData = typeof w.options === "object" && w.options !== null && w.options.data !== void 0;
|
|
146
|
+
const hasDataSource = w.dataset !== void 0 || w.object !== void 0 || w.data !== void 0 || optionsData;
|
|
147
|
+
const keyList = legacyUsed.map((k) => `\`${k}\``).join(", ");
|
|
148
|
+
const plural = legacyUsed.length > 1;
|
|
149
|
+
const datasetHint = `Bind a semantic dataset and select fields BY NAME: \`dataset: '<name>', dimensions: [...], values: [...]\`. Dataset-bound widgets render through DatasetWidget (pivot rows/cols come from \`dimensions\`, cell values from \`values\`).`;
|
|
150
|
+
if (!hasDataSource) {
|
|
151
|
+
push({
|
|
152
|
+
severity: "error",
|
|
153
|
+
rule: WIDGET_LEGACY_ANALYTICS_UNRENDERABLE,
|
|
154
|
+
message: `sets legacy analytics key${plural ? "s" : ""} ${keyList} (removed by the ADR-0021 single-form cutover) and binds no data source (no \`dataset\`, \`object\`, or inline \`data\`) \u2014 it renders nothing.`,
|
|
155
|
+
hint: `${datasetHint} The renderer ignores the legacy keys, so without a data source this widget has no data at all.`
|
|
156
|
+
});
|
|
157
|
+
} else {
|
|
158
|
+
push({
|
|
159
|
+
severity: "warning",
|
|
160
|
+
rule: WIDGET_LEGACY_ANALYTICS_SHAPE,
|
|
161
|
+
message: `sets legacy analytics key${plural ? "s" : ""} ${keyList} that the ADR-0021 single-form cutover removed \u2014 the dashboard renderer ignores ${plural ? "them" : "it"}.`,
|
|
162
|
+
hint: `${datasetHint} These inline keys are a no-op. Suppress with suppressWarnings: ['${WIDGET_LEGACY_ANALYTICS_SHAPE}'] if intentional.`
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
131
166
|
const dsName = typeof w.dataset === "string" ? w.dataset : void 0;
|
|
132
167
|
const dataset = dsName ? datasets.get(dsName) : void 0;
|
|
133
168
|
if (dsName && !dataset) {
|
|
@@ -248,17 +283,42 @@ function buildFieldIndex(objects) {
|
|
|
248
283
|
}
|
|
249
284
|
return idx;
|
|
250
285
|
}
|
|
286
|
+
function buildFieldTypeIndex(objects) {
|
|
287
|
+
const idx = /* @__PURE__ */ new Map();
|
|
288
|
+
for (const obj of objects) {
|
|
289
|
+
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
290
|
+
if (!name) continue;
|
|
291
|
+
const fields = obj.fields;
|
|
292
|
+
const types = {};
|
|
293
|
+
if (Array.isArray(fields)) {
|
|
294
|
+
for (const f of fields) {
|
|
295
|
+
const fn = f?.name;
|
|
296
|
+
const ft = f?.type;
|
|
297
|
+
if (typeof fn === "string" && typeof ft === "string") types[fn] = ft;
|
|
298
|
+
}
|
|
299
|
+
} else if (fields && typeof fields === "object") {
|
|
300
|
+
for (const [fn, def] of Object.entries(fields)) {
|
|
301
|
+
const ft = def?.type;
|
|
302
|
+
if (typeof ft === "string") types[fn] = ft;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
idx.set(name, types);
|
|
306
|
+
}
|
|
307
|
+
return idx;
|
|
308
|
+
}
|
|
251
309
|
function validateStackExpressions(stack) {
|
|
252
310
|
const issues = [];
|
|
253
311
|
const objects = asArray2(stack.objects);
|
|
254
312
|
const fieldIndex = buildFieldIndex(objects);
|
|
313
|
+
const fieldTypeIndex = buildFieldTypeIndex(objects);
|
|
255
314
|
const check = (where, raw, objectName, scope = "flattened") => {
|
|
256
315
|
if (raw == null) return;
|
|
257
316
|
const fields = objectName ? fieldIndex.get(objectName) : void 0;
|
|
317
|
+
const fieldTypes = objectName ? fieldTypeIndex.get(objectName) : void 0;
|
|
258
318
|
const res = validateExpression(
|
|
259
319
|
"predicate",
|
|
260
320
|
raw,
|
|
261
|
-
objectName ? { objectName, fields, scope } : { scope }
|
|
321
|
+
objectName ? { objectName, fields, fieldTypes, scope } : { scope }
|
|
262
322
|
);
|
|
263
323
|
for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
|
|
264
324
|
for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
|
|
@@ -306,19 +366,6 @@ function validateStackExpressions(stack) {
|
|
|
306
366
|
}
|
|
307
367
|
const fields = obj.fields;
|
|
308
368
|
const fieldList = Array.isArray(fields) ? fields : fields && typeof fields === "object" ? Object.values(fields) : [];
|
|
309
|
-
if (obj.external != null) {
|
|
310
|
-
const fieldEntries = Array.isArray(fields) ? fields.map((f) => [f?.name ?? "?", f]) : fields && typeof fields === "object" ? Object.entries(fields) : [];
|
|
311
|
-
for (const [fname, fdef] of fieldEntries) {
|
|
312
|
-
if (fdef && typeof fdef === "object" && fdef.columnName != null) {
|
|
313
|
-
issues.push({
|
|
314
|
-
where: `object '${objectName}' \xB7 field '${fname}'`,
|
|
315
|
-
message: `external object '${objectName}': field '${fname}' sets columnName='${String(fdef.columnName)}', which is not supported on federated objects (ADR-0062 D7). The driver's query pipeline ignores field.columnName for external objects; map remote columns via the datasource's external.columnMap instead.`,
|
|
316
|
-
source: `columnName='${String(fdef.columnName)}'`,
|
|
317
|
-
severity: "error"
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
369
|
for (const f of fieldList) {
|
|
323
370
|
if (f && typeof f === "object") {
|
|
324
371
|
const fname = f.name ?? "?";
|
|
@@ -330,7 +377,7 @@ function validateStackExpressions(stack) {
|
|
|
330
377
|
const res = validateExpression(
|
|
331
378
|
"value",
|
|
332
379
|
f.formula,
|
|
333
|
-
objectName ? { objectName, fields: fieldIndex.get(objectName), scope: "record" } : { scope: "record" }
|
|
380
|
+
objectName ? { objectName, fields: fieldIndex.get(objectName), fieldTypes: fieldTypeIndex.get(objectName), scope: "record" } : { scope: "record" }
|
|
334
381
|
);
|
|
335
382
|
const fieldWhere = `object '${objectName}' \xB7 field '${f.name ?? "?"}' formula`;
|
|
336
383
|
for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
|
|
@@ -437,6 +484,117 @@ function validateListViewMode(stack) {
|
|
|
437
484
|
return out;
|
|
438
485
|
}
|
|
439
486
|
|
|
487
|
+
// src/validate-flow-trigger-readiness.ts
|
|
488
|
+
var FLOW_TRIGGER_UNKNOWN_OBJECT = "flow-trigger-unknown-object";
|
|
489
|
+
var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
|
|
490
|
+
function asArray4(v) {
|
|
491
|
+
if (Array.isArray(v)) return v;
|
|
492
|
+
if (v && typeof v === "object") {
|
|
493
|
+
return Object.entries(v).map(([name, def]) => ({
|
|
494
|
+
name,
|
|
495
|
+
...def
|
|
496
|
+
}));
|
|
497
|
+
}
|
|
498
|
+
return [];
|
|
499
|
+
}
|
|
500
|
+
function startNodeOf(flow) {
|
|
501
|
+
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
502
|
+
const index = nodes.findIndex((n) => n?.type === "start");
|
|
503
|
+
return index >= 0 ? { node: nodes[index], index } : void 0;
|
|
504
|
+
}
|
|
505
|
+
function validateFlowTriggerReadiness(stack) {
|
|
506
|
+
const findings = [];
|
|
507
|
+
const flows = asArray4(stack.flows);
|
|
508
|
+
if (flows.length === 0) return findings;
|
|
509
|
+
const objectNames = new Set(
|
|
510
|
+
asArray4(stack.objects).map((o) => typeof o.name === "string" ? o.name : void 0).filter((n) => !!n)
|
|
511
|
+
);
|
|
512
|
+
flows.forEach((flow, flowIndex) => {
|
|
513
|
+
const flowName = typeof flow.name === "string" ? flow.name : `#${flowIndex}`;
|
|
514
|
+
const start = startNodeOf(flow);
|
|
515
|
+
const config = start?.node.config ?? {};
|
|
516
|
+
const triggerType = typeof config.triggerType === "string" ? config.triggerType : void 0;
|
|
517
|
+
const isRecordTriggered = !!triggerType && triggerType.startsWith("record-");
|
|
518
|
+
const isTimeRelative = config.timeRelative != null && typeof config.timeRelative === "object";
|
|
519
|
+
const isAutoTriggered = isRecordTriggered || triggerType === "api" || config.schedule != null || isTimeRelative || flow.type === "schedule" || flow.type === "api";
|
|
520
|
+
if (isRecordTriggered && start) {
|
|
521
|
+
const objectName = typeof config.objectName === "string" ? config.objectName : void 0;
|
|
522
|
+
if (objectName && !objectNames.has(objectName) && !objectName.startsWith("sys_")) {
|
|
523
|
+
findings.push({
|
|
524
|
+
severity: "warning",
|
|
525
|
+
rule: FLOW_TRIGGER_UNKNOWN_OBJECT,
|
|
526
|
+
where: `flow "${flowName}" \u203A start node`,
|
|
527
|
+
path: `flows[${flowIndex}].nodes[${start.index}].config.objectName`,
|
|
528
|
+
message: `targets object '${objectName}', which this stack does not define \u2014 if the name is wrong, the flow will never fire (and the runtime stays silent about it).`,
|
|
529
|
+
hint: `Object names match exactly. Check config.objectName against the object's registered name (e.g. 'app_candidate', not 'candidate'). If the object comes from another installed package, this warning can be ignored.`
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (isTimeRelative && start) {
|
|
534
|
+
const tr = config.timeRelative;
|
|
535
|
+
const objectName = typeof tr.object === "string" ? tr.object : void 0;
|
|
536
|
+
if (objectName && !objectNames.has(objectName) && !objectName.startsWith("sys_")) {
|
|
537
|
+
findings.push({
|
|
538
|
+
severity: "warning",
|
|
539
|
+
rule: FLOW_TRIGGER_UNKNOWN_OBJECT,
|
|
540
|
+
where: `flow "${flowName}" \u203A start node`,
|
|
541
|
+
path: `flows[${flowIndex}].nodes[${start.index}].config.timeRelative.object`,
|
|
542
|
+
message: `sweeps object '${objectName}', which this stack does not define \u2014 if the name is wrong, the sweep will match nothing (and the runtime stays quiet about it).`,
|
|
543
|
+
hint: `Object names match exactly. Check config.timeRelative.object against the object's registered name. If the object comes from another installed package, this warning can be ignored.`
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
|
|
548
|
+
findings.push({
|
|
549
|
+
severity: "warning",
|
|
550
|
+
rule: FLOW_DRAFT_STATUS_AMBIGUOUS,
|
|
551
|
+
where: `flow "${flowName}"`,
|
|
552
|
+
path: `flows[${flowIndex}].status`,
|
|
553
|
+
message: `has status 'draft' (the default when none is authored). Draft flows DO still fire their triggers (only 'obsolete'/'invalid' disable), so the intent is ambiguous.`,
|
|
554
|
+
hint: `Declare status: 'active' to arm it deliberately, or status: 'obsolete' to disable it.`
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
return findings;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/validate-view-containers.ts
|
|
562
|
+
var VIEW_CONTAINER_SHAPE = "view-container-shape";
|
|
563
|
+
var CONTAINER_SLOT_KEYS = ["list", "form", "listViews", "formViews"];
|
|
564
|
+
function asEntries(v) {
|
|
565
|
+
if (Array.isArray(v)) return v.map((value, i) => ({ key: `[${i}]`, value }));
|
|
566
|
+
if (v && typeof v === "object") {
|
|
567
|
+
return Object.entries(v).map(([name, value]) => ({ key: `.${name}`, value }));
|
|
568
|
+
}
|
|
569
|
+
return [];
|
|
570
|
+
}
|
|
571
|
+
function containerViewCount(rec) {
|
|
572
|
+
const named = (slot) => slot && typeof slot === "object" && !Array.isArray(slot) ? Object.keys(slot).length : 0;
|
|
573
|
+
return (rec.list ? 1 : 0) + (rec.form ? 1 : 0) + named(rec.listViews) + named(rec.formViews);
|
|
574
|
+
}
|
|
575
|
+
function validateViewContainers(stack) {
|
|
576
|
+
const out = [];
|
|
577
|
+
if (!stack || typeof stack !== "object") return out;
|
|
578
|
+
for (const { key, value } of asEntries(stack.views)) {
|
|
579
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
580
|
+
const rec = value;
|
|
581
|
+
if (rec.viewKind != null) continue;
|
|
582
|
+
if (containerViewCount(rec) > 0) continue;
|
|
583
|
+
const label = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
|
|
584
|
+
const hasContainerSlot = CONTAINER_SLOT_KEYS.some((k) => k in rec);
|
|
585
|
+
const looksFlat = !hasContainerSlot && ["type", "columns", "data", "filter", "sort"].some((k) => k in rec);
|
|
586
|
+
out.push({
|
|
587
|
+
severity: "error",
|
|
588
|
+
rule: VIEW_CONTAINER_SHAPE,
|
|
589
|
+
where: `views${key}${label}`,
|
|
590
|
+
path: `views${key}`,
|
|
591
|
+
message: looksFlat ? "Flat list-view object is not a view container: `ViewSchema` strips its keys, so it parses to an EMPTY container \u2014 zero views register and the Console renders no view for it." : "View container defines no views \u2014 all of `list` / `form` / `listViews` / `formViews` are absent or empty, so nothing registers.",
|
|
592
|
+
hint: "Wrap every view in a defineView container: defineView({ list: { type, data, columns, ... }, listViews: { ... }, formViews: { ... } }). See examples/app-showcase/src/ui/views/task.view.ts."
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
return out;
|
|
596
|
+
}
|
|
597
|
+
|
|
440
598
|
// src/validate-responsive-styles.ts
|
|
441
599
|
var STYLE_NODE_MISSING_ID = "style-node-missing-id";
|
|
442
600
|
var STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
|
|
@@ -640,7 +798,7 @@ function looksLikeTailwind(className) {
|
|
|
640
798
|
return false;
|
|
641
799
|
});
|
|
642
800
|
}
|
|
643
|
-
function
|
|
801
|
+
function asArray5(v) {
|
|
644
802
|
if (Array.isArray(v)) return v;
|
|
645
803
|
if (v && typeof v === "object") {
|
|
646
804
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -733,13 +891,13 @@ function checkNode(node, pageName, path, findings) {
|
|
|
733
891
|
}
|
|
734
892
|
function validateResponsiveStyles(stack) {
|
|
735
893
|
const findings = [];
|
|
736
|
-
const pages =
|
|
894
|
+
const pages = asArray5(stack.pages);
|
|
737
895
|
for (let p = 0; p < pages.length; p++) {
|
|
738
896
|
const page = pages[p];
|
|
739
897
|
const pageName = typeof page.name === "string" ? page.name : `pages[${p}]`;
|
|
740
|
-
const regions =
|
|
898
|
+
const regions = asArray5(page.regions);
|
|
741
899
|
for (let r = 0; r < regions.length; r++) {
|
|
742
|
-
const components =
|
|
900
|
+
const components = asArray5(regions[r].components);
|
|
743
901
|
for (let c = 0; c < components.length; c++) {
|
|
744
902
|
checkNode(components[c], pageName, `pages[${p}].regions[${r}].components[${c}]`, findings);
|
|
745
903
|
}
|
|
@@ -750,10 +908,10 @@ function validateResponsiveStyles(stack) {
|
|
|
750
908
|
|
|
751
909
|
// src/validate-jsx-pages.ts
|
|
752
910
|
import { parseJsx, compile } from "@objectstack/sdui-parser";
|
|
753
|
-
var
|
|
911
|
+
var asArray6 = (v) => Array.isArray(v) ? v : [];
|
|
754
912
|
function validateJsxPages(stack, opts = {}) {
|
|
755
913
|
const findings = [];
|
|
756
|
-
const pages =
|
|
914
|
+
const pages = asArray6(stack.pages);
|
|
757
915
|
for (let p = 0; p < pages.length; p++) {
|
|
758
916
|
const page = pages[p];
|
|
759
917
|
if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
|
|
@@ -800,10 +958,10 @@ function loadSucraseTransform() {
|
|
|
800
958
|
}
|
|
801
959
|
return cachedTransform;
|
|
802
960
|
}
|
|
803
|
-
var
|
|
961
|
+
var asArray7 = (v) => Array.isArray(v) ? v : [];
|
|
804
962
|
function validateReactPages(stack) {
|
|
805
963
|
const findings = [];
|
|
806
|
-
const pages =
|
|
964
|
+
const pages = asArray7(stack.pages);
|
|
807
965
|
for (let p = 0; p < pages.length; p++) {
|
|
808
966
|
const page = pages[p];
|
|
809
967
|
if (!page || page.kind !== "react") continue;
|
|
@@ -854,7 +1012,7 @@ function loadTypeScript() {
|
|
|
854
1012
|
}
|
|
855
1013
|
return cachedTs;
|
|
856
1014
|
}
|
|
857
|
-
var
|
|
1015
|
+
var asArray8 = (v) => Array.isArray(v) ? v : [];
|
|
858
1016
|
var BLOCKS = new Map(
|
|
859
1017
|
REACT_BLOCKS.map((b) => [
|
|
860
1018
|
b.tag,
|
|
@@ -893,7 +1051,7 @@ function nearestKnown(prop, known) {
|
|
|
893
1051
|
}
|
|
894
1052
|
function validateReactPageProps(stack) {
|
|
895
1053
|
const findings = [];
|
|
896
|
-
const pages =
|
|
1054
|
+
const pages = asArray8(stack.pages);
|
|
897
1055
|
for (let p = 0; p < pages.length; p++) {
|
|
898
1056
|
const page = pages[p];
|
|
899
1057
|
if (!page || page.kind !== "react") continue;
|
|
@@ -961,11 +1119,11 @@ function validateReactPageProps(stack) {
|
|
|
961
1119
|
|
|
962
1120
|
// src/validate-page-source-styling.ts
|
|
963
1121
|
var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
|
|
964
|
-
var
|
|
1122
|
+
var asArray9 = (v) => Array.isArray(v) ? v : [];
|
|
965
1123
|
var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
|
|
966
1124
|
function validatePageSourceStyling(stack) {
|
|
967
1125
|
const findings = [];
|
|
968
|
-
const pages =
|
|
1126
|
+
const pages = asArray9(stack.pages);
|
|
969
1127
|
for (let p = 0; p < pages.length; p++) {
|
|
970
1128
|
const page = pages[p];
|
|
971
1129
|
if (!page) continue;
|
|
@@ -994,7 +1152,7 @@ function validatePageSourceStyling(stack) {
|
|
|
994
1152
|
import { objectTitleCompleteness } from "@objectstack/spec/data";
|
|
995
1153
|
var TITLE_FORMAT_RETIRED = "title-format-retired";
|
|
996
1154
|
var TITLE_UNRESOLVABLE = "title-unresolvable";
|
|
997
|
-
function
|
|
1155
|
+
function asArray10(v) {
|
|
998
1156
|
if (Array.isArray(v)) return v;
|
|
999
1157
|
if (v && typeof v === "object") {
|
|
1000
1158
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1003,7 +1161,7 @@ function asArray9(v) {
|
|
|
1003
1161
|
}
|
|
1004
1162
|
function validateRecordTitle(stack) {
|
|
1005
1163
|
const findings = [];
|
|
1006
|
-
const objects =
|
|
1164
|
+
const objects = asArray10(stack.objects);
|
|
1007
1165
|
for (let i = 0; i < objects.length; i++) {
|
|
1008
1166
|
const obj = objects[i];
|
|
1009
1167
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
@@ -1039,7 +1197,7 @@ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
|
|
|
1039
1197
|
var FIELD_GROUP_EMPTY = "field-group-empty";
|
|
1040
1198
|
var FIELD_GROUP_SHADOWED = "field-group-shadowed";
|
|
1041
1199
|
var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
|
|
1042
|
-
function
|
|
1200
|
+
function asArray11(v) {
|
|
1043
1201
|
if (Array.isArray(v)) return v;
|
|
1044
1202
|
if (v && typeof v === "object") {
|
|
1045
1203
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1048,7 +1206,7 @@ function asArray10(v) {
|
|
|
1048
1206
|
}
|
|
1049
1207
|
function validateSemanticRoles(stack) {
|
|
1050
1208
|
const findings = [];
|
|
1051
|
-
const objects =
|
|
1209
|
+
const objects = asArray11(stack.objects);
|
|
1052
1210
|
for (let i = 0; i < objects.length; i++) {
|
|
1053
1211
|
const obj = objects[i];
|
|
1054
1212
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -1143,7 +1301,7 @@ function validateSemanticRoles(stack) {
|
|
|
1143
1301
|
// src/validate-form-layout.ts
|
|
1144
1302
|
var FORM_FIELD_UNKNOWN = "form-field-unknown";
|
|
1145
1303
|
var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
|
|
1146
|
-
function
|
|
1304
|
+
function asArray12(v) {
|
|
1147
1305
|
if (Array.isArray(v)) return v;
|
|
1148
1306
|
if (v && typeof v === "object") {
|
|
1149
1307
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1168,13 +1326,13 @@ function boundObject(view) {
|
|
|
1168
1326
|
function validateFormLayout(stack) {
|
|
1169
1327
|
const findings = [];
|
|
1170
1328
|
const objectFields = /* @__PURE__ */ new Map();
|
|
1171
|
-
for (const obj of
|
|
1329
|
+
for (const obj of asArray12(stack.objects)) {
|
|
1172
1330
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
1173
1331
|
if (!name) continue;
|
|
1174
1332
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
|
|
1175
1333
|
objectFields.set(name, new Set(fields));
|
|
1176
1334
|
}
|
|
1177
|
-
const views =
|
|
1335
|
+
const views = asArray12(stack.views);
|
|
1178
1336
|
for (let i = 0; i < views.length; i++) {
|
|
1179
1337
|
const view = views[i];
|
|
1180
1338
|
if (!view || typeof view !== "object") continue;
|
|
@@ -1224,7 +1382,7 @@ var VISIBILITY_ALIAS_DEPRECATED = "visibility-alias-deprecated";
|
|
|
1224
1382
|
var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
|
|
1225
1383
|
var CANONICAL = "visibleWhen";
|
|
1226
1384
|
var ALIASES = ["visibleOn", "visibility"];
|
|
1227
|
-
function
|
|
1385
|
+
function asArray13(v) {
|
|
1228
1386
|
if (Array.isArray(v)) return v;
|
|
1229
1387
|
if (v && typeof v === "object") {
|
|
1230
1388
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1286,7 +1444,7 @@ function isFieldObject(entry) {
|
|
|
1286
1444
|
function validateVisibilityPredicates(stack, opts = {}) {
|
|
1287
1445
|
const layer = opts.layer ?? "runtime";
|
|
1288
1446
|
const findings = [];
|
|
1289
|
-
const views =
|
|
1447
|
+
const views = asArray13(stack.views);
|
|
1290
1448
|
for (let i = 0; i < views.length; i++) {
|
|
1291
1449
|
const view = views[i];
|
|
1292
1450
|
if (!view || typeof view !== "object") continue;
|
|
@@ -1309,7 +1467,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
|
|
|
1309
1467
|
}
|
|
1310
1468
|
}
|
|
1311
1469
|
}
|
|
1312
|
-
const pages =
|
|
1470
|
+
const pages = asArray13(stack.pages);
|
|
1313
1471
|
for (let i = 0; i < pages.length; i++) {
|
|
1314
1472
|
const page = pages[i];
|
|
1315
1473
|
if (!page || typeof page !== "object") continue;
|
|
@@ -1333,7 +1491,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
|
|
|
1333
1491
|
// src/validate-capability-references.ts
|
|
1334
1492
|
import { PLATFORM_CAPABILITY_NAMES } from "@objectstack/spec/security";
|
|
1335
1493
|
var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
|
|
1336
|
-
function
|
|
1494
|
+
function asArray14(v) {
|
|
1337
1495
|
if (Array.isArray(v)) return v;
|
|
1338
1496
|
if (v && typeof v === "object") {
|
|
1339
1497
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1358,13 +1516,13 @@ function validateCapabilityReferences(stack) {
|
|
|
1358
1516
|
const findings = [];
|
|
1359
1517
|
if (!stack || typeof stack !== "object") return findings;
|
|
1360
1518
|
const known = new Set(PLATFORM_CAPABILITY_NAMES);
|
|
1361
|
-
for (const cap of
|
|
1519
|
+
for (const cap of asArray14(stack.capabilities)) {
|
|
1362
1520
|
if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
|
|
1363
1521
|
}
|
|
1364
|
-
for (const ps of
|
|
1522
|
+
for (const ps of asArray14(stack.permissions)) {
|
|
1365
1523
|
for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
|
|
1366
1524
|
}
|
|
1367
|
-
for (const seed of
|
|
1525
|
+
for (const seed of asArray14(stack.data)) {
|
|
1368
1526
|
if (seed.object !== "sys_capability") continue;
|
|
1369
1527
|
for (const rec of Array.isArray(seed.records) ? seed.records : []) {
|
|
1370
1528
|
const name = rec?.name;
|
|
@@ -1383,7 +1541,7 @@ function validateCapabilityReferences(stack) {
|
|
|
1383
1541
|
hint
|
|
1384
1542
|
});
|
|
1385
1543
|
};
|
|
1386
|
-
const objects =
|
|
1544
|
+
const objects = asArray14(stack.objects);
|
|
1387
1545
|
for (let i = 0; i < objects.length; i++) {
|
|
1388
1546
|
const obj = objects[i];
|
|
1389
1547
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -1392,27 +1550,27 @@ function validateCapabilityReferences(stack) {
|
|
|
1392
1550
|
for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
|
|
1393
1551
|
flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
|
|
1394
1552
|
}
|
|
1395
|
-
const fields =
|
|
1553
|
+
const fields = asArray14(obj.fields);
|
|
1396
1554
|
for (const f of fields) {
|
|
1397
1555
|
const fname = typeof f.name === "string" ? f.name : "(field)";
|
|
1398
1556
|
for (const cap of asCapArray(f.requiredPermissions)) {
|
|
1399
1557
|
flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
|
|
1400
1558
|
}
|
|
1401
1559
|
}
|
|
1402
|
-
for (const [ai, action] of
|
|
1560
|
+
for (const [ai, action] of asArray14(obj.actions).entries()) {
|
|
1403
1561
|
const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
|
|
1404
1562
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
1405
1563
|
flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
|
|
1406
1564
|
}
|
|
1407
1565
|
}
|
|
1408
1566
|
}
|
|
1409
|
-
for (const [i, action] of
|
|
1567
|
+
for (const [i, action] of asArray14(stack.actions).entries()) {
|
|
1410
1568
|
const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
|
|
1411
1569
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
1412
1570
|
flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
|
|
1413
1571
|
}
|
|
1414
1572
|
}
|
|
1415
|
-
const apps =
|
|
1573
|
+
const apps = asArray14(stack.apps);
|
|
1416
1574
|
for (let i = 0; i < apps.length; i++) {
|
|
1417
1575
|
const app = apps[i];
|
|
1418
1576
|
if (!app || typeof app !== "object") continue;
|
|
@@ -1439,8 +1597,14 @@ function validateCapabilityReferences(stack) {
|
|
|
1439
1597
|
}
|
|
1440
1598
|
|
|
1441
1599
|
// src/validate-approval-approvers.ts
|
|
1442
|
-
import {
|
|
1443
|
-
|
|
1600
|
+
import {
|
|
1601
|
+
ApproverType,
|
|
1602
|
+
APPROVAL_NODE_TYPE,
|
|
1603
|
+
DEPRECATED_APPROVER_TYPES,
|
|
1604
|
+
canonicalApproverType
|
|
1605
|
+
} from "@objectstack/spec/automation";
|
|
1606
|
+
var APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
|
|
1607
|
+
var APPROVAL_APPROVER_TYPE_DEPRECATED = "approval-approver-type-deprecated";
|
|
1444
1608
|
var APPROVAL_APPROVER_TYPE_UNKNOWN = "approval-approver-type-unknown";
|
|
1445
1609
|
var APPROVAL_ESCALATION_REASSIGN_NO_TARGET = "approval-escalation-reassign-no-target";
|
|
1446
1610
|
var MEMBERSHIP_TIERS = /* @__PURE__ */ new Set(["owner", "admin", "member", "guest"]);
|
|
@@ -1448,7 +1612,7 @@ var TYPE_FIX = {
|
|
|
1448
1612
|
business_unit: "department",
|
|
1449
1613
|
bu: "department"
|
|
1450
1614
|
};
|
|
1451
|
-
function
|
|
1615
|
+
function asArray15(v) {
|
|
1452
1616
|
if (Array.isArray(v)) return v;
|
|
1453
1617
|
if (v && typeof v === "object") {
|
|
1454
1618
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1458,7 +1622,7 @@ function asArray14(v) {
|
|
|
1458
1622
|
function validateApprovalApprovers(stack) {
|
|
1459
1623
|
const findings = [];
|
|
1460
1624
|
if (!stack || typeof stack !== "object") return findings;
|
|
1461
|
-
const flows =
|
|
1625
|
+
const flows = asArray15(stack.flows);
|
|
1462
1626
|
const validTypes = new Set(ApproverType.options);
|
|
1463
1627
|
for (let fi = 0; fi < flows.length; fi++) {
|
|
1464
1628
|
const flow = flows[fi];
|
|
@@ -1490,14 +1654,25 @@ function validateApprovalApprovers(stack) {
|
|
|
1490
1654
|
});
|
|
1491
1655
|
continue;
|
|
1492
1656
|
}
|
|
1493
|
-
|
|
1657
|
+
const canonical = canonicalApproverType(type);
|
|
1658
|
+
if (canonical === "org_membership_level" && value && !MEMBERSHIP_TIERS.has(value.toLowerCase())) {
|
|
1494
1659
|
findings.push({
|
|
1495
1660
|
severity: "warning",
|
|
1496
|
-
rule:
|
|
1661
|
+
rule: APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER,
|
|
1497
1662
|
where,
|
|
1498
1663
|
path: `${path}.value`,
|
|
1499
|
-
message: `approver { type: '
|
|
1500
|
-
hint: `If '${value}' is an org position, author { type: 'position', value: '${value}' } (resolved via sys_user_position, ADR-0090 D3). Keep type '
|
|
1664
|
+
message: `approver { type: '${type}', value: '${value}' } resolves against the better-auth org-membership tier (sys_member.role: owner/admin/member) \u2014 '${value}' is not a membership tier, so this approver matches nobody and the request stalls.`,
|
|
1665
|
+
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 (owner/admin/member).`
|
|
1666
|
+
});
|
|
1667
|
+
} else if (type in DEPRECATED_APPROVER_TYPES) {
|
|
1668
|
+
const fix = canonicalApproverType(type);
|
|
1669
|
+
findings.push({
|
|
1670
|
+
severity: "warning",
|
|
1671
|
+
rule: APPROVAL_APPROVER_TYPE_DEPRECATED,
|
|
1672
|
+
where,
|
|
1673
|
+
path: `${path}.type`,
|
|
1674
|
+
message: `approver type '${type}' is the deprecated spelling of '${fix}' (ADR-0090 D3) and is removed in the next major.`,
|
|
1675
|
+
hint: `Author { type: '${fix}', value: '${value}' }. It resolves identically today.`
|
|
1501
1676
|
});
|
|
1502
1677
|
}
|
|
1503
1678
|
}
|
|
@@ -1546,7 +1721,7 @@ var OWD_WIDTH = {
|
|
|
1546
1721
|
public_read: 1,
|
|
1547
1722
|
public_read_write: 2
|
|
1548
1723
|
};
|
|
1549
|
-
function
|
|
1724
|
+
function asArray16(v) {
|
|
1550
1725
|
if (Array.isArray(v)) return v;
|
|
1551
1726
|
if (v && typeof v === "object") {
|
|
1552
1727
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1572,7 +1747,7 @@ function refOf(def) {
|
|
|
1572
1747
|
return typeof r === "string" && r ? r : void 0;
|
|
1573
1748
|
}
|
|
1574
1749
|
function firstMasterDetailField(obj) {
|
|
1575
|
-
for (const f of
|
|
1750
|
+
for (const f of asArray16(obj.fields)) {
|
|
1576
1751
|
if (f.type === "master_detail") {
|
|
1577
1752
|
return { name: String(f.name ?? "?"), parent: refOf(f) };
|
|
1578
1753
|
}
|
|
@@ -1585,8 +1760,8 @@ function grantsObjectAccess(p) {
|
|
|
1585
1760
|
function validateSecurityPosture(stack, opts) {
|
|
1586
1761
|
const findings = [];
|
|
1587
1762
|
if (!stack || typeof stack !== "object") return findings;
|
|
1588
|
-
const objects =
|
|
1589
|
-
const permissionSets =
|
|
1763
|
+
const objects = asArray16(stack.objects);
|
|
1764
|
+
const permissionSets = asArray16(stack.permissions);
|
|
1590
1765
|
for (let i = 0; i < objects.length; i++) {
|
|
1591
1766
|
const obj = objects[i];
|
|
1592
1767
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -1715,10 +1890,10 @@ function validateSecurityPosture(stack, opts) {
|
|
|
1715
1890
|
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
1716
1891
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
1717
1892
|
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
1718
|
-
for (const f of
|
|
1893
|
+
for (const f of asArray16(obj.fields)) {
|
|
1719
1894
|
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
1720
1895
|
}
|
|
1721
|
-
for (const [ai, action] of
|
|
1896
|
+
for (const [ai, action] of asArray16(obj.actions).entries()) {
|
|
1722
1897
|
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
1723
1898
|
}
|
|
1724
1899
|
}
|
|
@@ -1727,19 +1902,19 @@ function validateSecurityPosture(stack, opts) {
|
|
|
1727
1902
|
if (!ps || typeof ps !== "object") continue;
|
|
1728
1903
|
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
1729
1904
|
}
|
|
1730
|
-
for (const [i, pos] of
|
|
1905
|
+
for (const [i, pos] of asArray16(stack.positions).entries()) {
|
|
1731
1906
|
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
1732
1907
|
}
|
|
1733
|
-
for (const [i, app] of
|
|
1908
|
+
for (const [i, app] of asArray16(stack.apps).entries()) {
|
|
1734
1909
|
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
1735
1910
|
}
|
|
1736
|
-
for (const [i, book] of
|
|
1911
|
+
for (const [i, book] of asArray16(stack.books).entries()) {
|
|
1737
1912
|
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
1738
1913
|
}
|
|
1739
1914
|
const stackSetNames = new Set(
|
|
1740
1915
|
permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
|
|
1741
1916
|
);
|
|
1742
|
-
for (const [i, book] of
|
|
1917
|
+
for (const [i, book] of asArray16(stack.books).entries()) {
|
|
1743
1918
|
const audience = book.audience;
|
|
1744
1919
|
if (!audience || typeof audience !== "object") continue;
|
|
1745
1920
|
const setName = audience.permissionSet;
|
|
@@ -1817,7 +1992,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
1817
1992
|
}
|
|
1818
1993
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
1819
1994
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
1820
|
-
for (const [i, seed] of
|
|
1995
|
+
for (const [i, seed] of asArray16(stack.data).entries()) {
|
|
1821
1996
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
1822
1997
|
if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
|
|
1823
1998
|
const records = Array.isArray(seed.records) ? seed.records : [];
|
|
@@ -1858,7 +2033,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
1858
2033
|
}
|
|
1859
2034
|
|
|
1860
2035
|
// src/build-access-matrix.ts
|
|
1861
|
-
function
|
|
2036
|
+
function asArray17(v) {
|
|
1862
2037
|
if (Array.isArray(v)) return v;
|
|
1863
2038
|
if (v && typeof v === "object") {
|
|
1864
2039
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1869,13 +2044,13 @@ function buildAccessMatrix(stack) {
|
|
|
1869
2044
|
const entries = [];
|
|
1870
2045
|
if (!stack || typeof stack !== "object") return { version: 1, entries };
|
|
1871
2046
|
const owdByObject = /* @__PURE__ */ new Map();
|
|
1872
|
-
for (const obj of
|
|
2047
|
+
for (const obj of asArray17(stack.objects)) {
|
|
1873
2048
|
const name = typeof obj.name === "string" ? obj.name : "";
|
|
1874
2049
|
if (!name) continue;
|
|
1875
2050
|
const owd = obj.sharingModel ?? obj.security?.sharingModel;
|
|
1876
2051
|
if (typeof owd === "string") owdByObject.set(name, owd);
|
|
1877
2052
|
}
|
|
1878
|
-
for (const ps of
|
|
2053
|
+
for (const ps of asArray17(stack.permissions)) {
|
|
1879
2054
|
const psName = typeof ps.name === "string" ? ps.name : "";
|
|
1880
2055
|
if (!psName) continue;
|
|
1881
2056
|
const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
|
|
@@ -1946,14 +2121,17 @@ function diffAccessMatrix(before, after) {
|
|
|
1946
2121
|
return lines;
|
|
1947
2122
|
}
|
|
1948
2123
|
export {
|
|
2124
|
+
APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER,
|
|
2125
|
+
APPROVAL_APPROVER_TYPE_DEPRECATED,
|
|
1949
2126
|
APPROVAL_APPROVER_TYPE_UNKNOWN,
|
|
1950
2127
|
APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
|
|
1951
|
-
APPROVAL_ROLE_NOT_MEMBERSHIP_TIER,
|
|
1952
2128
|
CAPABILITY_REFERENCE_UNKNOWN,
|
|
1953
2129
|
CHART_CONFIG_MISSING,
|
|
1954
2130
|
CHART_FIELD_UNKNOWN,
|
|
1955
2131
|
FIELD_GROUP_EMPTY,
|
|
1956
2132
|
FIELD_GROUP_UNDECLARED,
|
|
2133
|
+
FLOW_DRAFT_STATUS_AMBIGUOUS,
|
|
2134
|
+
FLOW_TRIGGER_UNKNOWN_OBJECT,
|
|
1957
2135
|
FORM_COLSPAN_ABSOLUTE,
|
|
1958
2136
|
FORM_FIELD_UNKNOWN,
|
|
1959
2137
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
@@ -1979,6 +2157,7 @@ export {
|
|
|
1979
2157
|
TABLE_COUNT_ONLY,
|
|
1980
2158
|
TITLE_FORMAT_RETIRED,
|
|
1981
2159
|
TITLE_UNRESOLVABLE,
|
|
2160
|
+
VIEW_CONTAINER_SHAPE,
|
|
1982
2161
|
VISIBILITY_ALIAS_DEPRECATED,
|
|
1983
2162
|
VISIBILITY_ROOT_MISLAYERED,
|
|
1984
2163
|
WIDGET_DATASET_UNKNOWN,
|
|
@@ -1988,6 +2167,7 @@ export {
|
|
|
1988
2167
|
diffAccessMatrix,
|
|
1989
2168
|
validateApprovalApprovers,
|
|
1990
2169
|
validateCapabilityReferences,
|
|
2170
|
+
validateFlowTriggerReadiness,
|
|
1991
2171
|
validateFormLayout,
|
|
1992
2172
|
validateJsxPages,
|
|
1993
2173
|
validateListViewMode,
|
|
@@ -1999,6 +2179,7 @@ export {
|
|
|
1999
2179
|
validateSecurityPosture,
|
|
2000
2180
|
validateSemanticRoles,
|
|
2001
2181
|
validateStackExpressions,
|
|
2182
|
+
validateViewContainers,
|
|
2002
2183
|
validateVisibilityPredicates,
|
|
2003
2184
|
validateWidgetBindings
|
|
2004
2185
|
};
|