@nanobpm/urban 0.81.0 → 0.82.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.
@@ -40,8 +40,8 @@ export { applyAmbientLineage, buildLineageTree, deriveLineage, LINEAGE_KEY, LINE
40
40
  export type { ApplyLineageOptions, LineageAttachment, LineageEdge, LineageEdgeType, LineageEnvelope, LineageNode, LineageTree, } from "./core/lineage.ts";
41
41
  export { LINEAGE_ATTACHMENTS_TABLE, LINEAGE_EDGES_TABLE, LINEAGE_SCHEMA_SQL, LineageStore, systemClock as lineageSystemClock, } from "./core/modules/lineage-store.ts";
42
42
  export type { Clock as LineageClock, LineageJobLike, LineageStoreOptions, } from "./core/modules/lineage-store.ts";
43
- export { and, assertReadModelParity, assertSqlIdentifier, caseWhen, col, compileToFn, compileToSqlSelect, defineReadModel, deriveReadModelViewDdl, eq, exists, gt, gte, lit, lt, lte, neq, not, or, pcol, ProjectionRegistry, projectionRegistry, ReadModelRegistry, readModelRegistry, when, } from "./core/read-model.ts";
44
- export type { AndExpr, BaseRow, CaseExpr, ColExpr, CompareExpr, CompareOp, DerivationFn, ExistsExpr, Expr, Literal, LitExpr, NotExpr, OrExpr, ParityDb, ParityOptions, ParitySample, ProjColExpr, ProjectionRows, ProjectionSource, ReadModel, ReadModelDecl, SqlCompileOptions, WhenClause, } from "./core/read-model.ts";
43
+ export { add, and, assertReadModelParity, assertRollupParity, assertSqlIdentifier, caseWhen, coalesce, col, compileToFn, compileToSqlSelect, count, countWhere, defineReadModel, defineRollup, deriveReadModelViewDdl, eq, exists, fromProjection, fromRollup, fromTable, gt, gte, isNotNull, isNull, joinSource, lit, lt, lte, max, minWhere, neq, not, or, pcol, ProjectionRegistry, projectionRegistry, rcol, ReadModelRegistry, readModelRegistry, RollupRegistry, rollupRegistry, when, } from "./core/read-model.ts";
44
+ export type { AddAgg, AggExpr, AndExpr, BaseRow, CaseExpr, ColExpr, CoalesceAgg, CompareExpr, CompareOp, CountAgg, CountWhereAgg, DerivationFn, ExistsExpr, Expr, IsNullExpr, Literal, LitExpr, LookupRows, MaxAgg, MinWhereAgg, NotExpr, OrExpr, ParityDb, ParityOptions, ParitySample, ProjColExpr, ProjectionRows, ProjectionSource, ReadModel, ReadModelDecl, RollupColExpr, Rollup, RollupDecl, RollupInputs, RollupJoinSource, RollupLookupDecl, RollupParityOptions, RollupProjectionSource, RollupRelation, RollupRollupSource, RollupSource, RollupTableSource, SqlCompileOptions, WhenClause, } from "./core/read-model.ts";
45
45
  export { CANONICAL_PROJECTIONS, registerCanonicalProjections, } from "./core/modules/canonical-projections.ts";
46
46
  export { OPEN_USER_TASKS_PROJECTION, OPEN_USER_TASKS_SCHEMA_SQL, OPEN_USER_TASKS_TABLE, OpenUserTasksStore, } from "./core/modules/open-user-tasks-store.ts";
47
47
  export type { OpenUserTaskRow, OpenUserTasksStoreOptions, } from "./core/modules/open-user-tasks-store.ts";
@@ -52,7 +52,12 @@ export { LINEAGE_ATTACHMENTS_TABLE, LINEAGE_EDGES_TABLE, LINEAGE_SCHEMA_SQL, Lin
52
52
  // workers.ts) applies its managed VIEW. The `writer-source-inversion` task registers here.
53
53
  // • `projectionRegistry` — register a canonical engine-truth projection NAME usable in `exists(...)`.
54
54
  // The `canonical-projections` task registers `urban_open_user_tasks` / `urban_instance_state`.
55
- export { and, assertReadModelParity, assertSqlIdentifier, caseWhen, col, compileToFn, compileToSqlSelect, defineReadModel, deriveReadModelViewDdl, eq, exists, gt, gte, lit, lt, lte, neq, not, or, pcol, ProjectionRegistry, projectionRegistry, ReadModelRegistry, readModelRegistry, when, } from "./core/read-model.js";
55
+ // The GROUP-BY sibling `defineRollup` (ADR 0065, #468): a declare-once aggregate rollup compiled to
56
+ // BOTH a managed `*_counts` VIEW and a TS group-reduce (closed aggregate op set: `count`/`countWhere`/
57
+ // `max`/`minWhere`/`coalesce`/`add`), registered as a canonical projection via `rollupRegistry` and
58
+ // parity-guarded by `assertRollupParity`. `defineReadModel` gains a key-correlated rollup lookup
59
+ // (`ReadModelDecl.lookups` + `rcol`) so per-row derivations read a rollup's columns for the base row.
60
+ export { add, and, assertReadModelParity, assertRollupParity, assertSqlIdentifier, caseWhen, coalesce, col, compileToFn, compileToSqlSelect, count, countWhere, defineReadModel, defineRollup, deriveReadModelViewDdl, eq, exists, fromProjection, fromRollup, fromTable, gt, gte, isNotNull, isNull, joinSource, lit, lt, lte, max, minWhere, neq, not, or, pcol, ProjectionRegistry, projectionRegistry, rcol, ReadModelRegistry, readModelRegistry, RollupRegistry, rollupRegistry, when, } from "./core/read-model.js";
56
61
  // Canonical engine-truth projection sidecars (ADR 0065, proposal point #1): two framework-owned,
57
62
  // per-source projections the DSL `exists(...)` derives over — `urban_open_user_tasks` (which instances
58
63
  // are parked on a human) and `urban_instance_state` (per-instance engine lifecycle state). Provisioned
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.81.0",
3
+ "version": "0.82.0",
4
4
  "description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -11,7 +11,7 @@ import { runInJobContext } from "../execContext.ts";
11
11
  import type { JobExecContext } from "../execContext.ts";
12
12
  import { readLineage } from "../lineage.ts";
13
13
  import { LineageStore } from "./lineage-store.ts";
14
- import { readModelRegistry } from "../read-model.ts";
14
+ import { readModelRegistry, rollupRegistry } from "../read-model.ts";
15
15
  import { registerCanonicalProjections } from "./canonical-projections.ts";
16
16
  import { InstanceStateStore } from "./instance-state-store.ts";
17
17
  import { OpenUserTasksStore } from "./open-user-tasks-store.ts";
@@ -139,6 +139,25 @@ function tryLineageStore(app: AppApi, log: RuntimeContext["host"]["log"]): Linea
139
139
  }
140
140
  }
