@datarails-ai/dr-appspace-cli 0.9.3 → 0.9.5

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.
Files changed (2) hide show
  1. package/dist/cli.js +110 -14
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import { fileURLToPath as fileURLToPath6 } from "url";
13
13
  // package.json
14
14
  var package_default = {
15
15
  name: "@datarails-ai/dr-appspace-cli",
16
- version: "0.9.3",
16
+ version: "0.9.5",
17
17
  description: "Datarails App Space CLI \u2014 build, try, push and publish App Space apps",
18
18
  type: "module",
19
19
  engines: {
@@ -57,7 +57,7 @@ var package_default = {
57
57
  };
58
58
 
59
59
  // src/buildInfo.ts
60
- var BUILD_TIME = "2026-09-30T10:53:27.024Z";
60
+ var BUILD_TIME = "2026-09-30T11:19:50.440Z";
61
61
  function formatBuildTime(iso) {
62
62
  if (iso === "") return "";
63
63
  const d = new Date(iso);
@@ -4009,6 +4009,7 @@ var FILTER_ATTR = "data-dr-filter";
4009
4009
  var ARGS_ATTR = "data-dr-args";
4010
4010
  var PROPS_ATTR = "data-dr-props";
4011
4011
  var DECLARES_ATTR = "data-dr-declares";
4012
+ var WHEN_ATTR = "data-dr-when";
4012
4013
  var COMPONENT_TYPES2 = COMPONENT_TYPES;
4013
4014
  var COMPONENT_TYPE_SET = new Set(COMPONENT_TYPES2);
4014
4015
  var MODULE_RE = /^components\/[a-z][a-z0-9_-]*\.js$/;
@@ -5236,6 +5237,7 @@ function propsOf(node, text3, tree) {
5236
5237
  if (ARGS_ATTR in node.attrs) props.args = parseArgs(node.attrs[ARGS_ATTR]);
5237
5238
  if (PROPS_ATTR in node.attrs) props.props = parseProps(node.attrs[PROPS_ATTR]);
5238
5239
  if (DECLARES_ATTR in node.attrs) props.declares = parseDeclares(node.attrs[DECLARES_ATTR]);
5240
+ if (WHEN_ATTR in node.attrs) props.when = parseArgs(node.attrs[WHEN_ATTR]);
5239
5241
  const inputType = inputTypeOf(node);
5240
5242
  if (inputType !== null) props.input_type = inputType;
5241
5243
  if ("required" in node.attrs) props.required = true;
@@ -7702,6 +7704,8 @@ var COMPONENT_PROPS3 = /* @__PURE__ */ new Set([
7702
7704
  "props",
7703
7705
  // What a custom's module declares it does (`data-dr-declares`, DC-5), one JSON object.
7704
7706
  "declares",
7707
+ // A row action's states (`data-dr-when`, dr-app-runtime 1.6.3), one JSON object.
7708
+ "when",
7705
7709
  "input_type",
7706
7710
  "required",
7707
7711
  "options"
@@ -12761,7 +12765,7 @@ var HANDLER_KEYS = [
12761
12765
  "measures",
12762
12766
  "requiresRole"
12763
12767
  ];
12764
- var ATTR_ORDER = ["component", "id", "entity", "action", "source", "field", "filter", "param", "measure", "kind", "x", "y", "series", "format", "module", "args", "props", "declares", "role", "label", "tone"];
12768
+ var ATTR_ORDER = ["component", "id", "entity", "action", "source", "field", "filter", "param", "measure", "kind", "x", "y", "series", "format", "module", "args", "props", "declares", "when", "role", "label", "tone"];
12765
12769
  var ENTITY_WRITERS2 = /* @__PURE__ */ new Set(["form", "list", "queue", "detail", "filter"]);
12766
12770
  var DECIMAL_TYPES = /* @__PURE__ */ new Set(["decimal", "numeric", "money"]);
12767
12771
  var NUMBER_TYPES = /* @__PURE__ */ new Set(["decimal", "numeric", "money", "int", "integer", "bigint", "float"]);
@@ -12798,28 +12802,35 @@ function roleListOf(element) {
12798
12802
  const role = isPlainObject7(element?.props) ? element.props.role : void 0;
12799
12803
  return Array.isArray(role) ? role : null;
12800
12804
  }
12801
- function foldRoleLists(elements) {
12805
+ function roleListCoversHandler(element, handlers) {
12806
+ const listed = roleListOf(element);
12807
+ if (listed === null || element?.type !== "action" || !listed.every(named4)) return false;
12808
+ const gate = handlers?.[element.props?.action]?.requiresRole;
12809
+ return Array.isArray(gate) && gate.length > 0 && gate.every((role) => listed.includes(role));
12810
+ }
12811
+ function foldRoleLists(elements, handlers = {}) {
12802
12812
  const folding = Object.keys(elements).filter((uid) => {
12803
12813
  const listed = roleListOf(elements[uid]);
12804
- return listed !== null && listed.length === 1 && named4(listed[0]);
12814
+ return listed !== null && (listed.length === 1 && named4(listed[0]) || roleListCoversHandler(elements[uid], handlers));
12805
12815
  });
12806
12816
  if (folding.length === 0) return elements;
12807
12817
  const out = { ...elements };
12808
12818
  for (const uid of folding) {
12809
12819
  const element = out[uid];
12810
- out[uid] = { ...element, props: { ...element.props, role: element.props.role[0] } };
12820
+ const { role, ...rest } = element.props;
12821
+ out[uid] = { ...element, props: role.length === 1 && named4(role[0]) ? { ...element.props, role: role[0] } : rest };
12811
12822
  }
12812
12823
  return out;
12813
12824
  }
12814
- function roleListProblems(elements) {
12825
+ function roleListProblems(elements, handlers = {}) {
12815
12826
  const problems = [];
12816
12827
  for (const uid of Object.keys(elements).sort(natural)) {
12817
12828
  const listed = roleListOf(elements[uid]);
12818
- if (listed === null || listed.length === 1 && named4(listed[0])) continue;
12829
+ if (listed === null || listed.length === 1 && named4(listed[0]) || roleListCoversHandler(elements[uid], handlers)) continue;
12819
12830
  problems.push({
12820
12831
  code: "app_model_mismatch",
12821
12832
  pointer: `/elements/${escapePointer3(uid)}/props/role`,
12822
- message: `"${uid}" states props.role ${JSON.stringify(listed)}, and a role gate names one role: it is written as data-dr-role and the page shows the element only to the caller who holds that role \u2014 write the one role as a string, or leave props.role off and let the handler's requiresRole name every role that may call it`
12833
+ message: `"${uid}" states props.role ${JSON.stringify(listed)}, and a role gate names one role: it is written as data-dr-role and the page shows the element only to the caller who holds that role \u2014 write the one role as a string, or leave props.role off and let the handler's requiresRole name every role that may call it (a button is drawn only for a caller the handler lets through)`
12823
12834
  });
12824
12835
  }
12825
12836
  return problems;
@@ -13458,8 +13469,9 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
13458
13469
  }
13459
13470
  problems.push(...appIdProblems(app));
13460
13471
  const typed = isPlainObject7(contract.elements) ? contract.elements : {};
13461
- problems.push(...statedIdProblems(typed), ...roleListProblems(typed));
13462
- const declared = foldDialogTriggers(foldNavRegions(foldHeaderRoles(foldRoleLists(foldStatedIds(typed)))));
13472
+ const gates = isPlainObject7(contract.data?.app?.handlers) ? contract.data.app.handlers : {};
13473
+ problems.push(...statedIdProblems(typed), ...roleListProblems(typed, gates));
13474
+ const declared = foldDialogTriggers(foldNavRegions(foldHeaderRoles(foldRoleLists(foldStatedIds(typed), gates))));
13463
13475
  problems.push(...headerPropsProblems(declared));
13464
13476
  const fromBundle = Object.keys(declared).filter((uid) => declared[uid]?.source === "bundle").sort(natural);
13465
13477
  if (fromBundle.length > 0) {
@@ -13549,7 +13561,7 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
13549
13561
  }
13550
13562
  const tree = treeOf(declared);
13551
13563
  problems.push(...tree.problems);
13552
- if (tree.problems.length === 0) problems.push(...byProblems(declared, tree, handlers, model), ...pickerProblems(declared, tree, handlers, model));
13564
+ if (tree.problems.length === 0) problems.push(...byProblems(declared, tree, handlers, model), ...pickerProblems(declared, tree, handlers, model), ...whenProblems(declared, tree, model));
13553
13565
  problems.push(...computedProblems(declared, tree, handlers, model));
13554
13566
  if (tree.problems.length === 0) problems.push(...modelProblems(declared, tree, handlers, model, rolesOf2(contract.backend?.roles, model), sources));
13555
13567
  if (tree.problems.length === 0) problems.push(...historyProblems(declared, tree, handlers, model));
@@ -14808,6 +14820,45 @@ function computedProblems(elements, tree, handlers, model) {
14808
14820
  }
14809
14821
  return problems;
14810
14822
  }
14823
+ function whenProblems(elements, tree, model) {
14824
+ const problems = [];
14825
+ for (const uid of Object.keys(elements).sort(natural)) {
14826
+ const element = elements[uid] ?? {};
14827
+ const when = element.props?.when;
14828
+ if (when === void 0 || when === null) continue;
14829
+ const at = `/elements/${escapePointer3(uid)}/props/when`;
14830
+ const push = (message, pointer2 = at) => problems.push({ code: "app_model_mismatch", pointer: pointer2, message });
14831
+ const holder = elements[parentOf(tree, uid)] ?? null;
14832
+ if (element.type !== "action" || !["list", "queue"].includes(holder?.type)) {
14833
+ push(`"${uid}" states when, and when says which rows a row action shows on: only an action inside a list or a queue has a row \u2014 drop when`);
14834
+ continue;
14835
+ }
14836
+ if (!isPlainObject7(when) || Object.keys(when).length === 0) {
14837
+ push(`"${uid}" states when as ${JSON.stringify(when)}; when is an object of column to the list of values the row may hold \u2014 {"status": ["open", "in_progress"]}`);
14838
+ continue;
14839
+ }
14840
+ const entity = entityOf(uid, elements, tree);
14841
+ for (const column of Object.keys(when).sort()) {
14842
+ const states = when[column];
14843
+ const pointer2 = `${at}/${escapePointer3(column)}`;
14844
+ if (!Array.isArray(states) || states.length === 0 || !states.every(named4)) {
14845
+ push(`"${uid}" states when.${column} as ${JSON.stringify(states)}; each column takes a non-empty list of the values it may hold, as strings \u2014 even one: ["open"]`, pointer2);
14846
+ continue;
14847
+ }
14848
+ if (entity === null || !Object.prototype.hasOwnProperty.call(model.tables, entity)) continue;
14849
+ if (!hasColumn(model, entity, column)) {
14850
+ push(`"${uid}" shows only where "${column}" holds ${JSON.stringify(states)}, and "${entity}" has no column "${column}"`, pointer2);
14851
+ continue;
14852
+ }
14853
+ const values = model.tables[entity].columns[column]?.enum ?? null;
14854
+ const stray = values === null ? [] : states.filter((state) => !values.includes(state));
14855
+ if (stray.length > 0) {
14856
+ push(`"${uid}" shows only where "${column}" holds ${JSON.stringify(stray)}, which "${entity}.${column}" cannot hold (${values.join(", ")})`, pointer2);
14857
+ }
14858
+ }
14859
+ }
14860
+ return problems;
14861
+ }
14811
14862
  function pickerProblems(elements, tree, handlers, model) {
14812
14863
  const problems = [];
14813
14864
  const push = (pointer2, message) => problems.push({ code: "app_model_mismatch", pointer: pointer2, message });
@@ -16365,6 +16416,11 @@ function attrsOf(element) {
16365
16416
  if (written !== "") out.push(["declares", written]);
16366
16417
  continue;
16367
16418
  }
16419
+ if (key === "when") {
16420
+ const written = writeArgs(value);
16421
+ if (written !== "") out.push(["when", written]);
16422
+ continue;
16423
+ }
16368
16424
  if (typeof value !== "string") continue;
16369
16425
  out.push([key, value]);
16370
16426
  }
@@ -17643,6 +17699,7 @@ var FILTER_ATTR2 = "data-dr-filter";
17643
17699
  var ARGS_ATTR2 = "data-dr-args";
17644
17700
  var PROPS_ATTR2 = "data-dr-props";
17645
17701
  var DECLARES_ATTR2 = "data-dr-declares";
17702
+ var WHEN_ATTR2 = "data-dr-when";
17646
17703
  var COMPONENT_TYPES5 = COMPONENT_TYPES4;
17647
17704
  var COMPONENT_TYPE_SET2 = new Set(COMPONENT_TYPES5);
17648
17705
  var MODULE_RE2 = /^components\/[a-z][a-z0-9_-]*\.js$/;
@@ -18822,6 +18879,7 @@ function propsOf2(node, text3, tree) {
18822
18879
  if (ARGS_ATTR2 in node.attrs) props.args = parseArgs2(node.attrs[ARGS_ATTR2]);
18823
18880
  if (PROPS_ATTR2 in node.attrs) props.props = parseProps2(node.attrs[PROPS_ATTR2]);
18824
18881
  if (DECLARES_ATTR2 in node.attrs) props.declares = parseDeclares2(node.attrs[DECLARES_ATTR2]);
18882
+ if (WHEN_ATTR2 in node.attrs) props.when = parseArgs2(node.attrs[WHEN_ATTR2]);
18825
18883
  const inputType = inputTypeOf2(node);
18826
18884
  if (inputType !== null) props.input_type = inputType;
18827
18885
  if ("required" in node.attrs) props.required = true;
@@ -20423,11 +20481,15 @@ function checkComponents(app, contract, contractError) {
20423
20481
  if (typeof role !== "string" || role === "" || roles.includes(role)) continue;
20424
20482
  violations.push({ rule: "A3.8", where: uid, message: `data-dr-role="${role}" names no role manifest.json declares (${roles.join(", ")})` });
20425
20483
  }
20484
+ const mounted = new Set(
20485
+ app.ui.filter((file) => [...file.text.matchAll(SCRIPT_SRC)].some((match) => RUNTIME_JS.test(match[1]))).map((file) => file.path)
20486
+ );
20426
20487
  let flat = false;
20427
20488
  for (const uid of uids) {
20428
20489
  const element = elements[uid];
20429
20490
  const required = actions[element.props?.action]?.requires_role ?? [];
20430
20491
  if (required.length === 0) continue;
20492
+ if (element.type === "action" && mounted.has(element.page)) continue;
20431
20493
  if (marked(elements, uid, required, pageRoles3)) continue;
20432
20494
  if (element.source === "bundle") {
20433
20495
  flat = true;
@@ -21162,7 +21224,8 @@ function rolesOf5(manifest) {
21162
21224
  }
21163
21225
 
21164
21226
  // ../flows/appspace-builder/kit/checks/rules/ux.mjs
21165
- var RULES4 = ["A3.23", "A3.24", "A3.25"];
21227
+ var RULES4 = ["A3.23", "A3.24", "A3.25", "A3.26", "A3.27"];
21228
+ var STATE_COLUMNS = ["status", "state"];
21166
21229
  var isPlainObject13 = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
21167
21230
  var said = (v) => typeof v === "string" && v.trim() !== "" ? v : null;
21168
21231
  var OPENERS3 = /* @__PURE__ */ new Set(["dialog", "list", "queue", "detail"]);
@@ -21249,6 +21312,39 @@ function checkUx(app, contract, contractError) {
21249
21312
  message: `dialog "${nameOf5(uid)}" on ${el.page} states no props.trigger, no label and no title, so its button has no words (dialog-no-trigger); state props.trigger \u2014 what the button says ("New ticket")`
21250
21313
  });
21251
21314
  }
21315
+ for (const uid of uids) {
21316
+ const el = elements[uid];
21317
+ if (!ROWS.has(el?.type) || el.type === "data_grid") continue;
21318
+ const table = tableOf2(el);
21319
+ const columns = isPlainObject13(tables[table]?.columns) ? tables[table].columns : {};
21320
+ const state = STATE_COLUMNS.find((c) => isPlainObject13(columns[c]) && Array.isArray(columns[c].enum));
21321
+ const seen = /* @__PURE__ */ new Map();
21322
+ for (const kid of kidsOf(uid)) {
21323
+ const action = elements[kid];
21324
+ if (action?.type !== "action" || !said(action.props?.action)) continue;
21325
+ const name = action.props.action;
21326
+ const role = handlers[name]?.role ?? "custom";
21327
+ const label = said(action.props?.label) ?? name;
21328
+ const twin = seen.get(`${name}
21329
+ ${label}`);
21330
+ if (twin !== void 0) {
21331
+ violations.push({
21332
+ rule: "A3.27",
21333
+ where: kid,
21334
+ message: `${el.type} "${nameOf5(uid)}" on ${el.page} draws "${label}" (${name}) twice \u2014 ${twin} and ${kid} (row-action-twins); keep one action element and drop its props.role: the button shows for every caller the handler's requiresRole lets through`
21335
+ });
21336
+ continue;
21337
+ }
21338
+ seen.set(`${name}
21339
+ ${label}`, kid);
21340
+ if (role !== "custom" || state === void 0 || action.props?.when != null) continue;
21341
+ violations.push({
21342
+ rule: "A3.26",
21343
+ where: kid,
21344
+ message: `row action "${label}" (${name}, a custom handler) on ${el.type} "${nameOf5(uid)}" states no when, so it shows on every row whatever its ${state} (row-action-stateless). State the values of ${state} it runs from \u2014 "when": {"${state}": ["${columns[state].enum.slice(0, 2).join('", "')}"]} \u2014 and have the handler refuse any other; a handler that only moves the row from one state to another is a decide with a guard, which hides its button the same way`
21345
+ });
21346
+ }
21347
+ }
21252
21348
  return { violations, skipped: [] };
21253
21349
  }
21254
21350
  function byPageThenRank(a, b) {
@@ -21828,7 +21924,7 @@ function kitFromDisk2() {
21828
21924
  }
21829
21925
  return kitOnDisk2;
21830
21926
  }
21831
- var ADVISORY = /* @__PURE__ */ new Set(["A1.5", "A1.6", "A2.5", "A2.6", "A3.15", "A3.17", "A3.19", "A3.20", "A3.21", "A3.22", "A3.23", "A3.24", "A3.25"]);
21927
+ var ADVISORY = /* @__PURE__ */ new Set(["A1.5", "A1.6", "A2.5", "A2.6", "A3.15", "A3.17", "A3.19", "A3.20", "A3.21", "A3.22", "A3.23", "A3.24", "A3.25", "A3.26", "A3.27"]);
21832
21928
  function verify2({ app: appRef }, { extractContract: extractContract2 = extract2, kit } = {}) {
21833
21929
  const KIT = kit ?? kitFromDisk2();
21834
21930
  const app = loadApp2(resolve12(appRef.dir));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datarails-ai/dr-appspace-cli",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "Datarails App Space CLI — build, try, push and publish App Space apps",
5
5
  "type": "module",
6
6
  "engines": {