@lotics/app-sdk 0.120.1 → 0.120.2
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.
- package/dist/index.js +1 -1
- package/docs/mutations.md +2 -2
- package/docs/workflows.md +10 -33
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25051,7 +25051,7 @@ var tableDateFieldSchema = tableFieldBaseSchema.extend({
|
|
|
25051
25051
|
format: dateFormatSchema.optional(),
|
|
25052
25052
|
timezone: zod_default.string().optional().describe("IANA timezone"),
|
|
25053
25053
|
derive_from: zod_default.enum(["created_at", "updated_at"]).optional().describe(
|
|
25054
|
-
"Auto-populate from the row's system timestamp. `created_at` stamps once at insert time; `updated_at` re-stamps on every record edit. A derived field is read-only: a write naming it is refused, as for formula/rollup/lookup.
|
|
25054
|
+
"Auto-populate from the row's system timestamp. `created_at` stamps once at insert time; `updated_at` re-stamps on every record edit. A derived field is read-only: a write naming it is refused, as for formula/rollup/lookup."
|
|
25055
25055
|
),
|
|
25056
25056
|
// A derived date is read-only, so derive_from and default_value are mutually
|
|
25057
25057
|
// exclusive — enforced in validateFieldDefaultValue.
|
package/docs/mutations.md
CHANGED
|
@@ -397,8 +397,8 @@ Why a full-form snapshot save is a bug, not a style choice — three independent
|
|
|
397
397
|
moment *any* field is touched — there is no value-equality pass (deep equality over stored
|
|
398
398
|
shapes would be a silent lock bypass). A snapshot touches every field, so it turns every
|
|
399
399
|
save against a locked record into a total rejection even when nothing changed.
|
|
400
|
-
2. **A table's checks
|
|
401
|
-
|
|
400
|
+
2. **A table's checks run on the write.** They run on every write that touches the record —
|
|
401
|
+
a snapshot save runs them for fields the user never edited.
|
|
402
402
|
3. **Snapshots clobber concurrent edits.** An update writes exactly the fields present in the
|
|
403
403
|
payload; unsent fields keep their current value. A diff therefore coexists with a
|
|
404
404
|
colleague's concurrent edit to a *different* field — a snapshot overwrites it with the
|
package/docs/workflows.md
CHANGED
|
@@ -51,9 +51,7 @@ compile-time "cannot find name", not a runtime `undefined`.
|
|
|
51
51
|
|---|---|---|
|
|
52
52
|
| `trigger` | every context | the trigger namespace. For an app workflow: `trigger.app_workflow.inputs.<name>` |
|
|
53
53
|
| `runtime` | every context | execution context — see the key table below |
|
|
54
|
-
| `record` | button
|
|
55
|
-
| `prev_record` | table `*_update` / `*_delete` | the prior record state — same shape as `record` |
|
|
56
|
-
| `changes` | table `*_update` | per-field diff — a *partial* map, so read it `changes["fld_x"]?.next_value` / `?.prev_value` |
|
|
54
|
+
| `record` | button workflows | the record the button was pressed on |
|
|
57
55
|
| `index` | inside a `for-of` body | the current 0-based iteration index — always the **innermost** loop's. To use an outer loop's index in a nested body, bind it in the outer one (`const outerIdx = index;`) and read that |
|
|
58
56
|
| `<bind_name>` | inside a `for-of` body | the current item. Each loop keeps its own, so a nested body reads the outer loop's item by its own bind name |
|
|
59
57
|
| `<name>` | its block, after the declaration | a `const` — a step's output (`rows.records`, `approval.status`, …) or a bound value |
|
|
@@ -61,10 +59,6 @@ compile-time "cannot find name", not a runtime `undefined`.
|
|
|
61
59
|
| `<param>` | inside a helper callback | that lambda's parameter |
|
|
62
60
|
| `(await <tool>({…}))` | anywhere the call runs unconditionally | that call's output, read in place (see *`await` inside a statement*) |
|
|
63
61
|
|
|
64
|
-
`record`, `prev_record` and `changes` are the **canonical** roots. `trigger.data`,
|
|
65
|
-
`trigger.prev_data` and `trigger.changes` are accepted and lower onto them, so a saved body prints
|
|
66
|
-
back in the bare form. Write the bare form.
|
|
67
|
-
|
|
68
62
|
A body bound to an app alias is an app workflow: it sees `trigger` and `runtime` only. There is
|
|
69
63
|
no `record` — an app workflow is not attached to a table. Pass the record id in as a
|
|
70
64
|
`record_link` input and `get_record` it.
|
|
@@ -72,11 +66,9 @@ no `record` — an app workflow is not attached to a table. Pass the record id i
|
|
|
72
66
|
`runtime` keys: `timezone`, `now`, `workflow_id`, `execution_id`, `workspace_id`,
|
|
73
67
|
`organization_id`, `triggered_by_member_id` (`null` for a system trigger or an anonymous
|
|
74
68
|
public-app caller), and `change_origin` — a discriminated union over how the **execution** was
|
|
75
|
-
started: `button` · `automation` · `app_action` · `app_workflow
|
|
69
|
+
started: `button` · `automation` · `app_action` · `app_workflow`. It describes
|
|
76
70
|
the dispatch, never the person, and it is fixed per attachment — an app workflow always reads
|
|
77
|
-
`app_workflow
|
|
78
|
-
read `trigger.change_origin` on a table workflow (that one carries the originating write's
|
|
79
|
-
origin, and spans every write path: `member`, `chat_agent`, `app_workflow`, …). `runtime`
|
|
71
|
+
`app_workflow`. `runtime`
|
|
80
72
|
is never null, so `runtime?.x` is rejected outright. Prefer the helper `now()` over
|
|
81
73
|
`runtime.now`.
|
|
82
74
|
|
|
@@ -118,16 +110,13 @@ is a second call (`linked(linked(order.data["fld_a"])[0]["fld_b"])[0]`). A linke
|
|
|
118
110
|
deleted, or that the run may not read, stays a positional `null`, so `linked(P)[i]` is always the
|
|
119
111
|
row `P[i]` names.
|
|
120
112
|
|
|
121
|
-
The argument is a path ending on a link field, off anything record-shaped: `record` /
|
|
122
|
-
`prev_record`, a `for-of` item, a lambda parameter, a `let` binding, or a `get_record` /
|
|
113
|
+
The argument is a path ending on a link field, off anything record-shaped: `record`, a `for-of` item, a lambda parameter, a `let` binding, or a `get_record` /
|
|
123
114
|
`query_records` output. The server checks the far field at save only where it can name the table
|
|
124
115
|
before the run — a call with a **literal** `table_id` — and the fetch works either way. Guard **inside** the
|
|
125
116
|
argument — `linked(record?.["fld_link"])[0]` — never on the call: `linked(P)?.[i]` is rejected,
|
|
126
117
|
because the call always returns an array and the guard could never fire. Two more rejections:
|
|
127
118
|
`const ids = record["fld_link"]; linked(ids)`, since the field name comes from the argument's last
|
|
128
|
-
member access (`linked(row["fld_link"])` off a bound ROW is fine)
|
|
129
|
-
`linked(changes["fld_link"]?.next_value)`, since a diff is not a record — read
|
|
130
|
-
`linked(record["fld_link"])` or `linked(prev_record["fld_link"])`, which hold the same ids.
|
|
119
|
+
member access (`linked(row["fld_link"])` off a bound ROW is fine).
|
|
131
120
|
`linked` is grammar, not a helper: it cannot be shadowed, bound, called as a method, or awaited.
|
|
132
121
|
|
|
133
122
|
## Step forms
|
|
@@ -257,13 +246,12 @@ only the dialog. What it may name depends on who renders the refusal:
|
|
|
257
246
|
| Workflow | `field_key` may name |
|
|
258
247
|
|---|---|
|
|
259
248
|
| app workflow | a declared INPUT name (`order_code`, a key of the binding's `inputs`), or the `fld_` key of a field on a table the body names as a literal (a `table_id: "tbl_…"`, or a `const` holding one) |
|
|
260
|
-
|
|
|
249
|
+
| button | the `fld_` key of a field on the button's table |
|
|
261
250
|
| recordless automation (schedule, webhook, email) | anything — nothing renders it, so nothing checks it |
|
|
262
251
|
|
|
263
252
|
An app workflow whose alias declares no inputs leaves input-shaped keys unchecked, so a body can
|
|
264
253
|
be written before its declaration. A key outside its row is refused when the body is SAVED — by
|
|
265
|
-
`set_app_workflow`, by an agent binding one
|
|
266
|
-
`verify_only` call before that. `return({ field_errors })` keys are not checked.
|
|
254
|
+
`set_app_workflow`, by an agent binding one — and by a `verify_only` call before that. `return({ field_errors })` keys are not checked.
|
|
267
255
|
|
|
268
256
|
### `wait_for_approval` — the one wait worth binding
|
|
269
257
|
|
|
@@ -657,9 +645,8 @@ linked record id as `RecordId<"tbl_…">`. They come out branded from a record r
|
|
|
657
645
|
or from a workflow input declared `member` / `file` / `record_link` — a `rec_*` string carried in
|
|
658
646
|
a plain `text` input does *not* type-check into a link field, so declare the input for what it is.
|
|
659
647
|
|
|
660
|
-
A link **read** is an id array, so it feeds straight back: `set: { fld_link: record["fld_link"] }
|
|
661
|
-
|
|
662
|
-
`?.prev_value`. What does not feed back is a ROW: a link field takes ids, so from rows you already
|
|
648
|
+
A link **read** is an id array, so it feeds straight back: `set: { fld_link: record["fld_link"] }`.
|
|
649
|
+
What does not feed back is a ROW: a link field takes ids, so from rows you already
|
|
663
650
|
hold write `pluck(rows.records, (r) => r.id)`, or `[x.id]` for one. Never route a link read through
|
|
664
651
|
`linked(...)` to get its ids back — it holds them already, and the fetch costs one read per row and
|
|
665
652
|
yields `null` where a row is deleted or unreadable, which the write then rejects.
|
|
@@ -796,8 +783,7 @@ The rules that are easy to get wrong because the failing code looks correct.
|
|
|
796
783
|
`` `Status: ${record["fld_status"]}` `` prints "Done" while
|
|
797
784
|
`record["fld_status"] == "opt_done"` compares keys — both correct, and neither substitutes for
|
|
798
785
|
the other. It holds on every route a record arrives by — a `let`, a callback parameter, a row
|
|
799
|
-
from a variable `table_id
|
|
800
|
-
behaves exactly like the same field read off the record.
|
|
786
|
+
from a variable `table_id`.
|
|
801
787
|
- **Multi-select `==` quietly desugars.** On a multi-select field,
|
|
802
788
|
`record["fld_tags"] == "opt_urgent"` rewrites to `includes(…)` at save. Either spelling is
|
|
803
789
|
fine — but a `switch` on a multi-select discriminant is **rejected**, because its value is an
|
|
@@ -810,15 +796,6 @@ The rules that are easy to get wrong because the failing code looks correct.
|
|
|
810
796
|
`xs = concat(xs, someArray)` fails to typecheck; appending a whole *array* has no `push`
|
|
811
797
|
spelling. Start that binding from the first array instead of seeding it `[]`. Appending single
|
|
812
798
|
**items** (`xs.push(item)`) works on a bare `[]` seed.
|
|
813
|
-
- **A mutation on the trigger table of an `after_*` workflow can re-trigger itself** — a lint
|
|
814
|
-
warning, not an error. Gate it on `trigger.change_origin` — the originating write's origin —
|
|
815
|
-
and name every origin you mean to let through, rather than testing for one. `member` is a
|
|
816
|
-
person in the browser and **only** that: the same edit made over MCP, the CLI, or a direct API
|
|
817
|
-
call arrives as `api_client`, and one made by the chat agent as `chat_agent`. A gate meaning
|
|
818
|
-
"someone did this by hand" is
|
|
819
|
-
`includes(["member", "chat_agent", "api_client"], trigger.change_origin.type)`. Not
|
|
820
|
-
`runtime.change_origin`: that is the execution's own origin, always `table_workflow` here, so
|
|
821
|
-
it discriminates nothing.
|
|
822
799
|
- **Expressions are bounded.** A helper that builds an array — `range`, `list`, `flatten`,
|
|
823
800
|
`concat` of arrays, `split`'s parts — makes at most 100,000 elements, and a callback helper
|
|
824
801
|
takes an array of at most 100,000. `join`, `replaceAll`, `padStart`, `padEnd`, `toString`,
|
package/package.json
CHANGED