@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 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. Replaces the per-table after_create workflow pattern."
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 and `after_update` workflows run on the write.** They run on every
401
- write that touches the record — a snapshot save runs them for fields the user never edited.
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 + table-lifecycle workflows | the record the trigger fired on (merged data on update) |
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` · `table_workflow`. It describes
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`, a table workflow always `table_workflow`. To branch on *who wrote the record*,
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), and
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
- | table-triggered (lifecycle, button) | the `fld_` key of a field on the trigger table |
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, by a table workflow's save — and by a
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
- and the same for `prev_record["fld_link"]` and for a diff's `changes["fld_link"]?.next_value` /
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` — and a change-diff read (`changes["fld_status"]?.next_value`)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.120.1",
3
+ "version": "0.120.2",
4
4
  "description": "The SDK a Lotics custom-code app reads and writes through — typed hooks over the host bridge, cell readers, mount() and AppRouter",
5
5
  "type": "module",
6
6
  "exports": {