@lotics/app-sdk 0.111.3 → 0.111.4
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/comments.d.ts +1 -1
- package/dist/index.js +3 -3
- package/docs/ai.md +2 -2
- package/docs/data_fetching.md +6 -6
- package/docs/files.md +2 -2
- package/docs/members_and_options.md +4 -3
- package/docs/queries.md +2 -2
- package/package.json +1 -1
package/dist/comments.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -15290,13 +15290,13 @@ var fileUploadResponseSchema = zod_default.object({
|
|
|
15290
15290
|
var fileFieldValueSchema = zod_default.object({
|
|
15291
15291
|
id: zod_default.string().describe("Unique identifier for the file"),
|
|
15292
15292
|
/**
|
|
15293
|
-
* Present on a
|
|
15293
|
+
* Present on a cell drawn from its `files` row and on its way off responses.
|
|
15294
15294
|
*
|
|
15295
15295
|
* Optional here because this schema types both, and a response is the half
|
|
15296
15296
|
* that loses it — no client reads it, and one that required it would reject
|
|
15297
15297
|
* the first response without it. Nothing weakens at write time: a file cell
|
|
15298
|
-
*
|
|
15299
|
-
*
|
|
15298
|
+
* stores ids alone and `toWireFileCell` mints this from the `files` row, so a
|
|
15299
|
+
* caller never supplies it.
|
|
15300
15300
|
* Code that derives a serving URL guards for it (`file_url_resolver.ts`).
|
|
15301
15301
|
*/
|
|
15302
15302
|
file_storage_key: zod_default.string().optional().describe("Storage key for the file"),
|
package/docs/ai.md
CHANGED
|
@@ -272,8 +272,8 @@ import { useAiContext } from "@lotics/app-sdk";
|
|
|
272
272
|
// A list screen publishes what it rendered:
|
|
273
273
|
useAiContext("orders_list", {
|
|
274
274
|
description: `Viewing ${rows.length} orders filtered to status=open, sorted by due date.`,
|
|
275
|
-
// Guarded, not `map`: the addressing columns are
|
|
276
|
-
// rows, so an unchecked ref carries `
|
|
275
|
+
// Guarded, not `map`: the addressing columns are `null` on a GROUPED query's
|
|
276
|
+
// rows, so an unchecked ref carries `null` and points the agent at nothing.
|
|
277
277
|
records: rows.flatMap((r) =>
|
|
278
278
|
r.__source_table_id && r.__source_record_id
|
|
279
279
|
? [{ table_id: r.__source_table_id, record_id: r.__source_record_id }]
|
package/docs/data_fetching.md
CHANGED
|
@@ -24,7 +24,7 @@ untyped. It is written from the app's live bindings by `lotics app create --cust
|
|
|
24
24
|
| `useQuery(alias, params?, { rows: false, total: true })` | `total` only, no rows (`total: { by: column }` or `total: { where: { name: filter } }` adds `counts`) | a facet chip, a queue badge, an "N awaiting approval" tile — the size of a set you are not listing |
|
|
25
25
|
| `useQueries(calls, opts?)` | one state per `{ alias, params?, filter?, sort?, aggregate?, total?, rows? }`, in order — a `sort` orders that call's rows before the cap cuts them | reads that are DATA — a list known only at render, one read per item, or one count per stage (`rows: false, total: true`, each state's `.total`); the deploy's alias scan reads a list as dynamic, so every alias in it is still declared. With `aggregate` a call answers the groups of its filtered rows instead of the rows ([queries](./queries.md)) |
|
|
26
26
|
| `queryAll(alias, params?, { filter, sort })` | a promise of every row | outside React, for a job that must hold the whole narrowed set in the browser (the ids an act runs on): it asks page after page until the server says the set ended, never stopping at the 10,000-row cap. The server's rows as stored — no write of this app is drawn over them |
|
|
27
|
-
| `exportQuery(alias, { params, filter, sort, tabs?, report, file })` | a promise of the file | every row the narrowed set holds, made into one file by the server — no row reaches the browser. `tabs` (2 to 5, each `{ tab, label, filter?, sort? }`) are each read as the export's own rows are — same query and params, under the tab's own filter and sort (absent, the declared order) — and a template reads each one's rows under its `tab`, a name none of the report's keys nor the template's `rows` key holds. `report` is `{ title, lines, readings, dates?, period?, per?, filename? }` — `dates` each date filter's `{ from?, to? }` by field alias, each bound a day (`yyyy-MM-dd`) or a minute of one (`yyyy-MM-ddTHH:mm`) with no zone; `period` the tabs' `{ from, to }`, bounds of the same form; `per` the value the report was picked for, in the reader's words; `filename` the file's name in the template grammar over one value each — `title`, `at` (the moment the server made the file), `dates.<field>.from` or `.to`, `period.from` or `.to`, `per`, `lines | lookup:<n>` — at most 200 bytes once filled (absent, the template's name, or `title`); `file` is `{ kind: "workbook", language, sheet, columns }` (the default report workbook, its head closing with the moment the server made it, each column `{ key, header, type }` an output column of the query — a `number` one on a number column, a `date` one on a date or datetime; with `tabs`, one sheet per tab named by its `label` in place of `sheet`) or `{ kind: "template", template }`, one the query declares under `templates` by name ([queries](./queries.md)). Past 20,000 rows, in any tab, it rejects, never cuts; each caller makes at most 60 exports a minute. The file's `url` is signed for the caller: open it with `openExternal` |
|
|
27
|
+
| `exportQuery(alias, { params, filter, sort, tabs?, report, file })` | a promise of the file | every row the narrowed set holds, made into one file by the server — no row reaches the browser. `tabs` (2 to 5, each `{ tab, label, filter?, sort? }`) are each read as the export's own rows are — same query and params, under the tab's own filter and sort (absent, the declared order) — and a template reads each one's rows under its `tab`, a name none of the report's keys nor the template's `rows` key holds. `report` is `{ title, lines, readings, dates?, period?, per?, filename? }` — `dates` each date filter's `{ from?, to? }` by field alias, each bound a day (`yyyy-MM-dd`) or a minute of one (`yyyy-MM-ddTHH:mm`) with no zone; `period` the tabs' `{ from, to }`, bounds of the same form; `per` the value the report was picked for, in the reader's words; `filename` the file's name in the template grammar over one value each — `title`, `at` (the moment the server made the file), `dates.<field>.from` or `.to`, `period.from` or `.to`, `per`, `lines | lookup:<n>` — at most 200 bytes once filled (absent, the template's name, or `title`); `file` is `{ kind: "workbook", language, sheet, columns }` (the default report workbook, its head closing with the moment the server made it, each column `{ key, header, type, labels? }` an output column of the query — a `number` one on a number column, a `date` one on a date or datetime; `labels` the words each bare option key of a `select` column no field names prints as, refused on any other column and for a key it lacks; with `tabs`, one sheet per tab named by its `label` in place of `sheet`) or `{ kind: "template", template }`, one the query declares under `templates` by name ([queries](./queries.md)). Past 20,000 rows, in any tab, it rejects, never cuts; each caller makes at most 60 exports a minute. The file's `url` is signed for the caller: open it with `openExternal` |
|
|
28
28
|
|
|
29
29
|
Every hook answers the same `QueryState`: `rows`, `truncated`, `total`, `counts`, `loading`,
|
|
30
30
|
`isValidating`, `error`, `pending`, `refetch`, `page`, `setPage`, `pageCount`, `hasMore`,
|
|
@@ -38,7 +38,7 @@ pagination; a `page` read whose pages you concatenate yourself is `more` done by
|
|
|
38
38
|
### One count per filtered set
|
|
39
39
|
|
|
40
40
|
`total` is a COUNT over the filtered set — `sort`, `limit`, `page` and `more` do not change it
|
|
41
|
-
(per value of `by`, when given). Two reads counting the same `(alias, params, filter, by)` share
|
|
41
|
+
(per value of `by`, or per name of `where`, when given). Two reads counting the same `(alias, params, filter, by, where)` share
|
|
42
42
|
**one** request, and page clicks and re-sorts reuse it. `page` counts by default:
|
|
43
43
|
|
|
44
44
|
```tsx
|
|
@@ -99,7 +99,7 @@ all its calls, and each call states its own `filter`, `sort`, `total` and `rows`
|
|
|
99
99
|
| `page` | `number` | — | Numbered pages of `page` rows: `page` (from 0), `setPage`, `pageCount`, `hasMore`, and `total`, counted by default. |
|
|
100
100
|
| `pageAt` | `{ at, onChange }` | — | With `page`, the page the screen keeps (its address, so a way back or a reload opens on it): `at` is read, `setPage` calls `onChange`, and nothing resets it — send a changed read back to 0 yourself. |
|
|
101
101
|
| `more` | `number` | — | A keyset feed read `more` rows at a time: `loadMore`, `hasMore`, `loadingMore`. |
|
|
102
|
-
| `total` | `boolean \| { by: column }` | `true` with `page`, else `false` | The whole set's count, its own request (`total`); with `by`, a count per value of that column from the same scan (`counts`). |
|
|
102
|
+
| `total` | `boolean \| { by: column } \| { where: { name: filter } }` | `true` with `page`, else `false` | The whole set's count, its own request (`total`); with `by`, a count per value of that column, or with `where`, a count per named filter within `filter`, from the same scan (`counts`). |
|
|
103
103
|
| `rows` | `boolean` | `true` | `false` reads no rows — a total alone. |
|
|
104
104
|
|
|
105
105
|
Runtime `sort`/`filter` are **bounded to the named query's output columns**, twice over. At
|
|
@@ -139,7 +139,7 @@ write it as `{ node_type: "condition", type: "record_id", operator, value }`.
|
|
|
139
139
|
|
|
140
140
|
- **A read's key** is what it asks: rows by `(alias, params, filter, sort, limit)` (+ the page
|
|
141
141
|
size and index with `page`), a feed by `(alias, params, filter, sort, more)`, a count by
|
|
142
|
-
`(alias, params, filter, by)`. Object *contents* make the key, not references — passing a fresh
|
|
142
|
+
`(alias, params, filter, by, where)`. Object *contents* make the key, not references — passing a fresh
|
|
143
143
|
inline `{ status: "open" }` each render is the same key; you never need to memoize params.
|
|
144
144
|
- **The cache survives unmount/remount**, and **arrival is a refresh event**: returning to a screen
|
|
145
145
|
renders the cached rows instantly *and* re-reads them in the background, so a list reflects what
|
|
@@ -369,14 +369,14 @@ Wire shapes per output column type:
|
|
|
369
369
|
Row-level (non-grouped) queries additionally carry system columns: `__source_record_id` /
|
|
370
370
|
`__source_table_id` / `__source_locked` (source addressing — pass `__source_record_id` to
|
|
371
371
|
workflows), `__created_at` / `__updated_at` (record timestamps), and per-projection `__src_field_*`
|
|
372
|
-
metadata. A grouped query collapses rows and
|
|
372
|
+
metadata. A grouped query collapses rows and answers each of these as `null`. Details:
|
|
373
373
|
[./queries.md](./queries.md).
|
|
374
374
|
|
|
375
375
|
A row is typed from the alias's own projection (`AppQueryColumns` in `.lotics/app_queries.d.ts`),
|
|
376
376
|
so a misspelt column is a `tsc` error rather than a blank cell — the server answers an unprojected
|
|
377
377
|
column with `undefined`, which every reader draws as its empty value. The values stay `unknown`
|
|
378
378
|
(the readers are the narrowing), and `__source_record_id` / `__source_table_id` are
|
|
379
|
-
`string | undefined
|
|
379
|
+
`string | null | undefined` — `null` on a grouped row — narrowed before use. Write the row type down as `RowOf<"alias">`; an alias
|
|
380
380
|
whose columns cannot be read off its AST keeps the open `QueryRow`, where any column compiles and
|
|
381
381
|
the server's answer is the only check — the rule the filter keys follow too.
|
|
382
382
|
|
package/docs/files.md
CHANGED
|
@@ -310,8 +310,8 @@ with `files` absent.
|
|
|
310
310
|
|
|
311
311
|
```tsx
|
|
312
312
|
const generate = useWorkflow("generateInvoice");
|
|
313
|
-
// `__source_record_id` is
|
|
314
|
-
//
|
|
313
|
+
// `__source_record_id` is `null` on a grouped row — so narrow it rather than
|
|
314
|
+
// handing the workflow no id where it declares a record.
|
|
315
315
|
if (!row.__source_record_id) return;
|
|
316
316
|
const result = await generate({ record_id: row.__source_record_id });
|
|
317
317
|
if (result.status === "success" && result.files?.length) {
|
|
@@ -313,8 +313,8 @@ const { comments, available, createComment } = useComments({ record_id: recordId
|
|
|
313
313
|
|
|
314
314
|
`record_id` must be a **real** id — the fetch keys on `available` alone, so an empty string is not
|
|
315
315
|
skipped, it fetches comments for nothing. When the id comes from a row's `__source_record_id`
|
|
316
|
-
addressing column (see [queries](./queries.md)) narrow it first: a grouped query
|
|
317
|
-
columns
|
|
316
|
+
addressing column (see [queries](./queries.md)) narrow it first: a grouped query answers its addressing
|
|
317
|
+
columns as `null`, and a hook cannot be called conditionally, so the guard belongs at the component that
|
|
318
318
|
renders the panel. State: `{ comments, loading, error, available, createComment,
|
|
319
319
|
updateComment, deleteComment, refetch }`.
|
|
320
320
|
|
|
@@ -330,7 +330,8 @@ updateComment, deleteComment, refetch }`.
|
|
|
330
330
|
least one file (empty input is a client no-op; the server enforces the same rule). A file id
|
|
331
331
|
that names no live file in this workspace refuses the comment. Content max 10,000 characters.
|
|
332
332
|
- `updateComment(id, { content, files? })` — omit `files` to keep the current attachment set (a
|
|
333
|
-
text-only edit never drops attachments); pass `files` to replace the whole set.
|
|
333
|
+
text-only edit never drops attachments); pass `files` to replace the whole set. An id the comment does not already hold must name a live
|
|
334
|
+
file in this workspace; one it holds stays even once its file is archived.
|
|
334
335
|
- All three mutations apply **optimistically** with rollback on error, then repopulate from the
|
|
335
336
|
server. **Limitation:** an optimistic create renders a placeholder row (temporary id, empty
|
|
336
337
|
`table_id`/`workspace_id`, `files: null`) until the refetch lands — don't persist anything keyed
|
package/docs/queries.md
CHANGED
|
@@ -152,7 +152,7 @@ Every row-level query carries auto-injected addressing columns alongside your ou
|
|
|
152
152
|
`__created_at`, `__updated_at`, and per-projection `__src_field_<output>` metadata. You never
|
|
153
153
|
declare these; output names starting with `__source_` / `__src_field_`, or equal to
|
|
154
154
|
`__created_at` / `__updated_at`, are **reserved** and rejected. A `group` collapses rows and
|
|
155
|
-
|
|
155
|
+
answers its addressing as `null` (grouped results can't be written through or matched by `record_id`); a `join`
|
|
156
156
|
keeps the **left** side's addressing.
|
|
157
157
|
|
|
158
158
|
A caller outside the app's organization — a public-app visitor, or a member of another
|
|
@@ -277,7 +277,7 @@ came from.
|
|
|
277
277
|
|
|
278
278
|
Full semantics in §8. `by` may be empty (a single-row aggregate); `aggregates` needs ≥ 1 entry.
|
|
279
279
|
Aggregate operation × input-column type is validated at bind. Grouping collapses rows —
|
|
280
|
-
addressing is
|
|
280
|
+
addressing is `null`. An aggregate's `filter` limits it to the rows that match (§8).
|
|
281
281
|
|
|
282
282
|
### `window` — aggregate without collapsing
|
|
283
283
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/app-sdk",
|
|
3
|
-
"version": "0.111.
|
|
3
|
+
"version": "0.111.4",
|
|
4
4
|
"description": "The SDK a Lotics custom-code app reads and writes through \u2014 typed hooks over the host bridge, cell readers, mount() and AppRouter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|