@lotics/app-sdk 0.111.4 → 0.112.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.d.ts +1 -0
- package/dist/index.js +26508 -26012
- package/dist/mock.d.ts +3 -2
- package/dist/queries.d.ts +3 -3
- package/dist/shared_types.d.ts +71 -7
- package/docs/data_fetching.md +3 -2
- package/docs/files.md +7 -4
- package/docs/mutations.md +16 -6
- package/docs/queries.md +8 -5
- package/docs/runtime.md +1 -1
- package/docs/workflows.md +22 -6
- package/package.json +1 -1
package/dist/mock.d.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* `useFileUpload` is not mocked.
|
|
8
8
|
*/
|
|
9
9
|
import type { QueryAggregate } from "./shared_types.js";
|
|
10
|
-
import type {
|
|
10
|
+
import type { WorkflowResult } from "./hooks.js";
|
|
11
11
|
import type { MockAgent } from "./mock_agent_run.js";
|
|
12
|
+
import type { ExportedFile } from "./shared_types.js";
|
|
12
13
|
import type { ExportCall, QuerySortKey } from "./queries.js";
|
|
13
14
|
import type { RecordingState } from "./recording_state.js";
|
|
14
15
|
/** A result, or a function of the inputs — return a slow promise to review the pending state. */
|
|
@@ -27,7 +28,7 @@ export type MockQuery = Array<Record<string, unknown>> | ((call: MockQueryCall)
|
|
|
27
28
|
/** The file an export of a query makes, as a function of the call — return a slow promise to review the pending state. */
|
|
28
29
|
export type MockExport = (call: ExportCall & {
|
|
29
30
|
alias: string;
|
|
30
|
-
}) =>
|
|
31
|
+
}) => ExportedFile | Promise<ExportedFile>;
|
|
31
32
|
export interface AppFixture {
|
|
32
33
|
queries?: Record<string, MockQuery>;
|
|
33
34
|
workflows?: Record<string, MockWorkflow>;
|
package/dist/queries.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { QueryAggregate } from "./shared_types.js";
|
|
2
2
|
import type { AppQueries, AppQueryColumns } from "./types.js";
|
|
3
3
|
import type { ResolvedOption } from "./select.js";
|
|
4
|
-
import type { UploadedFile } from "./hooks.js";
|
|
5
4
|
import type { ReportLanguage } from "./shared_types.js";
|
|
5
|
+
import type { ExportedFile } from "./shared_types.js";
|
|
6
6
|
import type { ReportColumn, ReportReading } from "./shared_types.js";
|
|
7
7
|
/**
|
|
8
8
|
* One row of a query whose columns codegen could not read (a dynamic alias, a
|
|
@@ -234,9 +234,9 @@ export interface ExportCall {
|
|
|
234
234
|
/**
|
|
235
235
|
* Every row a declared query matches — narrowed by `filter` and ordered by `sort` as a page of it is — made into one
|
|
236
236
|
* file by the server, so no row reaches the browser. Past the most rows one export holds it is refused, never cut.
|
|
237
|
-
* Resolves the
|
|
237
|
+
* Resolves a link to download the file, signed for at most an hour.
|
|
238
238
|
*/
|
|
239
|
-
export declare function exportQuery(alias: string, call: ExportCall): Promise<
|
|
239
|
+
export declare function exportQuery(alias: string, call: ExportCall): Promise<ExportedFile>;
|
|
240
240
|
/** One select column's full option set. */
|
|
241
241
|
export interface FieldOptions {
|
|
242
242
|
/** The source field's display name. */
|
package/dist/shared_types.d.ts
CHANGED
|
@@ -3,17 +3,81 @@
|
|
|
3
3
|
* `scripts/build_sdk.mjs`, because that package is private and ships in no
|
|
4
4
|
* tarball. Written at every build, never edited.
|
|
5
5
|
*/
|
|
6
|
-
export
|
|
7
|
-
export type
|
|
6
|
+
/** The file an app export made, as a link to download it: no workspace file, so no id, and kept a day. */
|
|
7
|
+
export type ExportedFile = {
|
|
8
|
+
filename: string;
|
|
9
|
+
mime_type: string;
|
|
10
|
+
/** Signed for at most an hour, for the caller who exported. */
|
|
11
|
+
url: string;
|
|
12
|
+
/** Bytes. */
|
|
13
|
+
size: number;
|
|
14
|
+
/** When the server made it, ISO 8601. */
|
|
15
|
+
created_at: string;
|
|
16
|
+
};
|
|
17
|
+
/** What an aggregate asks: output columns to group by, an output date grouped by its day, an output number summed. */
|
|
18
|
+
export type QueryAggregate = {
|
|
19
|
+
by?: readonly string[];
|
|
20
|
+
day?: string;
|
|
21
|
+
sum?: string;
|
|
22
|
+
};
|
|
23
|
+
export type ReportColumn = {
|
|
24
|
+
/** Each row's cell by it. */
|
|
25
|
+
key: string;
|
|
26
|
+
header: string;
|
|
27
|
+
/** A `date` cell is an ISO day or moment. */
|
|
28
|
+
type: "number" | "text" | "date";
|
|
29
|
+
/** A select whose cells hold bare option keys, no field of the query naming them: the words each key prints as. */
|
|
30
|
+
labels?: { readonly [x: string]: string; };
|
|
31
|
+
};
|
|
8
32
|
export type ReportLanguage = "vi" | "en";
|
|
9
|
-
export type ReportPart = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
33
|
+
export type ReportPart = {
|
|
34
|
+
label: string;
|
|
35
|
+
value: number;
|
|
36
|
+
/** Its share of the whole, 0–1; none where the whole is unknown or nothing. */
|
|
37
|
+
share: number | null;
|
|
38
|
+
};
|
|
39
|
+
export type ReportPivotColumn = {
|
|
40
|
+
label: string;
|
|
41
|
+
total: number | null;
|
|
42
|
+
};
|
|
43
|
+
export type ReportPivotRow = {
|
|
44
|
+
label: string;
|
|
45
|
+
/** Its figure under every column, in their order. */
|
|
46
|
+
values: number[];
|
|
47
|
+
total: number | null;
|
|
48
|
+
};
|
|
49
|
+
export type ReportPoint = {
|
|
50
|
+
/** The period as its axis names it. */
|
|
51
|
+
label: string;
|
|
52
|
+
value: number;
|
|
53
|
+
/** Still running, or cut by the window's edge. */
|
|
54
|
+
partial: boolean;
|
|
55
|
+
};
|
|
13
56
|
export type ReportReading = (ReportReadingHead & { kind: "metric"; value: number | null; }) | (ReportReadingHead & { kind: "breakdown"; byLabel: string; parts: ReportPart[]; total: number | null; }) | (ReportReadingHead & { kind: "trend"; points: ReportPoint[]; }) | (ReportReadingHead & { kind: "pivot"; rowsLabel: string; columnsLabel: string; columns: ReportPivotColumn[]; rows: ReportPivotRow[]; total: number | null; }) | { kind: "failed"; label: string; };
|
|
14
|
-
|
|
57
|
+
/** What every reading of numbers states beside its figures. */
|
|
58
|
+
export type ReportReadingHead = {
|
|
59
|
+
label: string;
|
|
60
|
+
/** What its figures are: the summed field's name, or a count's word. */
|
|
61
|
+
valueLabel: string;
|
|
62
|
+
/** The unit or currency its figures read in; none for a count or a plain number. */
|
|
63
|
+
unit: string | null;
|
|
64
|
+
/** The read behind it was cut: its figures stand, every total over them is left out (`null`). */
|
|
65
|
+
truncated: boolean;
|
|
66
|
+
};
|
|
15
67
|
export type TableField = { key: string; name: string; description: string; type: "number"; format: "number" | "currency" | "percentage"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; currency?: string | null | undefined; unit?: string | null | undefined; unit_field?: string | null | undefined; currency_field?: string | null | undefined; default_value?: number | null | undefined; } | { key: string; name: string; description: string; type: "text"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; unique?: boolean | undefined; format?: "text" | "link" | "markdown" | undefined; default_value?: string | null | undefined; } | { key: string; name: string; description: string; type: "date"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; format?: "date" | "datetime" | "date_range" | "datetime_range" | undefined; timezone?: string | undefined; derive_from?: "created_at" | "updated_at" | undefined; default_value?: string | null | undefined; } | { key: string; name: string; description: string; type: "boolean"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; default_value?: boolean | null | undefined; } | { key: string; name: string; description: string; type: "select"; options: { key: string; name: string; color: "red" | "orange" | "amber" | "yellow" | "lime" | "green" | "emerald" | "teal" | "cyan" | "sky" | "blue" | "indigo" | "violet" | "purple" | "fuchsia" | "pink" | "rose" | "slate" | "gray" | "zinc" | "neutral" | "stone"; mark?: { kind: string; name: string; } | undefined; }[]; confirm_before_update?: boolean | undefined; required?: boolean | undefined; multi?: boolean | undefined; default_value?: string[] | null | undefined; } | { key: string; name: string; description: string; type: "select_member"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; multi?: boolean | undefined; default_value?: string[] | null | undefined; } | { key: string; name: string; description: string; type: "select_record_link"; table_id: string; display_field_keys: string[]; confirm_before_update?: boolean | undefined; required?: boolean | undefined; display_field_widths?: Record<string, number> | undefined; paired_field_key?: string | undefined; cardinality?: "one" | "many" | undefined; } | { key: string; name: string; description: string; type: "rollup"; source_field_key: string; aggregate_option: { operation: "count"; field_key?: string | undefined; } | { operation: "unique" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique"; field_key: string; } | { operation: "min" | "unique" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "sum" | "avg" | "median" | "max" | "range"; field_key: string; } | { operation: "unique" | "date_range" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "earliest" | "latest"; field_key: string; } | { operation: "unique" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "selected"; field_key: string; } | { operation: "unique" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "selected"; field_key: string; } | { operation: "empty" | "filled" | "percent_empty" | "percent_filled" | "selected"; field_key: string; } | { operation: "checked" | "unchecked" | "percent_checked" | "percent_unchecked"; field_key: string; } | { operation: "empty" | "filled" | "percent_empty" | "percent_filled" | "selected"; field_key: string; } | { operation: "min" | "unique" | "date_range" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "sum" | "avg" | "median" | "max" | "range" | "earliest" | "latest" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked"; field_key: string; } | { operation: "min" | "unique" | "date_range" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "sum" | "avg" | "median" | "max" | "range" | "earliest" | "latest"; field_key: string; }; confirm_before_update?: boolean | undefined; required?: boolean | undefined; filter?: TableRecordFiltersGroupNode | undefined; aggregate_field_type?: "number" | "boolean" | "text" | "date" | "select" | "select_member" | "select_record_link" | "rollup" | "files" | "formula" | "lookup" | "button" | "autonumber" | undefined; aggregate_field_format?: string | undefined; aggregate_field_currency?: string | undefined; aggregate_field_unit?: string | undefined; aggregate_field_unit_field?: string | undefined; aggregate_field_currency_field?: string | undefined; } | { key: string; name: string; description: string; type: "lookup"; source_field_key: string; lookup_field_key: string; confirm_before_update?: boolean | undefined; required?: boolean | undefined; lookup_field_type?: "number" | "boolean" | "text" | "date" | "select" | "select_member" | "select_record_link" | "rollup" | "files" | "formula" | "lookup" | "button" | "autonumber" | undefined; lookup_field_format?: string | undefined; lookup_field_currency?: string | undefined; lookup_field_unit?: string | undefined; lookup_field_options?: { key: string; name: string; color?: string | undefined; mark?: { kind: string; name: string; } | undefined; }[] | undefined; order_by?: { field_key: string; direction: "asc" | "desc"; } | undefined; } | { key: string; name: string; description: string; type: "files"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; } | { key: string; name: string; description: string; type: "formula"; formula: { expression: string; format?: "number" | "currency" | "percentage" | "link" | undefined; currency?: string | undefined; unit?: string | undefined; unit_field?: string | undefined; currency_field?: string | undefined; options?: { key: string; name: string; color: "red" | "orange" | "amber" | "yellow" | "lime" | "green" | "emerald" | "teal" | "cyan" | "sky" | "blue" | "indigo" | "violet" | "purple" | "fuchsia" | "pink" | "rose" | "slate" | "gray" | "zinc" | "neutral" | "stone"; mark?: { kind: string; name: string; } | undefined; }[] | undefined; output_type?: "number" | "boolean" | "text" | "date" | "datetime" | "select" | undefined; volatile?: boolean | undefined; }; confirm_before_update?: boolean | undefined; required?: boolean | undefined; } | { key: string; name: string; description: string; type: "button"; text: string; confirm_before_update?: boolean | undefined; required?: boolean | undefined; workflow_id?: string | null | undefined; } | { key: string; name: string; description: string; type: "autonumber"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; prefix?: string | undefined; padding?: number | undefined; template?: string | undefined; };
|
|
16
68
|
export type TableRecordFilters = TableRecordFiltersConditionNode | TableRecordFiltersTraversalNode | TableRecordFiltersGroupNode;
|
|
17
69
|
export type TableRecordFiltersConditionNode = { node_type: "condition"; field_key?: string | undefined; type?: string | undefined; operator: string; value?: unknown; unit_option?: string | undefined; };
|
|
18
70
|
export type TableRecordFiltersGroupNode = { node_type: "group"; logic: "and" | "or"; children: (TableRecordFilters)[]; };
|
|
71
|
+
/**
|
|
72
|
+
* Traversal node — filter the current table by a field on a record reachable
|
|
73
|
+
* through one or more `select_record_link` hops. ANY semantics: a row matches
|
|
74
|
+
* if at least one reachable record satisfies the inner condition.
|
|
75
|
+
*
|
|
76
|
+
* - `path`: the link field keys to walk, in order. Last entry must be a
|
|
77
|
+
* `select_record_link` field; all entries must be link fields.
|
|
78
|
+
* - `condition`: the standard field-condition shape, evaluated on the FINAL
|
|
79
|
+
* table reached by the path.
|
|
80
|
+
*
|
|
81
|
+
* Max path depth and inner-condition syntax are validated by `validateFilters`.
|
|
82
|
+
*/
|
|
19
83
|
export type TableRecordFiltersTraversalNode = { node_type: "traversal"; path: string[]; condition: TableRecordFiltersConditionNode; };
|
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
|
|
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, 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 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 open it with `openExternal` when the promise resolves; 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`, `error`, `pending`, `refetch`, `page`, `setPage`, `pageCount`, `hasMore`,
|
|
@@ -151,7 +151,8 @@ write it as `{ node_type: "condition", type: "record_id", operator, value }`.
|
|
|
151
151
|
`refetch()` covers what none of those can know about.
|
|
152
152
|
- **This app's own writes show from the press.** Every read draws each write this app has made
|
|
153
153
|
over its rows before the server answers — cells, membership, new rows, `total`, `counts` and
|
|
154
|
-
groups — and says **`pending`** while what one changes is not all drawn.
|
|
154
|
+
groups — and says **`pending`** while what one changes is not all drawn. A visitor from outside
|
|
155
|
+
your organization sees each write when the server answers instead. Nothing to wire: [./mutations.md](./mutations.md)
|
|
155
156
|
§ A write shows from the press.
|
|
156
157
|
- **Realtime push keeps an already-open screen current.** When a table one of your queries reads
|
|
157
158
|
changes — another member, a workflow, the chat agent, or an external agent writing over the CLI or
|
package/docs/files.md
CHANGED
|
@@ -32,7 +32,9 @@ storage, size and upload day), which is inert like an upload until a save puts i
|
|
|
32
32
|
place. A save carrying `_added` and `_removed` of one field puts the added file where the removed
|
|
33
33
|
one stood, so a rename or a replace keeps its position. The name keeps the file's extension
|
|
34
34
|
exactly — `"scan.pdf"` → `"Invoice 9.pdf"`; `.html` is refused — and holds no `/`, `\` or control
|
|
35
|
-
character. Offer it only where the app writes that field: the save is what may be refused.
|
|
35
|
+
character. Offer it only where the app writes that field: the save is what may be refused. A
|
|
36
|
+
visitor from outside your organization may rename only a file uploaded through this app, within
|
|
37
|
+
twenty minutes of that upload starting; any other file answers as a missing one.
|
|
36
38
|
|
|
37
39
|
## Uploading — `useFileUpload`
|
|
38
40
|
|
|
@@ -96,9 +98,10 @@ to object storage → finalize. The API server never proxies the bytes.
|
|
|
96
98
|
- **Transport resilience.** The storage `PUT` has a 5-minute per-request timeout and retries
|
|
97
99
|
network errors / timeouts / 5xx up to 3 attempts with 1 s → 2 s backoff between attempts. 4xx
|
|
98
100
|
responses are terminal (no retry). The presigned upload URL itself is valid for 10 minutes.
|
|
99
|
-
- **Server-side verification.** The finalize step
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
- **Server-side verification.** The finalize step takes only the upload this app minted the URL
|
|
102
|
+
for, and verifies the actually-stored object: an object larger than the size declared when its
|
|
103
|
+
URL was minted (or of unverifiable size) is rejected **and its bytes deleted** — the file record
|
|
104
|
+
is never created.
|
|
102
105
|
|
|
103
106
|
### Upload limits
|
|
104
107
|
|
package/docs/mutations.md
CHANGED
|
@@ -188,8 +188,9 @@ envelope's grammar — `message` is required, `field_errors` never reaches the a
|
|
|
188
188
|
redeploy); use inline `options` for a fixed enum the app owns.
|
|
189
189
|
- **Validated at run.** On a success return, the returned `data` is validated against the
|
|
190
190
|
schema at the app boundary — a mismatch resolves as `status: "error"` with a field-level
|
|
191
|
-
message, so a declared output is a real contract.
|
|
192
|
-
|
|
191
|
+
message, so a declared output is a real contract. A visitor from outside your organization reads
|
|
192
|
+
the generic sentence a failed run gives them instead, since the message names the keys the body
|
|
193
|
+
returned. An *error* return's `data` passes through unvalidated.
|
|
193
194
|
- **An output field marked `required: false` types as `key?: T | null`.** The derivation
|
|
194
195
|
collapses "may be absent" and "may be null" into that one flag, so the generated type admits
|
|
195
196
|
both — narrow with `!= null`, never with a bare truthiness check on a number or a string.
|
|
@@ -327,8 +328,9 @@ const onClose = async (recordId: string) => {
|
|
|
327
328
|
```
|
|
328
329
|
|
|
329
330
|
Only on **success**: a refused write changed nothing. A body that names a table only at run time,
|
|
330
|
-
a run whose writes could not be predicted, a run before the app's write model has loaded,
|
|
331
|
-
`useAgentRun` leg that lands, re-read every
|
|
331
|
+
a run whose writes could not be predicted, a run before the app's write model has loaded, a run
|
|
332
|
+
by a visitor from outside your organization, and a `useAgentRun` leg that lands, re-read every
|
|
333
|
+
mounted query. A comment written or deleted re-reads
|
|
332
334
|
every `useCommentCounts`. Under a design-time
|
|
333
335
|
fixture every mounted read re-reads, since a fixture has no body to read tables from. The host's
|
|
334
336
|
realtime push is separate: it carries changes **other** people make, and is far too slow for your
|
|
@@ -554,8 +556,11 @@ What the steps cannot tell before they run is never guessed:
|
|
|
554
556
|
delete: every read over the tables it may touch (every read, where it names none) is `pending`
|
|
555
557
|
until the answer lands.
|
|
556
558
|
- A guard (`validate`, a `return` with `status: "error"`) it can decide over held rows: a refused
|
|
557
|
-
write draws nothing
|
|
558
|
-
a
|
|
559
|
+
write draws nothing, and neither does one whose `validate` check throws on those rows, since the
|
|
560
|
+
server refuses that write too. One over a value the app does not hold is assumed to pass — the
|
|
561
|
+
server is the authority, and a refusal takes the drawing back.
|
|
562
|
+
- A prediction that takes more than a second: the run is sent without it, and its writes wait for
|
|
563
|
+
the answer — predicting never holds a press back longer than that.
|
|
559
564
|
- A guard or branch comparing an input with a constant (`i.code == "VIP"`) never reaches the app:
|
|
560
565
|
the constant would be shown to everyone who can open it. The guard is assumed to pass, and the
|
|
561
566
|
branch's writes wait for the answer. A comparison with a held row's cell reaches it.
|
|
@@ -564,6 +569,11 @@ Every read hook returns **`pending`**: a write this app made is in flight over i
|
|
|
564
569
|
landed with part of what it changes not yet read back. The rows already show what is known; use it
|
|
565
570
|
for a quiet "saving" cue, never to hide the rows.
|
|
566
571
|
|
|
572
|
+
**A visitor from outside your organization sees a write when the server answers.** For anyone who
|
|
573
|
+
opens the app through its public link rather than access of their own, nothing is drawn ahead and
|
|
574
|
+
no read's `pending` turns on: each write shows once its run answers, so a press that needs a cue
|
|
575
|
+
before then takes it from the awaited result.
|
|
576
|
+
|
|
567
577
|
**A create gets its id before the server answers.** Every row a `create_records` makes without
|
|
568
578
|
`ids` is drawn under a `rec_*` the SDK mints, and the run carries those ids to the server, which
|
|
569
579
|
stores each row under the one drawn for it — so the row drawn from the press is the row the server
|
package/docs/queries.md
CHANGED
|
@@ -477,7 +477,7 @@ derived surfaces share one implementation.
|
|
|
477
477
|
| select_member | `select_member` | select ops + `is_current_member`, `is_not_current_member` (field-scoped, no value; `is_not_current_member` matches every cell not holding the requester, an empty one included) | same — `is_current_member` **works at the runtime layer** too | `unnest` fans member ids. Anonymous public request: current-member binds the app owner (§1). |
|
|
478
478
|
| 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). |
|
|
479
479
|
| 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. |
|
|
480
|
-
| formula | its declared output type (`number`/`text`/`boolean`/`date`/`datetime`, or `select` for one stating its own options, its cell a single select's; `json` until inferred) | filtered by the output type's operators; `#ERROR
|
|
480
|
+
| formula | its declared output type (`number`/`text`/`boolean`/`date`/`datetime`, or `select` for one stating its own options, its cell a single select's; `json` until inferred) | filtered by the output type's operators; a cell holding an error (`#ERROR: …`) holds no value — it projects as `null` whatever the output type, and matches what an empty cell matches (`is_empty`, a negation such as `not_equals`) and nothing that needs a value | 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). |
|
|
481
481
|
| 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`. |
|
|
482
482
|
| 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). |
|
|
483
483
|
| 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. |
|
|
@@ -569,8 +569,10 @@ axes from ONE named query — don't shard into a query-per-combination. Mark eac
|
|
|
569
569
|
`required: false`; the server **prunes every filter condition (or traversal) whose
|
|
570
570
|
`{{params.x}}` the caller didn't pass** — then collapses emptied groups — so an unset axis
|
|
571
571
|
stops constraining instead of erroring. A param passed empty (`""`, `[]`, `null`) counts as not
|
|
572
|
-
passed
|
|
573
|
-
|
|
572
|
+
passed, and so does one that leaves a search with no word once trimmed and accent-folded (`" "`,
|
|
573
|
+
a lone accent): the condition or `from_table.search` it fills prunes. A REQUIRED param passed
|
|
574
|
+
either way is refused (400, naming the condition or the search it feeds — to a caller outside the
|
|
575
|
+
app's organization, the param alone).
|
|
574
576
|
|
|
575
577
|
```jsonc
|
|
576
578
|
"search": {
|
|
@@ -633,8 +635,9 @@ index that is:
|
|
|
633
635
|
- multi-term: the query splits on whitespace (max 10 terms), all terms must match (AND).
|
|
634
636
|
|
|
635
637
|
It AND-s with `filter` (search *within* a scope) and is templatable
|
|
636
|
-
(`"search": "{{params.q}}"`).
|
|
637
|
-
|
|
638
|
+
(`"search": "{{params.q}}"`). A term with no word once trimmed and accent-folded matches
|
|
639
|
+
everything: an optional param that leaves one, or is not passed, prunes the search to match-all;
|
|
640
|
+
a required one is refused (§6).
|
|
638
641
|
|
|
639
642
|
**Search-as-you-type over a LARGE table uses `search`, never a `contains` OR-group alone.** Per-field
|
|
640
643
|
`contains` is unindexed — a zero-match keystroke forces a full-table scan that hangs the
|
package/docs/runtime.md
CHANGED
|
@@ -78,7 +78,7 @@ returns a promise: the read stays loading until it settles, to review a pending
|
|
|
78
78
|
the fixture nothing, as it asks the server nothing. **Partial mocking is
|
|
79
79
|
supported**: an alias absent from the fixture still flows through the real
|
|
80
80
|
transport. `fixture.exports[alias]` answers `exportQuery(alias, call)` — a
|
|
81
|
-
function of the call returning
|
|
81
|
+
function of the call returning an `ExportedFile`, or a promise of one to review the
|
|
82
82
|
pending state. Calling `mount` again (HMR) replaces the registration last-write-wins.
|
|
83
83
|
|
|
84
84
|
- **Fixture rows stand in for wire rows.** They pass through the same code path
|
package/docs/workflows.md
CHANGED
|
@@ -245,7 +245,9 @@ validate({ checks: [{
|
|
|
245
245
|
|
|
246
246
|
`fail_when` rejects when truthy. A failing check ends the run as `status: "error"` with the
|
|
247
247
|
failing messages joined — cleanly, not as a crash: it is control flow, so **`try`/`catch` does
|
|
248
|
-
not catch it** (nor `return`).
|
|
248
|
+
not catch it** (nor `return`). A check that throws instead of answering — its `fail_when` or its
|
|
249
|
+
`message` — refuses the write too: the run fails with a message naming the step and the check's
|
|
250
|
+
index and nothing the check read, and `try`/`catch` does not catch that either.
|
|
249
251
|
|
|
250
252
|
`field_key` is optional, a string literal, and carries the same key `return({ field_errors })`
|
|
251
253
|
does: **the control the message belongs to, on the surface that renders it**. Each failing check
|
|
@@ -341,10 +343,11 @@ try {
|
|
|
341
343
|
```
|
|
342
344
|
|
|
343
345
|
`e` is shaped `{ message, type, step_id?, detail? }`. Caught: tool errors and expression errors
|
|
344
|
-
thrown inside the body. **Not caught:** `validate` failures
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
steps
|
|
346
|
+
thrown inside the body. **Not caught:** `validate` failures — a check that throws included — and
|
|
347
|
+
`return` (control flow), the run's own bounds (its time, its step count, and the node and 512 MB
|
|
348
|
+
bounds in *Traps*), and errors from steps that run on *resume* after a `wait` inside the body —
|
|
349
|
+
the wrapper spans one execution pass. There is no `finally`: put always-run steps after the `try`,
|
|
350
|
+
always-on-failure steps in the `catch`.
|
|
348
351
|
|
|
349
352
|
### Loops
|
|
350
353
|
|
|
@@ -819,6 +822,16 @@ The rules that are easy to get wrong because the failing code looks correct.
|
|
|
819
822
|
`includes(["member", "chat_agent", "api_client"], trigger.change_origin.type)`. Not
|
|
820
823
|
`runtime.change_origin`: that is the execution's own origin, always `table_workflow` here, so
|
|
821
824
|
it discriminates nothing.
|
|
825
|
+
- **Expressions are bounded.** A helper that builds an array — `range`, `list`, `flatten`,
|
|
826
|
+
`concat` of arrays, `split`'s parts — makes at most 100,000 elements, and a callback helper
|
|
827
|
+
takes an array of at most 100,000. `join`, `replaceAll`, `padStart`, `padEnd`, `toString`,
|
|
828
|
+
`toJson` and a template string write at most 10,000,000 characters; `randomNumber` /
|
|
829
|
+
`randomAlphaNumeric` take a length of at most 1,000. Past one of those the call throws an
|
|
830
|
+
expression error, which `try`/`catch` catches, and so does a step whose result would take what
|
|
831
|
+
the run keeps past 67,108,864 characters of JSON. One evaluation runs at most 20,000,000 nodes —
|
|
832
|
+
a callback nested in another runs once per pair of elements — and the run's expressions build at
|
|
833
|
+
most 512 MB of arrays, objects and text in all, every temporary counted: past either, the run
|
|
834
|
+
fails whatever catches it. Build long text with `join`, never a fold of `+`.
|
|
822
835
|
|
|
823
836
|
## The bright line
|
|
824
837
|
|
|
@@ -886,7 +899,10 @@ check's message joined with `"; "`, and one `field_errors` entry per failing che
|
|
|
886
899
|
**A rehearsal can stop early, and it says so in `evaluation_errors`.** Four bounds apply — 1 000
|
|
887
900
|
iterations of a `while` / `do_while` / `c_for`, 10 000 steps, 50 live reads, and 20 seconds — and
|
|
888
901
|
tripping any of them appends an entry naming the bound and ending "the plan below is incomplete",
|
|
889
|
-
then stops.
|
|
902
|
+
then stops. It also stops, with an entry saying why, where the live run would fail: at a
|
|
903
|
+
`validate` check that throws, an evaluation past the run's node or 512 MB bound (*Traps*), or a
|
|
904
|
+
return too long to write.
|
|
905
|
+
So read `evaluation_errors` before reading `planned_calls`: an entry there may be a
|
|
890
906
|
truncated plan rather than a bug in your body. A `foreach` is deliberately NOT capped at 1 000 —
|
|
891
907
|
its length is known before the first iteration, so the rehearsal walks every item and instead
|
|
892
908
|
refuses, up front, exactly the lists a live run refuses (over 10 000 items). The 50-live-read bound
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/app-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.112.0",
|
|
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": {
|