|
26 | 26 | // |
27 | 27 | // And the runtime consequence is not the benign "consumer skips the unknown |
28 | 28 | // name and does the rest" that keeps `page-field-unknown` / `form-field-unknown` |
29 | | -// advisory. Nothing between the node and storage removes the key: the flow |
30 | | -// executor calls the data engine directly (bypassing the metadata-protocol |
31 | | -// ingress, which strips `readonly` — not unknown — keys anyway), the engine's |
32 | | -// write paths strip only readonly/readonlyWhen, and the SQL driver's |
33 | | -// `formatInput` / `applyWriteColumnMap` pass an unrecognized key straight |
34 | | -// through (`m[k] ?? k`). Every branch below was measured, not inferred: |
| 29 | +// advisory. The flow executor calls the data engine directly (`data.insert` / |
| 30 | +// `data.update` in service-automation's `builtin/crud-nodes.ts`, bypassing the |
| 31 | +// metadata-protocol ingress), so the node's `fields` map arrives as an ordinary |
| 32 | +// CALLER payload — and [#13858] the declared-field door (#8682 insert, #8738 |
| 33 | +// update) refuses a caller-named undeclared key from the object's field map |
| 34 | +// before any statement is built. Every branch below was measured through the |
| 35 | +// real AutomationEngine, the real builtin CRUD nodes, the real engine and BOTH |
| 36 | +// driver families (driver-sql on better-sqlite3, driver-memory), not inferred: |
35 | 37 | // |
36 | | -// • Through the engine, an undeclared key reaches `driver.update` / |
37 | | -// `driver.create` verbatim, alongside the audit stamps. |
38 | | -// • On SQLite/knex an UPDATE becomes `update "deal" set "name" = 'n2', |
39 | | -// "stagee" = 'won' … → no such column: stagee`. The statement is rejected |
40 | | -// WHOLE: `name` — spelled correctly, in the same payload — does not land |
41 | | -// either, and the step fails with a driver error naming a column, far from |
42 | | -// the authoring mistake. |
43 | | -// • An INSERT fails the same way (`table deal has no column named stagee`), |
44 | | -// and one notch harder: the row is never created at all, so every later |
45 | | -// node that expected `{<node>.id}` is working from a record that does not |
46 | | -// exist. |
47 | | -// • On a schemaless datasource (memory, MongoDB) nothing rejects it, so the |
48 | | -// stray key is persisted into a column the object never declares — where no |
49 | | -// schema-driven read surface will return it. |
| 38 | +// • Both families answer identically — `INVALID_FIELD` / 400, "Unknown field |
| 39 | +// 'stagee' on object 'deal'". No driver is reached, so there is no split to |
| 40 | +// observe. |
| 41 | +// • The write is refused WHOLE: `name` — spelled correctly, in the same |
| 42 | +// payload — does not land either. |
| 43 | +// • On `create_record` the row is never created at all, so every later node |
| 44 | +// that expected `{<node>.id}` is working from a record that does not exist. |
| 45 | +// • The node catches the refusal and folds it into a step failure |
| 46 | +// (`create_record(deal) failed: Unknown field 'stagee' on object 'deal'`), |
| 47 | +// so the RUN fails — far from the authoring mistake, which is exactly why |
| 48 | +// an author-time rule is still worth having. |
| 49 | +// |
| 50 | +// ⚠️ Until #13858 this block described the pre-#8682 driver split (SQL rejected |
| 51 | +// the statement, a schemaless datasource persisted the stray key). That is |
| 52 | +// retired, not merely restated: the severity below is unchanged because neither |
| 53 | +// the old outcome nor the new one is ever "the rest still works". |
50 | 54 | // |
51 | 55 | // No outcome is "the rest still works". That is the same call |
52 | 56 | // `validate-searchable-fields` makes for a stale entry and |
@@ -290,12 +294,18 @@ export function validateFlowNodeWrites(stack: AnyRec): FlowNodeWriteFinding[] { |
290 | 294 | where: `flow "${flowName}" › ${nodeWhere}`, |
291 | 295 | path: `${nodePath}.config.fields.${fieldName}`, |
292 | 296 | message: |
293 | | - `${node.type} writes '${fieldName}', but object '${objectName}' declares no such field. Nothing ` + |
294 | | - `between the node and storage removes the key: on a SQL datasource the driver rejects the whole ` + |
295 | | - `statement ('no such column'), so the correctly named fields in this same payload never land ` + |
296 | | - `either${ |
| 297 | + // [#13858] The node hands `fields` to the data engine directly |
| 298 | + // (`data.insert` / `data.update` in service-automation's |
| 299 | + // crud-nodes), so it is a CALLER payload and the #8682/#8738 |
| 300 | + // declared-field door refuses it before any datasource is reached. |
| 301 | + // Measured on driver-sql and driver-memory alike. |
| 302 | + `${node.type} writes '${fieldName}', but object '${objectName}' declares no such field. The ` + |
| 303 | + `node hands its fields map to the engine as an ordinary caller payload, so the ` + |
| 304 | + `declared-field door REFUSES the whole write — INVALID_FIELD / 400, identically on every ` + |
| 305 | + `datasource, before any statement is built. The correctly named fields in this same payload ` + |
| 306 | + `never land either${ |
297 | 307 | node.type === 'create_record' ? ' and the record is never created at all' : '' |
298 | | - }; on a schemaless one the stray key is persisted into a column no read surface returns.`, |
| 308 | + }, and the step fails the run.`, |
299 | 309 | hint: fixHint(fieldName, [...known]), |
300 | 310 | }); |
301 | 311 | } |
|
0 commit comments