@lotics/app-sdk 0.122.9 → 0.123.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/dist/index.js +2 -2
- package/dist/queries.d.ts +4 -1
- package/dist/shared_types.d.ts +5 -2
- package/dist/store.d.ts +6 -0
- package/docs/data_fetching.md +2 -2
- package/docs/files.md +9 -6
- package/docs/members_and_options.md +3 -2
- package/docs/mutations.md +5 -3
- package/docs/recipes.md +6 -4
- package/docs/runtime.md +2 -2
- package/docs/security.md +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -516,7 +516,7 @@ import { useCallback as useCallback2, useEffect as useEffect2, useRef, useSyncEx
|
|
|
516
516
|
var slots = /* @__PURE__ */ new Map();
|
|
517
517
|
var clock = 0;
|
|
518
518
|
var nextStaging = 1;
|
|
519
|
-
var FOCUS_THROTTLE_MS =
|
|
519
|
+
var FOCUS_THROTTLE_MS = 6e4;
|
|
520
520
|
var PAINT_CAP_MS = 1e3;
|
|
521
521
|
function draw(paint) {
|
|
522
522
|
if (paint.drawn) return;
|
|
@@ -32762,7 +32762,7 @@ async function queryAll(alias, params = {}, opts = {}) {
|
|
|
32762
32762
|
async function exportQuery(alias, call) {
|
|
32763
32763
|
const sort = call.sort === void 0 || call.sort.length === 0 ? void 0 : [...call.sort];
|
|
32764
32764
|
const tabs = call.tabs?.map((tab) => ({ ...tab, sort: tab.sort === void 0 || tab.sort.length === 0 ? void 0 : [...tab.sort] }));
|
|
32765
|
-
const body = { alias, params: call.params ?? {}, filter: call.filter, sort, ...tabs === void 0 ? {} : { tabs }, report: call.report, file: call.file };
|
|
32765
|
+
const body = { alias, params: call.params ?? {}, filter: call.filter, sort, ...tabs === void 0 ? {} : { tabs }, report: call.report, file: call.file, ...call.labels === void 0 ? {} : { labels: call.labels } };
|
|
32766
32766
|
const mocked = getMockExport(alias);
|
|
32767
32767
|
if (mocked !== null) return mocked(body);
|
|
32768
32768
|
return (await rpc("export", body)).file;
|
package/dist/queries.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { AppQueries, AppQueryColumns } from "./types.js";
|
|
|
3
3
|
import type { ResolvedOption } from "./select.js";
|
|
4
4
|
import type { ReportLanguage } from "./shared_types.js";
|
|
5
5
|
import type { ExportedFile } from "./shared_types.js";
|
|
6
|
-
import type { ReportColumn, ReportReading } from "./shared_types.js";
|
|
6
|
+
import type { ReportColumn, ReportLabels, ReportReading } from "./shared_types.js";
|
|
7
7
|
/**
|
|
8
8
|
* One row of a query whose columns codegen could not read (a dynamic alias, a
|
|
9
9
|
* bare `from_table`). Values stay `unknown` — the cell readers narrow them.
|
|
@@ -249,6 +249,9 @@ export interface ExportCall {
|
|
|
249
249
|
tabs?: readonly ExportTab[];
|
|
250
250
|
report: ExportReport;
|
|
251
251
|
file: ExportFile;
|
|
252
|
+
/** The words each option key of a select column prints as, by the column, in either file: a cell the server names
|
|
253
|
+
* by its field's option prints as that. */
|
|
254
|
+
labels?: ReportLabels;
|
|
252
255
|
}
|
|
253
256
|
/**
|
|
254
257
|
* Every row a declared query matches — narrowed by `filter` and ordered by `sort` as a page of it is — made into one
|
package/dist/shared_types.d.ts
CHANGED
|
@@ -35,9 +35,12 @@ export type ReportColumn = {
|
|
|
35
35
|
header: string;
|
|
36
36
|
/** A `date` cell is an ISO day or moment. */
|
|
37
37
|
type: "number" | "date" | "text";
|
|
38
|
-
/** A select whose cells hold bare option keys, no field of the query naming them: the words each key prints as. */
|
|
39
|
-
labels?: { readonly [x: string]: string; };
|
|
40
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* The words each option key of a select column prints as, by the column: a cell no field of the query names arrives
|
|
41
|
+
* as its bare key.
|
|
42
|
+
*/
|
|
43
|
+
export type ReportLabels = { readonly [x: string]: Readonly<Record<string, string>>; };
|
|
41
44
|
export type ReportLanguage = "vi" | "en";
|
|
42
45
|
export type ReportPart = {
|
|
43
46
|
label: string;
|
package/dist/store.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** How a key is read: from when the request was sent and the answer it holds now (a feed grows from it). */
|
|
2
2
|
export type Fetcher<T> = (askedAt: number, current: T | undefined) => Promise<T>;
|
|
3
|
+
/**
|
|
4
|
+
* A focus or reconnect within this long of a key's last ask asks nothing: returns to the tab a minute apart are one
|
|
5
|
+
* visit. A push asks at once whatever this says, so what this bounds is only what no push carries — a dead channel, a
|
|
6
|
+
* value the clock moves.
|
|
7
|
+
*/
|
|
8
|
+
export declare const FOCUS_THROTTLE_MS = 60000;
|
|
3
9
|
/** The read clock: every request stamps when it was sent, so an answer is known to carry what settled before it. */
|
|
4
10
|
export declare function tick(): number;
|
|
5
11
|
/** Ask again every key reading one of `aliases` — every query key where none is named; an unmounted one on its next mount. */
|
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 an `ExportedFile`: `{ filename, mime_type, url, size, created_at }` | 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
|
|
27
|
+
| `exportQuery(alias, { params, filter, sort, tabs?, report, file, labels? })` | a promise of an `ExportedFile`: `{ filename, mime_type, url, size, created_at }` | 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)). `labels`, by `select` output column, the words each option key prints as in either file — a cell no field of the query names (a union's literal column) arrives as its bare key; a cell its field's option names prints as that, a key the map lacks as itself, and a column no select of the query holds as it is. Past 20,000 rows, in any tab, it rejects, never cuts; each caller makes at most 60 exports a minute. The file has no `id` — it is a download, not a workspace file a workflow or a field takes: its `url` is signed for the caller for at most an hour, so download it with `downloadFileFromUrl(url, filename)` (`@lotics/ui/download`) when the promise resolves — a tab `openExternal` opens seconds after the click is a pop-up the browser blocks; the server deletes the file a day after it is made |
|
|
28
28
|
|
|
29
29
|
Every hook answers the same `QueryState`: `rows`, `truncated`, `total`, `counts`, `loading`,
|
|
30
30
|
`isValidating`, `answeredAt`, `error`, `pending`, `refetch`, `page`, `setPage`, `pageCount`, `hasMore`,
|
|
@@ -146,7 +146,7 @@ write it as `{ node_type: "condition", type: "record_id", operator, value }`.
|
|
|
146
146
|
renders the cached rows instantly *and* re-reads them in the background, so a list reflects what
|
|
147
147
|
another screen changed while you were away. Identical concurrent reads share one request, and reads asked within a few milliseconds of each other go to the server together, each answered or refused on its own.
|
|
148
148
|
Freshness comes from four places — arrival, window focus / tab return / network reconnect
|
|
149
|
-
(`revalidateOnFocus`, default on; a read asked within the last
|
|
149
|
+
(`revalidateOnFocus`, default on; a read asked within the last minute is not asked again,
|
|
150
150
|
and the reads it asks again re-render together, as a push's do — so what changed while the tab
|
|
151
151
|
was hidden or offline lands in one piece), a **realtime push**, and **this app's own successful
|
|
152
152
|
write**, which re-reads the mounted queries over the tables its body names without being asked
|
package/docs/files.md
CHANGED
|
@@ -247,7 +247,7 @@ Decode with `readFiles(cell)` → `AppFile[]` (`dist/row.d.ts`):
|
|
|
247
247
|
| `id` | `string` | The stored file id |
|
|
248
248
|
| `filename` | `string` | Original filename (post-optimization name for converted images) |
|
|
249
249
|
| `mime_type` | `string` | |
|
|
250
|
-
| `url` | `string` | Presigned serving URL — render, or
|
|
250
|
+
| `url` | `string` | Presigned serving URL — render it, or download it with `downloadFileFromUrl` |
|
|
251
251
|
| `thumbnail_url` | `string?` | Presigned image thumbnail (see the 404 note below) |
|
|
252
252
|
| `size` | `number?` | Byte size — **absent on older files not yet backfilled**; render only when present |
|
|
253
253
|
| `created_at` | `string?` | ISO upload timestamp |
|
|
@@ -320,22 +320,25 @@ dedupes them, and returns them on the execute response as `WorkflowResult.files?
|
|
|
320
320
|
with `files` absent.
|
|
321
321
|
|
|
322
322
|
```tsx
|
|
323
|
+
import { downloadFileFromUrl } from "@lotics/ui/download";
|
|
324
|
+
|
|
323
325
|
const generate = useWorkflow("generateInvoice");
|
|
324
326
|
// `__source_record_id` is `null` on a grouped row — so narrow it rather than
|
|
325
327
|
// handing the workflow no id where it declares a record.
|
|
326
328
|
if (!row.__source_record_id) return;
|
|
327
329
|
const result = await generate({ record_id: row.__source_record_id });
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
330
|
+
const [file] = result.status === "success" ? (result.files ?? []) : [];
|
|
331
|
+
if (file?.url) await downloadFileFromUrl(file.url, file.filename);
|
|
331
332
|
```
|
|
332
333
|
|
|
333
334
|
- **Don't hand back a `url` or `file_id` via `return({ data })`** — extraction is automatic, and
|
|
334
335
|
the extracted entry is presigned while a hand-returned id is not.
|
|
335
336
|
- The presigned URLs work for **anonymous public-app viewers** — a public visitor can download a
|
|
336
337
|
document the workflow generated for them.
|
|
337
|
-
-
|
|
338
|
-
|
|
338
|
+
- Download one with `downloadFileFromUrl(url, filename)` (`@lotics/ui/download`): it fetches the
|
|
339
|
+
presigned URL and saves it, and the iframe allows downloads. A tab `openExternal` opens once the
|
|
340
|
+
run answers is a pop-up the browser blocks. Bytes the app builds *in the browser* leave through
|
|
341
|
+
`downloadFile` ([runtime](./runtime.md)).
|
|
339
342
|
|
|
340
343
|
### Naming what goes into a zip
|
|
341
344
|
|
|
@@ -428,8 +428,9 @@ const { reverted, kept } = await undoRecordChange("job", recordId, entry.id);
|
|
|
428
428
|
before is in neither. The write is the viewer's own save: they hold `write_data` on the table as
|
|
429
429
|
the call arrives, on a row their own row rule covers, and the record's lock, the table's checks
|
|
430
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,
|
|
432
|
-
|
|
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.
|
|
433
434
|
The undo is an entry of its own; the answer arrives once the record's reads are read anew.
|
|
434
435
|
- **Pages**: newest first, about thirty entries each; `next` is where the trail goes on —
|
|
435
436
|
`undefined` at its end. A page may hold fewer entries and still say `next` where the trail it read
|
package/docs/mutations.md
CHANGED
|
@@ -142,13 +142,15 @@ Any step in the run whose tool output carries a `file_id` (most commonly
|
|
|
142
142
|
`generate_document`) is collected automatically into
|
|
143
143
|
`result.files[]` — each an `UploadedFile`
|
|
144
144
|
`{ id, filename, mime_type, url?, thumbnail_url? }` with a presigned `url` (24-hour TTL) the
|
|
145
|
-
app
|
|
145
|
+
app downloads directly — a tab opened once the run answers is a pop-up the browser blocks:
|
|
146
146
|
|
|
147
147
|
```tsx
|
|
148
|
+
import { downloadFileFromUrl } from "@lotics/ui/download";
|
|
149
|
+
|
|
148
150
|
const gen = useWorkflow("generateInvoice");
|
|
149
151
|
const { status, files } = await gen({ order_id });
|
|
150
|
-
const
|
|
151
|
-
if (status === "success" && url) await
|
|
152
|
+
const file = files?.[0];
|
|
153
|
+
if (status === "success" && file?.url) await downloadFileFromUrl(file.url, file.filename);
|
|
152
154
|
```
|
|
153
155
|
|
|
154
156
|
The same run reached through the `run_app_workflow` tool — from chat, from MCP, from an app
|
package/docs/recipes.md
CHANGED
|
@@ -28,19 +28,21 @@ return({ status: "success", message: `Đã tạo ${code}` }); // the file is e
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
```tsx
|
|
31
|
-
import { useWorkflow
|
|
31
|
+
import { useWorkflow } from "@lotics/app-sdk";
|
|
32
|
+
import { downloadFileFromUrl } from "@lotics/ui/download";
|
|
32
33
|
|
|
33
34
|
const gen = useWorkflow("genDebit");
|
|
34
35
|
const { files } = await gen({ record_id: rid });
|
|
35
|
-
const
|
|
36
|
-
if (url) await
|
|
36
|
+
const file = files?.[0];
|
|
37
|
+
if (file?.url) await downloadFileFromUrl(file.url, file.filename);
|
|
37
38
|
```
|
|
38
39
|
|
|
39
40
|
The `dtl_…` is a template you registered **once**, ahead of the app, through the CLI — see
|
|
40
41
|
`lotics docs document_templates` for the five types and how each is created. An app fills
|
|
41
42
|
templates; it never authors them.
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
**A generated file downloads with `downloadFileFromUrl`, never a tab.** The sandbox drops `window.open`
|
|
45
|
+
silently, and a tab `openExternal` opens seconds after the click is a pop-up the browser blocks
|
|
44
46
|
([runtime](./runtime.md)).
|
|
45
47
|
|
|
46
48
|
## Return structured data from a workflow
|
package/docs/runtime.md
CHANGED
|
@@ -293,7 +293,7 @@ nobody stated.
|
|
|
293
293
|
|
|
294
294
|
```tsx
|
|
295
295
|
import { openExternal } from "@lotics/app-sdk";
|
|
296
|
-
await openExternal(
|
|
296
|
+
await openExternal(invoiceUrl);
|
|
297
297
|
```
|
|
298
298
|
|
|
299
299
|
`openExternal(url: string): Promise<void>` (`dist/open_external.d.ts`). The
|
|
@@ -376,7 +376,7 @@ function onExportClick() {
|
|
|
376
376
|
string): void` (`dist/download.d.ts`) saves bytes the app generated **in the
|
|
377
377
|
browser** (an exported .xlsx, a CSV, generated text) to the visitor's device.
|
|
378
378
|
It is the client-side counterpart to the server path (workflow generates a file
|
|
379
|
-
→ `WorkflowResult.files[].url` → `
|
|
379
|
+
→ `WorkflowResult.files[].url` → `downloadFileFromUrl` from `@lotics/ui/download`).
|
|
380
380
|
|
|
381
381
|
- **Pure DOM, no RPC.** The embed sandbox includes `allow-downloads`, so a
|
|
382
382
|
same-origin blob download triggered from a user gesture needs no host
|
package/docs/security.md
CHANGED
|
@@ -103,7 +103,7 @@ A publicly-shared app (its own origin, or its public link) is reachable by **any
|
|
|
103
103
|
| 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
104
|
| Comments (`useComments`) | **No** — members-only |
|
|
105
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 |
|
|
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
107
|
| Member roster (`useMembers`) | **No** — same-org members only (below) |
|
|
108
108
|
|
|
109
109
|
**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/package.json
CHANGED