141
141
 
142
+ /**
143
+ * Apply every managed rollup VIEW registered in the process-wide `rollupRegistry` (ADR 0065, #468) to
144
+ * the app's DEFAULT data source, in dependency order (a composed rollup after the rollup it reads).
145
+ * Mirrors {@link tryProvisionReadModelViews}: empty until a sibling task registers a rollup, and
146
+ * absent-/error-safe so a missing datasource or a VIEW-provisioning failure never breaks a worker mount.
147
+ */
148
+ function tryProvisionRollupViews(app: AppApi, log: RuntimeContext["host"]["log"]): void {
149
+ if (rollupRegistry.all().length === 0) return;
150
+ if (!app.data.hasDefaultSource()) {
151
+ log("debug", "rollups: no default data source; managed VIEW provisioning skipped");
152
+ return;
153
+ }
154
+ try {
155
+ rollupRegistry.ensureViews(app.data.source().db);
156
+ } catch (err) {
157
+ log("warn", "rollups: failed to provision managed VIEWs", { error: String(err) });
158
+ }
159
+ }
160
+
142
161
  /**
143
162
  * Apply every managed read-model VIEW registered in the process-wide `readModelRegistry` to the app's
144
163
  * DEFAULT data source (ADR 0065, surface #3). A `defineReadModel(...)` declaration compiles to a
@@ -251,6 +270,10 @@ export async function mountWorkers(
251
270
  // provision the projection tables per source (absent-safe), and finally the read-model VIEWs.
252
271
  registerCanonicalProjections();
253
272
  tryProvisionCanonicalProjections(app, ctx.host.log);
273
+ // Rollup VIEWs (ADR 0065, #468) BEFORE the read-model VIEWs: a read model's key-correlated rollup
274
+ // lookup `LEFT JOIN`s a rollup's `*_counts` VIEW, and a composed rollup reads another rollup's VIEW,
275
+ // so every rollup VIEW (in dependency order) must exist before any dependent VIEW compiles.
276
+ tryProvisionRollupViews(app, ctx.host.log);
254
277
  tryProvisionReadModelViews(app, ctx.host.log);
255
278
 
256
279
  // Cache module loads so a multi-type handler module is imported once.
@@ -24,6 +24,7 @@ import {
24
24
  pcol,
25
25
  ProjectionRegistry,
26
26
  projectionRegistry,
27
+ rcol,
27
28
  ReadModelRegistry,
28
29
  isReservedObjectName,
29
30
  RESERVED_OBJECT_PREFIXES,
@@ -247,8 +248,32 @@ test("compileToFn resolves col/pcol identifiers case-insensitively (SQLite ident
247
248
  assert.equal(corr({ key: "pi-1" }, { p: [{ process_instance_key: "pi-2" }] }), false);
248
249
  });
249
250
 
250
- test("mixed-case col/pcol identifiers: the VIEW and TS backends agree (VIEW/TS parity on the no-edge fallback)", async () => {
251
- // End-to-end guard for the case-fold fix: author a model with mixed-case identifiers over lowercase
251
+ test("compileToFn / rcol reads the resolved lookup row by OWN property (a `__proto__` alias can't resolve Object.prototype)", () => {
252
+ // Lookup aliases are only identifier-validated, so `__proto__` is a legal alias. A bare `fnFor` caller
253
+ // supplies a plain `{}` for `lookups`; a raw `lookups["__proto__"]` resolves the inherited
254
+ // `Object.prototype` (truthy), bypassing the "missing lookup row → throw" guard and reading NULLs off the
255
+ // prototype chain. Reading by OWN property keeps the guard fail-loud on the bare-caller path.
256
+ const fn = compileToFn(rcol("__proto__", "amount"));
257
+ assert.throws(() => fn({}), /without a resolved lookup row/);
258
+ // A genuinely-supplied own `__proto__` row still resolves (null-prototype dict, an own property).
259
+ const lookups: Record<string, Record<string, unknown>> = Object.create(null);
260
+ lookups.__proto__ = { amount: 7 };
261
+ assert.equal(fn({}, {}, lookups), 7);
262
+ });
263
+
264
+ test("compileToFn / exists reads projections by OWN property (a `__proto__` projection can't resolve Object.prototype)", () => {
265
+ // Projection names are user-controlled, so `__proto__` is legal. A bare `fnFor` caller passes a plain
266
+ // `{}` for projections; a raw `projections["__proto__"]` resolves the inherited `Object.prototype`
267
+ // (truthy, non-array), so `.some(...)` throws a TypeError. Reading by OWN property yields an empty EXISTS.
268
+ const fn = compileToFn(exists("__proto__", eq(col("k"), lit(1))));
269
+ assert.equal(fn({ k: 1 }), false); // no such projection → empty EXISTS, not a prototype read
270
+ // A genuinely-supplied own `__proto__` projection still matches.
271
+ const projections: Record<string, ReadonlyArray<Record<string, unknown>>> = Object.create(null);
272
+ projections.__proto__ = [{ k: 1 }];
273
+ assert.equal(fn({ k: 1 }, projections), true);
274
+ });
275
+
276
+ test("mixed-case col/pcol identifiers: the VIEW and TS backends agree (VIEW/TS parity on the no-edge fallback)", async () => { // End-to-end guard for the case-fold fix: author a model with mixed-case identifiers over lowercase
252
277
  // columns and confirm the SQLite VIEW and the TS backend derive the SAME value — including the
253
278
  // parity-critical no-edge fallback that passes the stored status through.
254
279
  const mixed: ReadModel = defineReadModel({