@lotics/app-sdk 0.66.0 → 0.67.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 +1 -1
- package/dist/src/hooks.js +28 -10
- package/docs/data_fetching.md +12 -8
- package/docs/members_and_options.md +4 -4
- package/docs/queries.md +15 -2
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -15,7 +15,7 @@ signature; open the file.**
|
|
|
15
15
|
| Doc | Read it for |
|
|
16
16
|
|---|---|
|
|
17
17
|
| [docs/queries.md](./docs/queries.md) | **The query engine authoring reference** — AST node kinds, per-field-type operator support, filters/params/pruning, free-text search, combining tables (join/union/link/`unnest`/`record_id`), shaping (aggregates, date buckets, windows), runtime refinement bounds, limits & the efficiency playbook. |
|
|
18
|
-
| [docs/data_fetching.md](./docs/data_fetching.md) | The three read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`), the `QueryRow` shape (projected columns `unknown`; `__source_record_id`/`__source_table_id` typed but optional), cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`), `useFieldOptions`, data discipline, the search-as-you-type + record-picker patterns. |
|
|
18
|
+
| [docs/data_fetching.md](./docs/data_fetching.md) | The three read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`), the `QueryRow` shape (projected columns `unknown`; `__source_record_id`/`__source_table_id` typed but optional), cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`), `useFieldOptions`, caching — **arrival revalidates** (a re-mount renders cache *and* refreshes it in the background, `loading` never flips) — data discipline, the search-as-you-type + record-picker patterns. |
|
|
19
19
|
| [docs/mutations.md](./docs/mutations.md) | `useWorkflow` (the ONLY write path), the `WorkflowResult` resolve-never-throw contract, typed inputs, diff-before-update, locked records, `useOptimistic`, `useNewRecord` (client-minted `rec_*` id so a new-record surface never remounts on its first save), read-after-write ordering (a re-read must not overtake an in-flight write). |
|
|
20
20
|
| [docs/workflows.md](./docs/workflows.md) | **The workflow-BODY authoring reference** — the JS subset a `src/workflows/<alias>.ts` body may use: the parse-at-save/never-execute model, opaque `fld_*`/`opt_*` keys, expression sources + link descent, every step form (tool call, `agent`, waits, `validate`, `return`), the accepted sugar and its canonical lowering, helpers + callback rules, record-write surfaces, the traps, the bright line, and the verify loop — `check` (the only local gate: the app's own `npm run typecheck` never sees a body) → `dry_run_workflow` (static green is not a run) → `set`. |
|
|
21
21
|
| [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload`, `useAttachments`, `readFiles`/presigned URLs (**a bearer credential for the bytes** — never logged, reported, or persisted), workflow-generated files, **naming a zip's entries** (`{ id, name }` per file — a file name, never a path), preview pairing, filter operators, the server-side delivery bounds. **Uploads declare a `fidelity`** (`standard` / `high` / `original`) — the app picks how much of the image survives storage; use `high` whenever text must stay legible. |
|
package/dist/src/hooks.js
CHANGED
|
@@ -31,19 +31,24 @@ export function useWorkflow(alias) {
|
|
|
31
31
|
// rows (no retry loop that masks the error), and honor the focus/reconnect
|
|
32
32
|
// opt-out.
|
|
33
33
|
//
|
|
34
|
-
// `revalidateIfStale
|
|
35
|
-
// -
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
34
|
+
// `revalidateIfStale` is LEFT AT SWR's DEFAULT (`true`): re-mounting a screen
|
|
35
|
+
// renders its cached rows AND re-reads them. Pinning it `false` saved a re-query
|
|
36
|
+
// per navigation and cost every screen a hand-rolled arrival refetch — where
|
|
37
|
+
// forgetting is SILENT (no error, no empty state, just last visit's data until a
|
|
38
|
+
// hard reload), which is not a mistake an author gets told about once and learns.
|
|
39
|
+
//
|
|
40
|
+
// The default costs one BACKGROUND request per re-mount of a warm key and
|
|
41
|
+
// nothing visible: SWR's `isLoading` is true only on an initial load with no
|
|
42
|
+
// cached data, so nothing blanks to a skeleton, and `dedupingInterval` collapses
|
|
43
|
+
// a burst of navigation into one fetch.
|
|
44
|
+
//
|
|
45
|
+
// `useAppContext` (viewer.ts) still pins it off — the app's identity is read
|
|
46
|
+
// once at boot and cannot change under the reader. `useInfiniteQuery` is the
|
|
47
|
+
// one hook this does not reach; see its own note.
|
|
42
48
|
function swrConfig(revalidateOnFocus) {
|
|
43
49
|
return {
|
|
44
50
|
revalidateOnFocus,
|
|
45
51
|
revalidateOnReconnect: revalidateOnFocus,
|
|
46
|
-
revalidateIfStale: false,
|
|
47
52
|
shouldRetryOnError: false,
|
|
48
53
|
};
|
|
49
54
|
}
|
|
@@ -152,7 +157,20 @@ export function useInfiniteQuery(alias, params, opts) {
|
|
|
152
157
|
sort,
|
|
153
158
|
filter,
|
|
154
159
|
});
|
|
155
|
-
},
|
|
160
|
+
},
|
|
161
|
+
// `revalidateFirstPage: false` — `loadMore()` fetches the next page and only
|
|
162
|
+
// that page. SWR's default re-fetches page 1 on every load-more, costing a
|
|
163
|
+
// request per scroll and reshuffling the top of the feed under a reader
|
|
164
|
+
// looking further down it (pinned by the keyset test).
|
|
165
|
+
//
|
|
166
|
+
// THE COST, stated because it is otherwise invisible: this hook does not
|
|
167
|
+
// arrival-revalidate the way the other two do. SWRInfinite decides per page
|
|
168
|
+
// and never consults `revalidateIfStale`, and neither lever that would change
|
|
169
|
+
// that is cheap — `revalidateFirstPage` pays per load-more, `revalidateOnMount`
|
|
170
|
+
// refetches EVERY loaded page. A cold key still fetches, focus/reconnect and
|
|
171
|
+
// `refetch()` still refresh; a feed that must be fresh on arrival calls
|
|
172
|
+
// `refetch()`.
|
|
173
|
+
{ revalidateFirstPage: false, ...swrConfig(revalidateOnFocus) });
|
|
156
174
|
// SWRInfinite leaves an in-flight page slot `undefined` until it resolves —
|
|
157
175
|
// operate on resolved pages only so a page being appended never crashes the
|
|
158
176
|
// flatten or skews the counts.
|
package/docs/data_fetching.md
CHANGED
|
@@ -67,14 +67,18 @@ server validates system conditions by `type` and never reads `field_key` on them
|
|
|
67
67
|
- **Cache identity** is the tuple `(alias, params, pageSize, sort, filter)` (+ the page index for
|
|
68
68
|
the paged hooks). Object *contents* are hashed, not references — passing a fresh inline
|
|
69
69
|
`{ status: "open" }` each render is the same key; you never need to memoize params.
|
|
70
|
-
- The cache **survives unmount/remount
|
|
71
|
-
and
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
- The cache **survives unmount/remount**, and **arrival is a refresh event**: returning to a screen
|
|
71
|
+
renders the cached rows instantly *and* revalidates them in the background, so a list reflects what
|
|
72
|
+
another screen changed while you were away. Identical concurrent reads dedupe to one request.
|
|
73
|
+
Freshness comes from four places — arrival, window focus / tab return / network reconnect
|
|
74
|
+
(`revalidateOnFocus`, default on), the host's post-chat-turn poke, and explicit `refetch()`.
|
|
75
|
+
**Still call `refetch()` after a write the user is watching for**: arrival covers navigation, not a
|
|
76
|
+
mutation made on the screen you are already standing on.
|
|
77
|
+
- **`useInfiniteQuery` does not arrival-revalidate** — the one exception. A warm feed re-mounted
|
|
78
|
+
refetches nothing; a cold key still fetches, and focus / reconnect / the chat poke still refresh.
|
|
79
|
+
Re-fetching page 1 on arrival would cost a request on every `loadMore()` and reshuffle the top of
|
|
80
|
+
the feed under a reader scrolling further down it, and re-fetching every loaded page grows without
|
|
81
|
+
bound. **A feed that must be fresh on arrival calls `refetch()`.**
|
|
78
82
|
- **`loading`** is `true` only on the *initial* load of a key — a request is in flight and there
|
|
79
83
|
are no rows yet. It stays `false` during background revalidation of a key that already has rows,
|
|
80
84
|
so consumers never blank loaded data to a spinner on refetch. A key *change* (new params, sort,
|
|
@@ -89,10 +89,10 @@ name**, not the field key. Each `FieldOptions`:
|
|
|
89
89
|
|
|
90
90
|
### Caching & freshness
|
|
91
91
|
|
|
92
|
-
Field config is slow-changing, so the hook
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
Field config is slow-changing, so the hook does **not** revalidate on window focus or reconnect — it
|
|
93
|
+
re-reads on arrival (a remount) like the query hooks, and on an explicit `refetch()`. The option set
|
|
94
|
+
is resolved live from field config at fetch time, so an option added, renamed, or recolored in the
|
|
95
|
+
table flows through on the next fetch, with no app redeploy. `opts.enabled` defers the fetch (e.g.
|
|
96
96
|
until an edit drawer opens). State: `{ fields, loading, isValidating, error, refetch }`.
|
|
97
97
|
|
|
98
98
|
### Coloring a stored value (the `byKey` idiom)
|
package/docs/queries.md
CHANGED
|
@@ -440,8 +440,8 @@ derived surfaces share one implementation.
|
|
|
440
440
|
| select_record_link | `select_record_link` | membership by linked-record **id**: `has_any_of`, `has_none_of`, `has_all_of`; text over the cached **display**: `contains`, `not_contains`, `starts_with`, `ends_with`; `is_empty`, `is_not_empty` | same — id-membership works at the runtime layer (converged `[{id}]` containment) | Sorting orders by raw JSON, not display text — project the display (link extraction / `display_output`) and sort that. `unnest` fans link ids (+ display). |
|
|
441
441
|
| files | `files` | `has_filename`, `has_mime_type` (substring), `has_file_count` (exact count), `is_empty`, `is_not_empty` | same | Presign-enriched at delivery (§1); `unnest` fans file ids. Only presence-counting aggregates. |
|
|
442
442
|
| formula | its declared output type (`number`/`text`/`boolean`/`date`/`datetime`; `json` until inferred) | filtered by the output type's operators; `#ERROR:` cells are guarded — they count as empty and never match value operators | same | Extracted as a real scalar, so numeric formulas feed `sum`/`avg`/sorts. A **datetime-output** formula matches day-level date filters (full-day expansion). |
|
|
443
|
-
| rollup |
|
|
444
|
-
| lookup | the looked-up field's type | operators of
|
|
443
|
+
| rollup | decided by the OPERATION, never by the aggregated field: `earliest`/`latest` → `date`/`datetime` per that field's format; **every** other op → `number` | number or date operators per output type; text operators route as text | same | Datetime-format rollups match day-level date filters. `date_range` counts DAYS between the extremes, so it is a number despite its date input. A presence count (`filled`/`empty`/`percent_*`) is a number whatever it counts — including over `files`. |
|
|
444
|
+
| lookup | the type of the VALUE the looked-up field yields — a formula's output type, a rollup's operation result, an autonumber's display string (`text`); never the word `formula`/`rollup` | operators of that type, evaluated with ANY-element semantics over the linked values (a row matches if *any* linked value matches) | array-typed lookups follow their column type's rules | **Projection returns the first element only.** Emptiness on a files-lookup checks the inner file arrays (a linked record with zero files counts empty). For all values across links: unnest the link column + join (§8). |
|
|
445
445
|
| autonumber | `text` | text operators — filter by the visible composed string (`"ORD-0042"`) | same | Sorts numerically when the stored value is a bare integer, lexicographically otherwise — so `"ORD-0042"` orders by its padding and an unprefixed `"124"` orders after `"99"`, not before it. |
|
|
446
446
|
| button | `json` | **not filterable** — buttons are not data | — | Project/ignore; only presence-counting aggregates. |
|
|
447
447
|
|
|
@@ -780,6 +780,19 @@ template as derived nodes, in this order: `filter` (narrow) → `sort` (order)
|
|
|
780
780
|
- **`count: true`** — returns `{ total }` only: a single-row COUNT over the *filtered* set,
|
|
781
781
|
ignoring sort/limit/offset. Drives "Page 1 of N".
|
|
782
782
|
|
|
783
|
+
**A terminal aggregate cannot be refined — plan the total accordingly.** Because refinement wraps
|
|
784
|
+
*around* the template over its **output** columns, a query that has already collapsed (a `group`
|
|
785
|
+
with an empty `by` and a `sum`, say) has no source column left for the filter to name, so a facet
|
|
786
|
+
the list narrows by 400s against the total beside it. `window` is not a way out either: the frame
|
|
787
|
+
aggregate is computed before the refinement wraps, so the number describes the unfiltered set.
|
|
788
|
+
Both alternatives are worse than they look — baking the facet into the aggregate stops it being a
|
|
789
|
+
runtime facet (a filter TREE is not a scalar param), and leaving the total unfiltered puts a count
|
|
790
|
+
that follows the facet next to a sum that does not. **Project the summed column plus the columns
|
|
791
|
+
the facet can name, fetch under the same runtime `filter`, and total in the browser** — correct and
|
|
792
|
+
facet-aware, but it ships every matching row, so it holds only where the scope is already small
|
|
793
|
+
(one member's own records, a single project). A workspace-wide total that must follow a runtime
|
|
794
|
+
facet has no server-side shape today.
|
|
795
|
+
|
|
783
796
|
The SDK hooks map onto this directly (`dist/src/hooks.d.ts` for exact signatures): `useQuery`
|
|
784
797
|
sends `limit: pageSize, offset: 0` (a cap, not pagination) plus `opts.sort`/`opts.filter`;
|
|
785
798
|
`useInfiniteQuery` pages by **keyset** (`keyset: true` + the prior `next_cursor`), so its scroll
|