You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 68d0003
Browse filesBrowse the repository at this point in the historyBrowse files
`create_record` / `update_record` field values accept the CEL value envelope, declared and evaluated together.
8
+
9
+
A value in a `create_record` or `update_record` node's `fields` map may now be a CEL value envelope, `{ dialect: 'cel', source: '…' }`, with the same shape and dialect rules the `assignment` node's `assignments` map already has. The envelope is evaluated by the expression engine that flow conditions use, so the whole CEL stdlib is reachable from a field value, and the result is written with its type kept:
10
+
11
+
```ts
12
+
fields: {
13
+
subject: 'Quote for {account.name}', // `{token}` template — unchanged
14
+
total: { dialect: 'cel', source: 'round(amount * 100.0) / 100.0' }, // CEL, evaluated to the value written
15
+
}
16
+
```
17
+
18
+
Clause-②: yes (widening) — a published authoring slot's accept set grows (a valid envelope in `fields.*` is newly evaluated), and the one newly refused shape is the edge the `assignments` map accepted when it gained the envelope: a malformed one.
19
+
20
+
**What newly passes.** A valid CEL value envelope as a top-level `fields` value, on both nodes. Before this release the executor wrote such an object into the record verbatim: a text or JSON column stored `{"dialect":"cel","source":"…"}` and the run reported success, and a number column was refused by the data engine.
21
+
22
+
**What newly refuses.** A top-level `fields` value that is a plain object with a string `dialect` key and is NOT a valid CEL value envelope. That covers a missing, empty or whitespace-only `source`, an `ast` with no `source`, a `template` or `cron` dialect, and a `source` that does not parse as CEL. Every door refuses it, located at `config.fields.<field>`: `AutomationEngine.registerFlow` refuses the flow, `objectstack validate` reports an `expression-invalid` error, the runtime publish gate answers `422 INVALID_METADATA`, and the node's execute-time contract parse refuses it. Such an object used to be written as data.
23
+
24
+
**The rule for nested and literal values.** Only the top-level value of each field is judged. An object nested inside a JSON value or an array is data, whatever keys it carries, and strings inside it still interpolate. A plain string is always a `{token}` template with its existing meaning, and every other literal is written as before. A JSON column whose intended literal value is itself an object with a string `dialect` key is now read as an envelope. To write such an object as data, bind it to a flow variable and write `'{thatVariable}'` (a sole token keeps its type). Measured: no flow in this repository or in HotCRM writes an envelope-shaped object into `fields`.
25
+
26
+
**The refusal sentence is slot-neutral.** A refused field value used to be told it was "an assignment value". The sentence every value-slot refusal leads with is now `VALUE_ENVELOPE_REFUSAL`: "A value carrying a `dialect` key is read as an expression envelope, and this one is not a valid CEL value envelope." The published `ASSIGNMENT_VALUE_ENVELOPE_REFUSAL` is kept and is the same string, so code that matches on the constant keeps matching. Code that matched the old literal text ("An assignment value carrying…") does not.
27
+
28
+
**New in `@objectstack/spec/automation`** (5 exports, 0 removed):
29
+
30
+
-`VALUE_ENVELOPE_REFUSAL`, the slot-neutral refusal sentence.
31
+
-`FlowValueSlotSchema` / `FlowValueSlot` / `FlowValueSlotParsed`, the value contract every value slot shares (`AssignmentValueSchema` is the same rule under the assignment map's description).
32
+
-`resolveFlowNodeValueSlots(nodeType, config)`, which returns every authored value in the ledger's value slots, strings included.
33
+
- The expression ledger `FLOW_NODE_EXPRESSION_PATHS` has two new rows, `create_record.fields.*` and `update_record.fields.*` (role `value`), and `LEDGER_DECLARED_NODE_CONFIG_SCHEMAS` carries both CRUD contracts.
34
+
35
+
**Author-time hint (`@objectstack/lint`).**`objectstack validate` warns when a value slot holds a `{…}` template expression, meaning arithmetic or a call to `round` / `floor` / `ceil` / `abs` / `min` / `max`, and points it at the envelope. The warning never fails a build, and the template form keeps working unchanged. Plain references, the `NOW()` / `TODAY()` macros and `$User` paths are not hinted. CEL's `now()` / `today()` are timestamps rather than the strings those macros write, and the flow's CEL scope binds no user.
36
+
37
+
**Corrected guidance: `/ 100.0`, not `/ 100`.** The template dialect's `round()` arity refusal used to call `round(x * 100) / 100` the CEL authoring pattern. In CEL that expression truncates: `round()` returns an int, and int / int is integer division, so `x = 1234.5678` gives `1234` instead of `1234.57`. The refusal now prescribes `round(x * 100) / 100.0`, which is correct in both dialects. In the template dialect `/ 100` and `/ 100.0` give the same value.
`@objectstack/spec/data` declares which field types are masked on read — `MASKED_ON_READ_FIELD_TYPES` and `isMaskedOnReadFieldType(fieldType, managedBy)` (#20141)
7
+
8
+
Clause-②: yes
9
+
10
+
The protocol used to state this only in prose (the `FieldType` comments), so
11
+
two consumers each carried their own hand-written copy: objectql's
12
+
`collectMaskedReadFields` (the generic read mask and the echoed-mask write
13
+
guard) and the renderer's masked-type set. The fact is now declared once:
14
+
15
+
-`MASKED_ON_READ_FIELD_TYPES` — a deep-frozen per-type rule table:
16
+
`secret` is masked on every object; `password` is masked on every object
17
+
except `managedBy: 'better-auth'` ones. Each masked type carries its own
18
+
`exemptManagedBy` list, typed against `ObjectSchema.managedBy`'s enum.
19
+
-`isMaskedOnReadFieldType(fieldType, managedBy)` — the one reading of that
20
+
table. `managedBy` is a required argument (pass `undefined` when the object
21
+
has none), and exemptions fail closed: an absent or unlisted `managedBy`
22
+
never unmasks a masked type.
23
+
24
+
`@objectstack/objectql`: `collectMaskedReadFields` and
25
+
`collectMaskedPasswordFields` now ask `isMaskedOnReadFieldType` instead of
26
+
carrying their own `type === 'secret'` / `'password'` arms. No behaviour
27
+
change: the masked-on-read answer is identical for every `FieldType` ×
28
+
`managedBy` cell, pinned by a table test.
29
+
30
+
`@objectstack/spec`: `ObjectSchema.create()`'s author-time warning for a `password` field on a non-auth object now reads its `managedBy` exemption from `isMaskedOnReadFieldType` instead of hard-coding `'better-auth'`, so it follows the declaration; which objects and fields warn is unchanged.
31
+
32
+
A client that renders credential fields (show the mask, offer no copy) should
33
+
derive its set from `isMaskedOnReadFieldType` rather than keep its own list,
34
+
so the server's mask and the client's cannot drift apart.
@@ -1785,7 +1795,16 @@ means the same thing inside braces.
1785
1795
| Start-node `condition`|**CEL** (bare, no braces) |`record.amount > 500`|`record.*`, `previous.*`, bare field names, `vars.*`|
1786
1796
| Edge `condition`|**CEL** (bare, no braces) |`record.status == 'open'`| same as above |
1787
1797
| Decision-node `conditions[].expression`|**CEL** (bare, no braces) |`order_amount > 10000`| flow variables by name, and `vars.*`|
1788
-
| Field values in `create_record` / `update_record`|**Interpolation** (braces required) |`'Follow up on {record.name}'`, `'{TODAY() + 7}'`|`{var}`, `{var.path}`, `{$User.Id}`, `{$User.Email}`, `{NOW()}`, `{TODAY()}`, `{TODAY() + 90}` (whole days), and the CEL-mirrored numeric functions `round`, `floor`, `ceil`, `abs`, `min`, `max` (#11060) — `round` is **integer-only**, exactly like CEL's (there is no `round(x, 2)`); for N decimals write the CEL idiom `{round(x * 100) / 100}` (scale 2) |
1798
+
| Field values in `create_record` / `update_record`|**Interpolation** (braces required) |`'Follow up on {record.name}'`, `'{TODAY() + 7}'`|`{var}`, `{var.path}`, `{$User.Id}`, `{$User.Email}`, `{NOW()}`, `{TODAY()}`, `{TODAY() + 90}` (whole days), and the CEL-mirrored numeric functions `round`, `floor`, `ceil`, `abs`, `min`, `max` (#11060) — `round` is **integer-only**, exactly like CEL's (there is no `round(x, 2)`); for N decimals write `{round(x * 100) / 100.0}` (scale 2). Keep the decimal point: in CEL `round()` returns an int and `int / int` is integer division, so `round(x * 100) / 100` drops the decimals there — `/ 100.0` is right in both dialects |
1799
+
| Field values and assignment values, as a **CEL value envelope**|**CEL** (in an envelope) |`{ dialect: 'cel', source: 'round(price * 100.0) / 100.0' }`| flow variables by name, and `vars.*` — the whole CEL stdlib (`joinNonEmpty`, …) |
1800
+
1801
+
A value slot takes either form, chosen by shape: a string is interpolation, an
1802
+
object naming a `dialect` is a CEL envelope. The template form keeps working
1803
+
unchanged; `objectstack validate` points a template **expression** — arithmetic
1804
+
or one of the six functions inside braces — at the envelope with a warning,
1805
+
never an error. Plain references (`{record.name}`), the date macros and
1806
+
`{$User.*}` are left alone: CEL's `now()` / `today()` are timestamps, not the
1807
+
strings the macros write, and the flow's CEL scope binds no user.
0 commit comments