@nanobpm/urban 0.60.0 → 0.62.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.
@@ -336,3 +336,18 @@ export interface EngineClient {
336
336
  /** Tear down all connections. */
337
337
  close(): Promise<void>;
338
338
  }
339
+ /**
340
+ * The names of every method on {@link EngineClient} — the single *runtime* source of
341
+ * truth for the interface's surface. It is compile-time-pinned to `keyof EngineClient`
342
+ * in both directions (see the two assertions below): the `satisfies` rejects a stray or
343
+ * misspelled name, and the exhaustiveness assertion rejects an *omitted* one, so adding,
344
+ * renaming, or removing an `EngineClient` method that this list does not mirror fails
345
+ * `npm run typecheck`.
346
+ *
347
+ * A fake/adapter (e.g. `@nanobpm/urban-testkit`'s `WasmEngineClient`) can iterate this
348
+ * list to assert at *runtime* that it implements the full surface — catching the
349
+ * "the SDK grew a method the test double lags" seam-lag class (issue #341: `openUserTasks`,
350
+ * and `getForm` before it) that a purely structural `implements EngineClient` check no
351
+ * longer catches once the fake is published/compiled against an older `@nanobpm/urban`.
352
+ */
353
+ export declare const ENGINE_CLIENT_METHODS: readonly ["deployResources", "createInstance", "cancelInstance", "publishMessage", "searchUserTasks", "openUserTasks", "getForm", "completeUserTask", "searchProcessInstances", "registerWorker", "close"];
@@ -42,3 +42,31 @@ export function presentFormIdentifier(value) {
42
42
  const trimmed = value?.trim();
43
43
  return trimmed ? trimmed : undefined;
44
44
  }
45
+ /**
46
+ * The names of every method on {@link EngineClient} — the single *runtime* source of
47
+ * truth for the interface's surface. It is compile-time-pinned to `keyof EngineClient`
48
+ * in both directions (see the two assertions below): the `satisfies` rejects a stray or
49
+ * misspelled name, and the exhaustiveness assertion rejects an *omitted* one, so adding,
50
+ * renaming, or removing an `EngineClient` method that this list does not mirror fails
51
+ * `npm run typecheck`.
52
+ *
53
+ * A fake/adapter (e.g. `@nanobpm/urban-testkit`'s `WasmEngineClient`) can iterate this
54
+ * list to assert at *runtime* that it implements the full surface — catching the
55
+ * "the SDK grew a method the test double lags" seam-lag class (issue #341: `openUserTasks`,
56
+ * and `getForm` before it) that a purely structural `implements EngineClient` check no
57
+ * longer catches once the fake is published/compiled against an older `@nanobpm/urban`.
58
+ */
59
+ export const ENGINE_CLIENT_METHODS = [
60
+ "deployResources",
61
+ "createInstance",
62
+ "cancelInstance",
63
+ "publishMessage",
64
+ "searchUserTasks",
65
+ "openUserTasks",
66
+ "getForm",
67
+ "completeUserTask",
68
+ "searchProcessInstances",
69
+ "registerWorker",
70
+ "close",
71
+ ];
72
+ const _engineClientMethodsAreExhaustive = true;
@@ -39,6 +39,15 @@ function javascript(body, status = 200, extraHeaders) {
39
39
  /** A SQL identifier guard — a table name must be a bare identifier *and* a known table
40
40
  * (checked against `schema()`), so `/app/data/:table` can never inject SQL. */
41
41
  const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
42
+ // A query flag (e.g. ?count=1) is "on" when present and not one of the falsy
43
+ // spellings. A bare `?count` (empty string) still counts as on, so a caller can
44
+ // opt in without inventing a value.
45
+ function isTruthyParam(v) {
46
+ if (v === null)
47
+ return false;
48
+ const s = v.trim().toLowerCase();
49
+ return s !== "0" && s !== "false" && s !== "no";
50
+ }
42
51
  /**
43
52
  * Build the page-runtime routes over an injected datasource + engine. Pure over its deps
44
53
  * so it is unit-testable without a real host/server (see pages.test.ts).
@@ -225,6 +234,22 @@ export function createPagesRoutes(opts, deps) {
225
234
  orderSql = ` ORDER BY ${quoteIdent(field)} ${dir}`;
226
235
  }
227
236
  const whereSql = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
237
+ // ?count=1 → a lightweight COUNT(*) instead of the rows themselves. The same
238
+ // whitelisted where clauses apply, so a caller (e.g. a nav item's live count
239
+ // badge) gets exactly the number of matching rows without transferring or
240
+ // materialising them. ORDER BY / LIMIT are irrelevant to a count and skipped.
241
+ if (isTruthyParam(req.query.get("count"))) {
242
+ try {
243
+ const rows = await db.query(`SELECT COUNT(*) AS n FROM ${quoteIdent(table)}${whereSql}`, params);
244
+ const first = Array.isArray(rows) && rows.length > 0 ? rows[0] : undefined;
245
+ const rawCount = isRecord(first) ? first.n : 0;
246
+ const count = Number(rawCount);
247
+ return json({ count: Number.isFinite(count) ? count : 0 });
248
+ }
249
+ catch (e) {
250
+ return json({ error: errorMessage(e) }, 500);
251
+ }
252
+ }
228
253
  try {
229
254
  const rows = await db.query(`SELECT * FROM ${quoteIdent(table)}${whereSql}${orderSql} LIMIT ${rowLimit}`, params);
230
255
  return json({ rows });
@@ -630,6 +655,11 @@ table.pc-grid th { font-weight:600; color:var(--nano-text-muted); }
630
655
  .pc-nav-link:hover { background:var(--nano-hover); color:var(--nano-text); }
631
656
  .pc-nav-link.active { background:var(--nano-accent); color:var(--nano-on-accent); }
632
657
  .pc-nav-icon { line-height:1; }
658
+ /* A nav item's live count pill (item.badge). Slightly more compact than the base
659
+ .pc-badge so it sits neatly beside a nav label; [hidden] must win over the
660
+ .pc-badge inline-flex display so hideWhenZero / a failed fetch truly hides it. */
661
+ .pc-nav-badge { min-width:1.1rem; height:1.1rem; font-size:.68rem; padding:0 .35rem; margin-left:.1rem; }
662
+ .pc-nav-badge[hidden] { display:none; }
633
663
  .pc-nav-empty { color:var(--nano-text-faint); font-size:.85rem; padding:.4rem .7rem; }
634
664
  /* The per-row mobile "More" toggle (revealing mobile:{priority:"hidden"} columns)
635
665
  only exists for the card view — hidden on the desktop table, shown by the