|
1 | 1 | # @objectstack/example-crm |
2 | 2 |
|
| 3 | +## 4.0.92-rc.2 |
| 4 | + |
| 5 | +### Patch Changes |
| 6 | + |
| 7 | +- e533b0b: feat(spec)!: retire `datasource.capabilities` — eleven flags nothing read, one of them a safety claim (#4583) |
| 8 | + |
| 9 | + `DatasourceCapabilities` declared eleven booleans — `transactions`, seven `query*` |
| 10 | + flags, `joins`, `fullTextSearch`, `readOnly`, `dynamicSchema` — all strict-guarded, |
| 11 | + all read by nothing. Pushdown is decided by the runtime driver's own `supports.*` |
| 12 | + object, a different mechanism entirely, so a datasource declaring |
| 13 | + `queryAggregations: false` never once changed which engine path ran. The block is |
| 14 | + removed rather than bridged: there was nothing on the other side to connect it to. |
| 15 | + |
| 16 | + **`readOnly` is why this is not tidy-up.** It reads as a safety property and was |
| 17 | + authored as one — the shipped CRM example labelled a datasource "CRM Analytics Read |
| 18 | + Replica" on the strength of it, while the datasource accepted writes exactly like the |
| 19 | + primary. The key had already been MOVED twice toward somewhere it might be enforced, |
| 20 | + out of `config` in #4410 and into `capabilities` in #4465, and was inert at every |
| 21 | + address. This removes it instead of moving it a third time. |
| 22 | + |
| 23 | + **Removing it does not hand you a working replacement, and the rejection says so.** |
| 24 | + The one enforced datasource-wide write gate is `external.allowWrites: false`, and it |
| 25 | + applies only to a FEDERATED datasource — `assertWriteAllowed` returns early for a |
| 26 | + `managed` (or unset-`schemaMode`) datasource, so that key would be equally inert for a |
| 27 | + local database. **A managed datasource has no read-only gate at all**; that gap is |
| 28 | + #4584, deliberately not invented here. Until it is answered, enforce read-only where |
| 29 | + it is real: grant the connection SELECT-only at the database. |
| 30 | + |
| 31 | + FROM → TO: |
| 32 | + |
| 33 | + ```ts |
| 34 | + // before — parsed cleanly, changed nothing |
| 35 | + defineDatasource({ |
| 36 | + name: 'analytics', driver: 'sqlite', config: { filename: ':memory:' }, |
| 37 | + capabilities: { readOnly: true, queryAggregations: true }, |
| 38 | + }) |
| 39 | + |
| 40 | + // after — delete the block; for a FEDERATED datasource the enforced gate is: |
| 41 | + defineDatasource({ |
| 42 | + name: 'warehouse', driver: 'postgres', config: { … }, |
| 43 | + schemaMode: 'external', |
| 44 | + external: { allowWrites: false }, |
| 45 | + }) |
| 46 | + ``` |
| 47 | + |
| 48 | + `os migrate meta --from 16` rewrites it automatically (ADR-0087 conversion |
| 49 | + `datasource-capabilities-removed`). Both `DatasourceSchema` and |
| 50 | + `DriverDefinitionSchema` are `.strict()`, so a leftover key is a loud rejection |
| 51 | + carrying the prescription — never a silent strip. |
| 52 | + |
| 53 | + Also fixed: `READ_ONLY_BELONGS_ON_DATASOURCE`, the prescription every SQL driver |
| 54 | + shares for a `readOnly` written inside `config`, was still sending authors _to_ the |
| 55 | + removed key. It now names the enforced gate and states plainly where that gate does |
| 56 | + not apply — a prescription that lands on an inert key manufactures exactly the belief |
| 57 | + it was meant to correct. |
| 58 | + |
| 59 | + The `datasource` liveness ledger drops from 20 dead properties to 9 (remaining: |
| 60 | + `healthCheck` ×3, `retryPolicy` ×4, `external` ×2 — batches B/C/D of #4583). |
| 61 | + |
| 62 | +- 5293114: fix(automation): a decision's three declared ways to route a branch are now one working model (#4414) |
| 63 | + |
| 64 | + A `decision` node advertised three mechanisms for splitting a path and only one |
| 65 | + of them did anything. The other two were the ADR-0049 `declared ≠ enforced` |
| 66 | + shape, and the pair of them shipped a guard that does not guard in |
| 67 | + `examples/app-crm`. |
| 68 | + |
| 69 | + | mechanism | before | now | |
| 70 | + | :--------------------------------------------------- | :----------------------------------------------------------------------------------------------------- | :---------------------------------------------------- | |
| 71 | + | `edge.condition` | ✅ the only one that worked | unchanged | |
| 72 | + | `edge.isDefault` | **zero readers** anywhere but the schema declaration | BPMN default flow, enforced in `traverseNext` | |
| 73 | + | `decision.config.conditions[].label` → `branchLabel` | matched **0** out-edge labels across every example app, then fell back to the full edge set in silence | routes; an unclaimable label is logged, not swallowed | |
| 74 | + |
| 75 | + ## What was broken, end to end |
| 76 | + |
| 77 | + `crm_convert_lead_wizard` means "already converted → abort screen; otherwise → |
| 78 | + the wizard". It ran **both**: an already-converted lead got |
| 79 | + "This lead has already been converted" and then walked straight into the |
| 80 | + conversion wizard behind it. Four independent silences stacked up: |
| 81 | + |
| 82 | + 1. the decision's first condition was authored `{lead_record.status} == |
| 83 | +'converted'` — braces in a slot declared bare CEL, so it was string-compared |
| 84 | + and never true; |
| 85 | + 2. the second (`'true'`) therefore won, yielding `branchLabel: 'No — proceed'`; |
| 86 | + 3. no out-edge carried that label (they were `'Yes'` / `'No'`), so traversal |
| 87 | + discarded the branch and considered every out-edge; |
| 88 | + 4. `e3b` was unconditional, so it ran regardless — and the natural fix, marking |
| 89 | + it `isDefault: true`, was a dead key. |
| 90 | + |
| 91 | + ## The model |
| 92 | + |
| 93 | + `branchLabel` narrows the edge set → `condition` gates each edge → `isDefault` |
| 94 | + catches whatever is left. Concretely: |
| 95 | + |
| 96 | + - **`isDefault` is enforced.** A default edge is traversed only when no |
| 97 | + conditional sibling of the same source node matched, and it is no longer part |
| 98 | + of the unconditional parallel fan-out — that distinction is the whole point of |
| 99 | + the marker. Passed over because a real branch won, its target records the same |
| 100 | + `skipped` step a closed gate does (#4354). |
| 101 | + - **An unclaimable branch label warns.** Traversal still falls back to the full |
| 102 | + edge set (a run mid-flight must not die on a metadata error) but says so, |
| 103 | + naming the computed branch and the out-edge labels that exist. |
| 104 | + - **A decision that declares no `conditions` reports no branch.** It used to |
| 105 | + report `'default'` unconditionally — a label no out-edge in the repo ever |
| 106 | + carried — which is why every decision node fell back to the full edge set. |
| 107 | + The `'default'` sentinel survives for the case it actually describes (declared |
| 108 | + conditions, none matched) and is now claimed by the `isDefault` edge as well |
| 109 | + as by an edge literally labelled `'default'`. |
| 110 | + - **`conditions[].expression` is evaluated as the bare CEL it is declared to |
| 111 | + be.** The raw string went to the legacy `{var}` template path, where |
| 112 | + `lead.status == 'converted'` cannot resolve and the branch is decided by |
| 113 | + string comparison. Unlike `edge.condition` this slot carries no |
| 114 | + `ExpressionInput` envelope — the decision descriptor is deliberately |
| 115 | + schemaless — so the executor supplies the dialect. A brace-in-CEL predicate |
| 116 | + now fails loudly (ADR-0032 §1c) instead of deciding `false`. |
| 117 | + |
| 118 | + ## Caught at authoring time too |
| 119 | + |
| 120 | + Four new `os build` / `os validate` warnings, because a wrong route is silent at |
| 121 | + run time by nature (Prime Directive #12): |
| 122 | + |
| 123 | + `flow-branch-label-unmatched` (the shipped shape), |
| 124 | + `flow-decision-unconditional-branch` (a guarded decision with an unconditional |
| 125 | + sibling — the actual hole), `flow-default-edge-with-condition` and |
| 126 | + `flow-multiple-default-edges`. |
| 127 | + |
| 128 | + Both of the first two fire on the pre-fix `convert-lead.flow.ts` and are silent |
| 129 | + after it. |
| 130 | + |
| 131 | + ## Effect on flows that already exist |
| 132 | + |
| 133 | + Enforcing `isDefault` changes how a **stored** flow behaves, and the flows it |
| 134 | + changes are mostly Studio's own. `objectui`'s flow edge inspector has always |
| 135 | + written `isDefault: true` when you bind an out-edge to a decision's default/else |
| 136 | + branch — into a key with zero readers, so that edge ran unconditionally, in |
| 137 | + parallel with whichever branch actually matched. Those flows now take exactly |
| 138 | + one branch. That is the fix, but it is a behaviour change on existing data |
| 139 | + rather than only on newly authored metadata, so it is worth knowing before |
| 140 | + upgrading: a flow that quietly ran two paths will now run one. |
| 141 | + |
| 142 | + Nothing changes for an edge that never carried the marker — `isDefault` defaults |
| 143 | + to `false`, and an ordinary unconditional out-edge still fans out in parallel |
| 144 | + exactly as before. |
| 145 | + |
| 146 | + ## The example app |
| 147 | + |
| 148 | + `crm_convert_lead_wizard`'s guard is now a plain exclusive gateway: the |
| 149 | + redundant `config.conditions` is gone and `e3b` carries `isDefault: true`. One |
| 150 | + mechanism per decision, and exactly one branch runs. |
| 151 | + |
| 152 | + Verified: 11 new engine/executor tests (including the reported repro in both |
| 153 | + directions), 12 new linter tests; `@objectstack/service-automation` 577 tests |
| 154 | + and `@objectstack/cli` 652 tests green, all three example apps build with no new |
| 155 | + findings. |
| 156 | + |
| 157 | +- Updated dependencies [430dcc2] |
| 158 | +- Updated dependencies [80334c7] |
| 159 | +- Updated dependencies [ce5242c] |
| 160 | +- Updated dependencies [a7163ea] |
| 161 | +- Updated dependencies [e6e9379] |
| 162 | +- Updated dependencies [98877c9] |
| 163 | +- Updated dependencies [98877c9] |
| 164 | +- Updated dependencies [e6b1b69] |
| 165 | +- Updated dependencies [7e7a605] |
| 166 | +- Updated dependencies [ad047d2] |
| 167 | +- Updated dependencies [2826d1e] |
| 168 | +- Updated dependencies [5a84d41] |
| 169 | +- Updated dependencies [20b1a9e] |
| 170 | +- Updated dependencies [203a449] |
| 171 | +- Updated dependencies [4820f55] |
| 172 | +- Updated dependencies [462d9c4] |
| 173 | +- Updated dependencies [7d21581] |
| 174 | +- Updated dependencies [f2445c9] |
| 175 | +- Updated dependencies [23338c3] |
| 176 | +- Updated dependencies [5b843fb] |
| 177 | +- Updated dependencies [b4487aa] |
| 178 | +- Updated dependencies [65ca83a] |
| 179 | +- Updated dependencies [67bf2e2] |
| 180 | +- Updated dependencies [c6d1cb4] |
| 181 | +- Updated dependencies [36030ff] |
| 182 | +- Updated dependencies [6117f7b] |
| 183 | +- Updated dependencies [e533b0b] |
| 184 | +- Updated dependencies [cdf4d9a] |
| 185 | +- Updated dependencies [aee1806] |
| 186 | +- Updated dependencies [c13350b] |
| 187 | +- Updated dependencies [c13350b] |
| 188 | +- Updated dependencies [63b33e6] |
| 189 | +- Updated dependencies [9ca2d85] |
| 190 | +- Updated dependencies [c13350b] |
| 191 | +- Updated dependencies [a52e2ef] |
| 192 | +- Updated dependencies [5293114] |
| 193 | +- Updated dependencies [ff17642] |
| 194 | +- Updated dependencies [20bc357] |
| 195 | +- Updated dependencies [5966c2a] |
| 196 | +- Updated dependencies [2382580] |
| 197 | +- Updated dependencies [3c7bcc0] |
| 198 | +- Updated dependencies [4b6cac7] |
| 199 | +- Updated dependencies [7631964] |
| 200 | +- Updated dependencies [ac471a0] |
| 201 | +- Updated dependencies [60ae58e] |
| 202 | +- Updated dependencies [ce92674] |
| 203 | +- Updated dependencies [51c5227] |
| 204 | +- Updated dependencies [a4a85c8] |
| 205 | +- Updated dependencies [07a4e26] |
| 206 | +- Updated dependencies [ec975f1] |
| 207 | +- Updated dependencies [eb4204b] |
| 208 | +- Updated dependencies [4f13be2] |
| 209 | +- Updated dependencies [d52d4fe] |
| 210 | +- Updated dependencies [742cebb] |
| 211 | +- Updated dependencies [ce92674] |
| 212 | +- Updated dependencies [cf2c9b7] |
| 213 | +- Updated dependencies [0f9faa2] |
| 214 | +- Updated dependencies [7cf42fe] |
| 215 | +- Updated dependencies [5966c2a] |
| 216 | +- Updated dependencies [8aacf94] |
| 217 | +- Updated dependencies [f78dd83] |
| 218 | +- Updated dependencies [a2cd18a] |
| 219 | +- Updated dependencies [4638aaa] |
| 220 | +- Updated dependencies [0222d3c] |
| 221 | +- Updated dependencies [071d0dc] |
| 222 | +- Updated dependencies [0a936ea] |
| 223 | +- Updated dependencies [155507e] |
| 224 | +- Updated dependencies [7bba90b] |
| 225 | +- Updated dependencies [061406d] |
| 226 | +- Updated dependencies [c1f344b] |
| 227 | +- Updated dependencies [9c93465] |
| 228 | +- Updated dependencies [ebb209c] |
| 229 | +- Updated dependencies [63b33e6] |
| 230 | +- Updated dependencies [2a44c1d] |
| 231 | +- Updated dependencies [071d0dc] |
| 232 | +- Updated dependencies [0848bea] |
| 233 | +- Updated dependencies [b8b3c64] |
| 234 | +- Updated dependencies [0c0fbd9] |
| 235 | +- Updated dependencies [f3141d8] |
| 236 | +- Updated dependencies [5a84d41] |
| 237 | +- Updated dependencies [fd3013a] |
| 238 | +- Updated dependencies [21676eb] |
| 239 | +- Updated dependencies [e336549] |
| 240 | +- Updated dependencies [e5e7ee0] |
| 241 | +- Updated dependencies [800bdb0] |
| 242 | +- Updated dependencies [04f1182] |
| 243 | +- Updated dependencies [5647006] |
| 244 | +- Updated dependencies [38f7e4f] |
| 245 | +- Updated dependencies [c57f3cf] |
| 246 | +- Updated dependencies [97faca3] |
| 247 | +- Updated dependencies [ad5fe25] |
| 248 | +- Updated dependencies [ea90179] |
| 249 | +- Updated dependencies [ce92674] |
| 250 | +- Updated dependencies [5ef0b5b] |
| 251 | +- Updated dependencies [48fbacb] |
| 252 | +- Updated dependencies [355e951] |
| 253 | +- Updated dependencies [dadb43f] |
| 254 | + - @objectstack/runtime@17.0.0-rc.2 |
| 255 | + - @objectstack/spec@17.0.0-rc.2 |
| 256 | + |
3 | 257 | ## 4.0.92-rc.1 |
4 | 258 |
|
5 | 259 | ### Patch Changes |
|
0 commit comments