@lotics/app-sdk 0.123.11 → 0.124.0
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/AGENTS.md +3 -3
- package/dist/{chunk-COWN73CO.js → chunk-BWMAO5IY.js} +33 -3
- package/dist/comments.d.ts +1 -1
- package/dist/index.d.ts +5 -6
- package/dist/index.js +2488 -1782
- package/dist/outside_link.d.ts +27 -0
- package/dist/router.js +1 -1
- package/dist/row.d.ts +9 -2
- package/dist/rpc.d.ts +17 -3
- package/dist/shared_types.d.ts +5 -4
- package/dist/viewer.d.ts +2 -1
- package/docs/ai.md +3 -29
- package/docs/data_fetching.md +1 -0
- package/docs/files.md +1 -1
- package/docs/members_and_options.md +12 -62
- package/docs/mutations.md +4 -3
- package/docs/recipes.md +4 -4
- package/docs/runtime.md +5 -1
- package/docs/security.md +0 -3
- package/docs/workflows.md +19 -46
- package/package.json +1 -1
- package/dist/activity.d.ts +0 -40
|
@@ -261,7 +261,7 @@ host did not say (a visitor from outside the organization, an older host). Both
|
|
|
261
261
|
context, read once as the app opens. Display only: the workflow body re-checks who may act
|
|
262
262
|
(`current_member_stands_in_for(...)`).
|
|
263
263
|
|
|
264
|
-
`openableApps` lists the workspace's apps the viewer can open, each by its `id` and its `name` — what a
|
|
264
|
+
`openableApps` lists the workspace's apps the viewer can open, each by its `id` and its `name`, and `table_id`, the table its register lists, where the host says — what a
|
|
265
265
|
way through to a sibling app (`openApp`, `peekApp`) is offered by. `null` where the host did not say (standalone, a
|
|
266
266
|
render, an older host, or a page that had not listed its apps when the app opened). Display only: the
|
|
267
267
|
app opened checks the viewer itself.
|
|
@@ -301,7 +301,7 @@ Opt in on the app — a JSON app states it in its model; a custom-code app sets
|
|
|
301
301
|
version:
|
|
302
302
|
|
|
303
303
|
```jsonc
|
|
304
|
-
//
|
|
304
|
+
// set_app
|
|
305
305
|
{ "app_id": "app_…", "capabilities": { "comments": true } }
|
|
306
306
|
```
|
|
307
307
|
|
|
@@ -378,65 +378,17 @@ table's id: read it off a row of the query that renders the badges (`rows[0]?.__
|
|
|
378
378
|
rather than pasting a `tbl_` id, which a copy of the app cannot carry. Pass `undefined` while no
|
|
379
379
|
row is on screen — no request is sent and `counts` is `{}`.
|
|
380
380
|
|
|
381
|
-
##
|
|
381
|
+
## Outside links: `useOutsideLinks`
|
|
382
382
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const { reverted, kept } = await undoRecordChange("job", recordId, entry.id);
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
- **Authority: the app's own read of the record.** The server reaches the record through the
|
|
396
|
-
entity's read of one row — the read the record screen draws it with — so a row that read does not
|
|
397
|
-
reach (the reader's scope, another workspace's) answers not found. Only the fields that read draws
|
|
398
|
-
appear: a field the app does not read never does, and an edit of one neither shows nor splits an
|
|
399
|
-
entry. A field taken out of the app while its table keeps it stays hidden; a field gone from its
|
|
400
|
-
table appears where the version serving when it was written read it, marked `removed`. Comments
|
|
401
|
-
appear only where the app declares `comments`.
|
|
402
|
-
- **Members of the app's organization only** — the trail names its people. For a public visitor,
|
|
403
|
-
a member of another organization, and standalone, `available` is `false` and nothing is asked.
|
|
404
|
-
- **One entry per save.** Edits by one actor on one record less than ten minutes apart read as one
|
|
405
|
-
entry: each field from its value before the first edit to its value after the last, a field edited
|
|
406
|
-
back to where it began dropped (and an entry left with nothing). A comment, another actor or a
|
|
407
|
-
longer pause starts another entry, and the save that made the record is an entry alone (`created`):
|
|
408
|
-
its maker's edits after it, however soon, are entries of their own. `at` is the entry's first
|
|
409
|
-
write, `until` its last.
|
|
410
|
-
- **An entry** (`ActivityEntry`) is `kind: "change"` with `changes`, or `kind: "comment"` with
|
|
411
|
-
`comment: { id, body, mentions }`. A change entry also says `undoable` — the viewer may put it
|
|
412
|
-
back — `undone: { by, at }` once someone has, and `undo: true` where it is itself the save that
|
|
413
|
-
put an earlier one back. `actor`: `{ kind, id, name, image? }` — `member` (a person),
|
|
414
|
-
`agent` (an AI acting for a member: the chat, an app's agent, an MCP client — named by that
|
|
415
|
-
member), `workflow` (a workflow on its own, named by the workflow) or `import` (a script over the
|
|
416
|
-
API or the CLI, named by the member whose key ran it). `name` is empty where nothing resolves it.
|
|
417
|
-
- **A change** (`ActivityChange`): `{ field_id, label, removed?, before, after }`. `field_id` is a
|
|
418
|
-
spec field's `id`; `label` the app's label today, else — a removed field — its label when written.
|
|
419
|
-
Values are drawn as the record's read draws them, so the cell readers apply: a select
|
|
420
|
-
`[{ key, label }]` (`readSelect`; each option's label as it was when written), a member field
|
|
421
|
-
`[{ id, name, image? }]`, a link `[{ id, display }]` (`readLinks`), files presigned (`readFiles`),
|
|
422
|
-
`null` for empty.
|
|
423
|
-
- **`undoRecordChange(entity, record_id, entry_id)`** puts one change entry back: each field it
|
|
424
|
-
changed that the app reads returns to what it held before the entry, where the record still holds
|
|
425
|
-
what the entry wrote. It answers `{ reverted, kept }`: the fields put back (`{ field_id, label }`),
|
|
426
|
-
and those left as they stand with a `reason` — `changed_since` (someone changed it since) or
|
|
427
|
-
`refused` (the table refused the write: a lock, a check). A field already holding what it held
|
|
428
|
-
before is in neither. The write is the viewer's own save: they hold `write_data` on the table as
|
|
429
|
-
the call arrives, on a row their own row rule covers, and the record's lock, the table's checks
|
|
430
|
-
and its write rules answer as for any save of theirs. `undoable` is `false`, and the call
|
|
431
|
-
refused, for the entry that made the record, an entry that is an undo, one already put back, a
|
|
432
|
-
viewer who may not write the record, and a JSON app that saves no record of the entity (an undo is
|
|
433
|
-
a save); a record in the trash is refused until it is restored.
|
|
434
|
-
The undo is an entry of its own; the answer arrives once the record's reads are read anew.
|
|
435
|
-
- **Pages**: newest first, about thirty entries each; `next` is where the trail goes on —
|
|
436
|
-
`undefined` at its end. A page may hold fewer entries and still say `next` where the trail it read
|
|
437
|
-
held little the reader may see.
|
|
438
|
-
- **Freshness:** read again when a push or this app's own write names the entity's reads, when a
|
|
439
|
-
comment is written or deleted through `useComments`, on focus, and on `refetch()`.
|
|
383
|
+
`useOutsideLinks(recordId)` (`dist/outside_link.d.ts`) → `{ links, loading, error, available, create, revoke }` — a
|
|
384
|
+
member's links to one record, for the record screen of an app whose model states an `Outside` (the person with no seat
|
|
385
|
+
who opens a link; see the model reference). `available` is true for a signed-in member of the app's organization; for
|
|
386
|
+
anyone else nothing is asked and `links` stays empty. Pass `null` where the record draws no Outside: nothing is
|
|
387
|
+
asked either, and `create` and `revoke` throw `OutsideLinkRecordError`. `links` holds the record's title and, per link, who it is for, when
|
|
388
|
+
it closes, whether it was opened and why a closed one closed; the secret is never in it. `create(outside)` and
|
|
389
|
+
`revoke(link)` ask the **host**, which draws its own dialog (who the link is for and for how long; a confirmation),
|
|
390
|
+
makes or withdraws the link and shows the address itself, so the frame never sees or sends a secret. `create` resolves
|
|
391
|
+
to the link made, or `null` when the member backed out; `revoke` to whether it did. Both read the list again.
|
|
440
392
|
|
|
441
393
|
## Where each hook works
|
|
442
394
|
|
|
@@ -449,5 +401,3 @@ const { reverted, kept } = await undoRecordChange("job", recordId, entry.id);
|
|
|
449
401
|
| `useViewer` | member id (view-as target) | `null` |
|
|
450
402
|
| `useWorkspaceTimezone` / `useWorkspaceCurrency` | ✓ | ✓ |
|
|
451
403
|
| `useComments` / `useCommentCounts` | ✓ when capability declared | ✗ `available: false` |
|
|
452
|
-
| `useRecordActivity` | ✓ the app's organization's members | ✗ `available: false`, nothing asked |
|
|
453
|
-
| `undoRecordChange` | ✓ a member holding the table's write, on a row they may write | ✗ rejects |
|
package/docs/mutations.md
CHANGED
|
@@ -441,7 +441,8 @@ Row-level query results carry a `__source_locked` addressing column automaticall
|
|
|
441
441
|
project/filter/sort/join but not grouping, where rows stop being records).
|
|
442
442
|
`readLocked(row)` — pass the **whole row**, not a cell — returns `true` for a locked record,
|
|
443
443
|
`false` when the flag is absent (`dist/row.d.ts`). Use it to render a locked state and to
|
|
444
|
-
switch the save path.
|
|
444
|
+
switch the save path. `readLockedBy(row)` names who can change it — its table's approvers, else
|
|
445
|
+
the organization's admins — for a member of the app's organization; `[]` for anyone else.
|
|
445
446
|
|
|
446
447
|
### What a write against a locked record does
|
|
447
448
|
|
|
@@ -459,8 +460,8 @@ return({ status: "success", message: "Updated." });
|
|
|
459
460
|
```
|
|
460
461
|
|
|
461
462
|
`update_records` has no bypass flag — a locked record's values change only through the
|
|
462
|
-
approval flow below (or an admin explicitly unlocking the record first; `unlock_records` is
|
|
463
|
-
admin-gated, so don't build an unlock→edit→relock workflow around the approval flow — the
|
|
463
|
+
approval flow below (or the table's owner or an admin explicitly unlocking the record first; `unlock_records` is
|
|
464
|
+
owner- or admin-gated — in an app's run, judged as the member who pressed, never the app's owner — so don't build an unlock→edit→relock workflow around the approval flow — the
|
|
464
465
|
lock exists to route changes through review).
|
|
465
466
|
|
|
466
467
|
### The change-request path: `request_locked_record_change`
|
package/docs/recipes.md
CHANGED
|
@@ -119,7 +119,7 @@ the wrong half is *plausible and wrong* rather than broken.
|
|
|
119
119
|
|
|
120
120
|
Two **separate** layers; set both.
|
|
121
121
|
|
|
122
|
-
**Launcher chrome** — `lotics run
|
|
122
|
+
**Launcher chrome** — `lotics run set_app '{"app_id":"…","body":{"icon":"truck","color":"blue"}}'`.
|
|
123
123
|
`icon` is any Lucide name (the full library, validated at write time — a wrong name is rejected, not
|
|
124
124
|
silently boxed). Don't guess the kebab spelling:
|
|
125
125
|
|
|
@@ -127,9 +127,9 @@ silently boxed). Don't guess the kebab spelling:
|
|
|
127
127
|
lotics run search_app_icons '{"query":"shipping"}' → ["truck","ship","package",…]
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
`
|
|
131
|
-
blue indigo violet purple fuchsia pink rose slate gray zinc neutral stone`. `null` clears
|
|
132
|
-
`
|
|
130
|
+
`color` is a named palette colour: `red orange amber yellow lime green emerald teal cyan sky
|
|
131
|
+
blue indigo violet purple fuchsia pink rose slate gray zinc neutral stone`. `null` in `body` clears
|
|
132
|
+
either. `set_app` sets both.
|
|
133
133
|
|
|
134
134
|
**In-app palette** — the app's own `src/theme.ts`, exact hex. This is what the app *renders* with;
|
|
135
135
|
the launcher colour does not feed it. Pick the named colour closest to your brand hex.
|
package/docs/runtime.md
CHANGED
|
@@ -219,7 +219,6 @@ surfaces:
|
|
|
219
219
|
|---|---|---|
|
|
220
220
|
| `query`, `field_options`, `workflow`, `members`, `context`, `upload`, `urlState.get/set`, `openExternal` | yes | yes |
|
|
221
221
|
| `comments.*` | yes | rejects — `"Comments are available only in embedded apps — a signed-in member is required."` |
|
|
222
|
-
| `activity.list`, `activity.undo` | yes, for a member of the app's organization; `activity.undo` for one who may write the record. A host that does not carry the op answers `"Unknown RPC op: activity.undo"` | rejects — `"A record's activity is available only in embedded apps — a signed-in member is required."` |
|
|
223
222
|
| `agentRun` (streaming, internal to `useAgentRun`) | yes | yes |
|
|
224
223
|
| `agentRun.get`, `agentRun.cancel` | yes | yes |
|
|
225
224
|
| `agentRuns` (session history) | **no** — the product host doesn't implement it either (`"Unknown RPC op: agentRuns"`) | rejects — a standalone caller is anonymous, and a history is its member's (`"App agent runs require an authenticated member …"`) |
|
|
@@ -275,6 +274,11 @@ semantics are documented:
|
|
|
275
274
|
| `urlState.get` / `urlState.set` | — / `{ params }` | `UrlParams` / `void` | [navigation & state](./navigation_and_state.md) |
|
|
276
275
|
| `comments.list/create/update/delete/counts` | comment ops | comment payloads | [members & options](./members_and_options.md) |
|
|
277
276
|
| `recording.start` / `recording.stop` | `{ alias, inputs }` / `{ alias }` | `{ started }` / `void` | [mutations](./mutations.md#recording-into-a-workflow-userecording) |
|
|
277
|
+
| `outsideLink.list/create/revoke` | `{ record_id }` / `{ record_id, outside }` / `{ record_id, link_id }` | links / link or `null` / `{ id }` or `null` | [members & options](./members_and_options.md#outside-links-useoutsidelinks) |
|
|
278
|
+
|
|
279
|
+
`installRpcTransport(transport)` answers every op itself, for a page that is never an app's (the page a person with no
|
|
280
|
+
seat opens from a link to one record); the last install wins and a page installs its own before it mounts. An app never
|
|
281
|
+
calls it.
|
|
278
282
|
|
|
279
283
|
The **streaming** agent-run op is *not* reachable through `rpc()` — its
|
|
280
284
|
response is a chunk stream, not a single value; it's internal to `useAgentRun`.
|
package/docs/security.md
CHANGED
|
@@ -10,7 +10,6 @@ Every data operation an app performs — queries, workflows, agent runs — exec
|
|
|
10
10
|
| Workflows (`useWorkflow`) | App owner | Yes — `runtime.triggered_by_member_id` in the workflow body (`null` for anonymous) |
|
|
11
11
|
| Agent runs (`useAgentRun`) | App owner | Yes — requires an authenticated member; runs are private to that member |
|
|
12
12
|
| Comments (`useComments`) | App authority for **access**; the **author** is always the real member | Always — members-only, anonymous callers are rejected |
|
|
13
|
-
| Record activity (`useRecordActivity`) | The app's own read of the record — the entity's read of one row, and only the fields it draws | Always — the app's organization's members only |
|
|
14
13
|
|
|
15
14
|
Consequences of owner authority:
|
|
16
15
|
|
|
@@ -102,8 +101,6 @@ A publicly-shared app (its own origin, or its public link) is reachable by **any
|
|
|
102
101
|
| Query/workflow file outputs | Yes — file cells and workflow-produced files return direct presigned URLs (24-hour TTL) that anonymous viewers can fetch; see [files](./files.md) |
|
|
103
102
|
| Agent runs (`useAgentRun`) | **No** — rejected: agent runs require an authenticated member, and each member's run history is private to them (a guessed session id cannot read another member's thread) |
|
|
104
103
|
| Comments (`useComments`) | **No** — members-only |
|
|
105
|
-
| Record activity (`useRecordActivity`) | **No** — the app's organization's members only |
|
|
106
|
-
| Undo of a record's change (`undoRecordChange`) | **No** — a member of the app's organization holding `write_data` on the record's table, on a row their own row rule covers, in an app that saves records of that entity |
|
|
107
104
|
| Member roster (`useMembers`) | **No** — same-org members only (below) |
|
|
108
105
|
|
|
109
106
|
**Warning — `is_current_member` on a public app resolves to the app owner.** An anonymous request has no viewer to bind, so both `is_current_member` and `current_member in_any_group` fall back to the owner's identity and the owner's groups. A "my items" query shows every anonymous visitor the *owner's* items, and a manager-only read template gated on a group the owner belongs to **passes for every anonymous visitor**. Never rely on viewer-bound predicates in a public app.
|
package/docs/workflows.md
CHANGED
|
@@ -34,8 +34,7 @@ if (order.data["fld_status"] == "opt_open") { … }
|
|
|
34
34
|
A body is the app's, stored on the server and bound to an alias: `set_app_workflow` takes the
|
|
35
35
|
alias, the body's `source`, and the alias's `inputs` (leave `outputs` off to let the body's
|
|
36
36
|
`return({ data })` derive it). The server verifies the body and the write lands live, as a new
|
|
37
|
-
version of the app; `get_app_workflow` reads it back, and
|
|
38
|
-
version before it. Input declaration vocabulary and validation:
|
|
37
|
+
version of the app; `get_app_workflow` reads it back, and an earlier body is restored by writing it again. Input declaration vocabulary and validation:
|
|
39
38
|
[mutations](./mutations.md#declaring-workflow-inputs).
|
|
40
39
|
|
|
41
40
|
An app workflow body carries **no trigger declaration** — the binding supplies the trigger
|
|
@@ -138,7 +137,7 @@ The body is a sequence of statements, each of which maps 1:1 onto a stored step.
|
|
|
138
137
|
| Pause | `await wait({ duration_in_minutes: 5 });` |
|
|
139
138
|
| Pause for an event | `await wait_for_event({ event_type: "payment" \| "webhook", event_ref, timeout_in_minutes? });` |
|
|
140
139
|
| Pause for a decision | `const a = await wait_for_approval({ approvers, prompt, timeout_in_minutes? });` |
|
|
141
|
-
| LLM reasoning | `const x = await agent({ instructions, input,
|
|
140
|
+
| LLM reasoning | `const x = await agent({ instructions, input, model?, output });` |
|
|
142
141
|
| Run a declared agent | `const x = await app_agent({ alias, input, wait? });` |
|
|
143
142
|
| Guard | `validate({ checks: [{ fail_when, field_key?, message }, …] });` |
|
|
144
143
|
| End the run | `return({ status, message, field_errors?, data? });` |
|
|
@@ -283,7 +282,6 @@ const order = await get_record({ table_id: "tbl_orders", record_id: i.order_id }
|
|
|
283
282
|
const summary = await agent({
|
|
284
283
|
instructions: "Summarize the order in one sentence for an internal digest.",
|
|
285
284
|
input: { code: order.data["fld_order_code"], total: order.data["fld_total"] },
|
|
286
|
-
tools: [],
|
|
287
285
|
output: { mode: "text" },
|
|
288
286
|
});
|
|
289
287
|
```
|
|
@@ -293,13 +291,14 @@ name" when broken:
|
|
|
293
291
|
|
|
294
292
|
- **Only `input` sees the workflow.** It must be an **object literal**; its field values are
|
|
295
293
|
ordinary expressions and are the *only* workflow data the agent receives. `instructions` is a
|
|
296
|
-
static string literal and
|
|
297
|
-
|
|
294
|
+
static string literal and may not read a binding, so do not interpolate a record into
|
|
295
|
+
`instructions`; move it into `input`. An agent step calls no tool and states no `tools`: the
|
|
296
|
+
workflow's own steps write, send and notify with what it returns.
|
|
298
297
|
- **Only the bound name comes back.** In `{ mode: "text" }` the result *is* the string — use it
|
|
299
298
|
directly. In `{ mode: "object", schema }` the `schema` is itself a top-level object declaration
|
|
300
299
|
(`{ type: "object", fields: { total: { type: "number" }, … } }`, the same vocabulary as an
|
|
301
300
|
alias's `outputs`); read the declared fields as `x.field`, and a name not in `schema` does not
|
|
302
|
-
exist. The agent's
|
|
301
|
+
exist. The agent's reasoning is not readable.
|
|
303
302
|
|
|
304
303
|
`model` is an optional pin — omit it to follow the platform default. The step is atomic (no wait
|
|
305
304
|
or approval inside it).
|
|
@@ -494,12 +493,12 @@ full menu by category, so a miss is one informed retry.
|
|
|
494
493
|
|
|
495
494
|
| Category | Helpers |
|
|
496
495
|
|---|---|
|
|
497
|
-
| **Type / null** | `isNull`, `isNotNull`, `isEmpty`, `isString`, `isNumber`, `isBoolean`, `isArray`, `isObject`, `coalesce`, `toNumber`, `toString`, `typeOf`, `parseJson`, `toJson` |
|
|
496
|
+
| **Type / null** | `isNull`, `isNotNull`, `isEmpty`, `isString`, `isNumber`, `isBoolean`, `isArray`, `isObject`, `coalesce`, `toNumber`, `toFigure`, `toString`, `typeOf`, `parseJson`, `toJson` |
|
|
498
497
|
| **Array** | `size`, `first`, `requireFirst`, `last`, `nth`, `at`, `slice`, `includes`, `filter`, `find`, `some`, `every`, `pluck`, `sortBy`, `groupBy`, `countBy`, `unique`, `uniqueBy`, `compact`, `flatten`, `reverse`, `concat`, `difference`, `differenceBy`, `intersection`, `intersectionBy`, `list`, `range`, `reduce` |
|
|
499
498
|
| **Number** | `sum`, `sumBy`, `mean`, `meanBy`, `min`, `max`, `minBy`, `maxBy`, `round`, `ceil`, `floor`, `abs`, `mod`, `pow`, `sqrt`, `clamp`, `percentage` |
|
|
500
|
-
| **String** | `upper`, `lower`, `capitalize`, `trim`, `contains`, `startsWith`, `endsWith`, `replace`, `replaceAll`, `substring`, `length`, `split`, `join`, `padStart`, `padEnd`, `formatNumber(value, decimals)` (fixed-decimal, ungrouped), `formatDecimal(value, decimals, locale)` (grouped for a reader — `formatDecimal(151000, 0, "vi-VN")` → `151.000`), `numberToWords(x, lang?)` (`"vi"` or `"en"`) |
|
|
499
|
+
| **String** | `upper`, `lower`, `capitalize`, `trim`, `contains`, `startsWith`, `endsWith`, `replace`, `replaceAll`, `substring`, `length`, `split`, `join`, `padStart`, `padEnd`, `uuidFrom(text)` (the same UUID for the same text), `formatNumber(value, decimals)` (fixed-decimal, ungrouped), `formatDecimal(value, decimals, locale)` (grouped for a reader — `formatDecimal(151000, 0, "vi-VN")` → `151.000`), `numberToWords(x, lang?)` (`"vi"` or `"en"`) |
|
|
501
500
|
| **Object** | `keys`, `values`, `entries`, `get`, `pick`, `omit`, `merge`, `nonNullKeys` |
|
|
502
|
-
| **Date** | `now`, `formatDate`, `parseDate`, `addDays`, `subDays`, `addHours`, `subHours`, `addMinutes`, `subMinutes`, `startOfDay`, `endOfDay`, `differenceInCalendarDays`, `differenceInHours`, `differenceInMinutes`, `isBefore`, `isAfter`, `isSameDay`, `isToday`, `isWithinRange` |
|
|
501
|
+
| **Date** | `now`, `formatDate`, `parseDate`, `addDays`, `subDays`, `addHours`, `subHours`, `addMinutes`, `subMinutes`, `startOfDay`, `endOfDay`, `differenceInCalendarDays`, `differenceInHours`, `differenceInMinutes`, `isBefore`, `isAfter`, `isSameDay`, `isToday`, `isWithinRange`, `meetsBounds` |
|
|
503
502
|
| **Other** | `formatCurrency(amount, locale, currency)`, `randomNumber(len)`, `randomAlphaNumeric(len)`, `sample(items)`, `current_member_in_any_group(["grp_…"])`, `current_member_stands_in_for([memberId])`, `current_member_stands_in_for_any_group(["grp_…"])` |
|
|
504
503
|
|
|
505
504
|
**Emptiness on a record cell.** `isNull` is the strict check — true only for `null`/`undefined`.
|
|
@@ -752,7 +751,7 @@ existed only to feed it, which is usually where the round trips were hiding.
|
|
|
752
751
|
|
|
753
752
|
### Measuring, if you do
|
|
754
753
|
|
|
755
|
-
`query_workflow_executions`
|
|
754
|
+
`query_workflow_executions` and `get_workflow_execution` give `started_at` / `completed_at` per run, the server's own
|
|
756
755
|
duration. **Run-to-run variance is large** (a second, minutes apart), so compare medians of several
|
|
757
756
|
interleaved runs, never one before/after pair.
|
|
758
757
|
|
|
@@ -849,41 +848,15 @@ fix-stating messages before you see them.
|
|
|
849
848
|
|
|
850
849
|
`check` and `set` prove parse, types, name resolution, structure, and lint — **none of them
|
|
851
850
|
evaluates an expression**. A body that saves clean can still take the wrong branch, hand a tool a filter it
|
|
852
|
-
rejects, or read a path that is null on real data. The rehearsal for that is `
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
**Add `live_reads: true` whenever the body READS.** By default the read-only tools
|
|
863
|
-
(`query_records`, `get_record`, `aggregate_records`) return stubs — an empty result set, a blank
|
|
864
|
-
record — so any branch gated on stored data takes the empty path. With `live_reads: true` those
|
|
865
|
-
three dispatch against the real workspace under YOUR authority while writes stay recorded-only. (A
|
|
866
|
-
table-triggered body gets this from `record_id` + `table_id`; an app workflow has no trigger
|
|
867
|
-
record, so it asks directly.) Do this before the first live run of anything that writes.
|
|
868
|
-
|
|
869
|
-
**`return_value` is what the CALLER receives**, not a summary of it: `{ status, message }` always,
|
|
870
|
-
plus `data` when the body returns one and `field_errors` when it returns those — the same map a
|
|
871
|
-
form reads. A `validate` step that fails produces the same shape the live run does: every failing
|
|
872
|
-
check's message joined with `"; "`, and one `field_errors` entry per failing check that names a
|
|
873
|
-
`field_key`.
|
|
874
|
-
|
|
875
|
-
**A rehearsal can stop early, and it says so in `evaluation_errors`.** Four bounds apply — 1 000
|
|
876
|
-
iterations of a `while` / `do_while` / `c_for`, 10 000 steps, 50 live reads, and 20 seconds — and
|
|
877
|
-
tripping any of them appends an entry naming the bound and ending "the plan below is incomplete",
|
|
878
|
-
then stops. It also stops, with an entry saying why, where the live run would fail: at a
|
|
879
|
-
`validate` check that throws, an evaluation past the run's compute or memory bounds (*Traps*), or a
|
|
880
|
-
return too long to write.
|
|
881
|
-
So read `evaluation_errors` before reading `planned_calls`: an entry there may be a
|
|
882
|
-
truncated plan rather than a bug in your body. A `foreach` is deliberately NOT capped at 1 000 —
|
|
883
|
-
its length is known before the first iteration, so the rehearsal walks every item and instead
|
|
884
|
-
refuses, up front, exactly the lists a live run refuses (over 10 000 items). The 50-live-read bound
|
|
885
|
-
is the one `live_reads: true` makes reachable: a loop body that reads issues one real query per
|
|
886
|
-
iteration.
|
|
851
|
+
rejects, or read a path that is null on real data. The rehearsal for that is `set_app_workflow` with
|
|
852
|
+
`verify_only: true` and a `record_id` (the row an Act would run on) or `preview_inputs` (the values of a
|
|
853
|
+
workflow that takes inputs and no row): the draft is walked by the engine an Act's preview uses, under the
|
|
854
|
+
app's authority, with the production expression evaluator. Reads are real; writes and service sends are
|
|
855
|
+
said, not made, and a document a step renders is made as a paper no listing shows. It answers `preview`
|
|
856
|
+
(the calls the run would make, each with its fully-resolved input, and the reads it made) and `returned`
|
|
857
|
+
(`{ status, message }` always, plus `data` and `field_errors` when the body returns them — what the caller
|
|
858
|
+
receives). A draft it cannot rehearse — a step that reads what a call before it answers, a wait, a bound the
|
|
859
|
+
walk reaches — answers `preview.available: false` and the reason. Nothing is written.
|
|
887
860
|
|
|
888
861
|
## A workflow that receives a recording
|
|
889
862
|
|
package/package.json
CHANGED
package/dist/activity.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { ActivityEntry, ActivityUndoResult } from "./shared_types.js";
|
|
2
|
-
export interface RecordActivityState {
|
|
3
|
-
/** Newest first. */
|
|
4
|
-
entries: ActivityEntry[];
|
|
5
|
-
/** Where the trail goes on — pass it as `after` to read the page past this one; `undefined` at its end. */
|
|
6
|
-
next: string | undefined;
|
|
7
|
-
loading: boolean;
|
|
8
|
-
error: string | null;
|
|
9
|
-
/** A signed-in member of the app's organization; for anyone else nothing is read and `entries` stays empty. */
|
|
10
|
-
available: boolean;
|
|
11
|
-
refetch: () => void;
|
|
12
|
-
}
|
|
13
|
-
export interface RecordActivityOptions {
|
|
14
|
-
/** A page's `next`: read the page past it. Absent, the newest page. */
|
|
15
|
-
after?: string;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* One page of the record's activity. `entity` is the alias the app's spec names the record's entity under; `record_id`
|
|
19
|
-
* a real id. Read again when a push or this app's own write names the entity's reads, and when a comment is written.
|
|
20
|
-
*
|
|
21
|
-
* ```tsx
|
|
22
|
-
* const { entries, next } = useRecordActivity("job", recordId);
|
|
23
|
-
* // The page past it, mounted where the reader asks for more:
|
|
24
|
-
* const older = useRecordActivity("job", recordId, { after: next });
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
export declare function useRecordActivity(entity: string, record_id: string, options?: RecordActivityOptions): RecordActivityState;
|
|
28
|
-
/**
|
|
29
|
-
* Put one change entry of the record's activity back, as the viewer's own save: each field the entry changed returns
|
|
30
|
-
* to what it held before it where the record still holds what the entry wrote. The answer names the fields put back
|
|
31
|
-
* (`reverted`) and those left as they stand (`kept`), each with its `reason` — `changed_since`, or `refused` where the
|
|
32
|
-
* table refused the write (a lock, a check) — and arrives once the record's reads are read anew. Refused for an entry
|
|
33
|
-
* whose `undoable` is false: the record's making, an undo, an entry already put back, and a viewer who may not write
|
|
34
|
-
* the record.
|
|
35
|
-
*
|
|
36
|
-
* ```tsx
|
|
37
|
-
* const { reverted, kept } = await undoRecordChange("job", recordId, entry.id);
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
export declare function undoRecordChange(entity: string, record_id: string, entry_id: string): Promise<ActivityUndoResult>;
|