@lotics/app-sdk 0.114.0 → 0.115.1
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 +1016 -600
- package/dist/shared_types.d.ts +1 -1
- package/docs/members_and_options.md +3 -3
- package/docs/mutations.md +2 -3
- package/docs/queries.md +13 -6
- package/docs/workflows.md +13 -14
- package/package.json +1 -1
package/dist/shared_types.d.ts
CHANGED
|
@@ -64,7 +64,7 @@ export type ReportReadingHead = {
|
|
|
64
64
|
/** The read behind it was cut: its figures stand, every total over them is left out (`null`). */
|
|
65
65
|
truncated: boolean;
|
|
66
66
|
};
|
|
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; };
|
|
67
|
+
export type TableField = { key: string; name: string; description: string; type: "number"; format: "number" | "currency" | "percentage"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; notation?: { style: "decimal"; } | { style: "currency"; currency: string | { per_row: string; }; } | { style: "unit"; unit: string | { per_row: string; }; } | 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_notation?: { style: "decimal"; } | { style: "currency"; currency: string | { per_row: string; }; } | { style: "unit"; unit: string | { per_row: string; }; } | 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_notation?: { style: "decimal"; } | { style: "currency"; currency: string | { per_row: string; }; } | { style: "unit"; unit: string | { per_row: string; }; } | 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; lookup_field_autonumber?: boolean | 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; notation?: { style: "decimal"; } | { style: "currency"; currency: string | { per_row: string; }; } | { style: "unit"; unit: string | { per_row: string; }; } | undefined; link?: true | undefined; 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; };
|
|
68
68
|
export type TableRecordFilters = TableRecordFiltersConditionNode | TableRecordFiltersTraversalNode | TableRecordFiltersGroupNode;
|
|
69
69
|
export type TableRecordFiltersConditionNode = { node_type: "condition"; field_key?: string | undefined; type?: string | undefined; operator: string; value?: unknown; unit_option?: string | undefined; };
|
|
70
70
|
export type TableRecordFiltersGroupNode = { node_type: "group"; logic: "and" | "or"; children: (TableRecordFilters)[]; };
|
|
@@ -86,9 +86,9 @@ a compile error, and every key is optional. Each `FieldOptions`:
|
|
|
86
86
|
|
|
87
87
|
### A figure read in each row's own unit: `units`
|
|
88
88
|
|
|
89
|
-
A number whose field
|
|
90
|
-
|
|
91
|
-
that does not, and keeps only the rows in that unit (a measured unit's kin, `kg` beside `t`,
|
|
89
|
+
A number whose field's `notation` reads its unit or currency on each row (`{ "per_row": <select> }`)
|
|
90
|
+
reads each row in the option that select holds, so a comparison on it states which one as
|
|
91
|
+
`unit_option` — the server refuses one that does not, and keeps only the rows in that unit (a measured unit's kin, `kg` beside `t`,
|
|
92
92
|
converted). `units` is keyed like `fields`: `{ vocabulary: "unit" | "currency", options: { key, label }[] }`
|
|
93
93
|
per such column, whether or not the query projects the select. A range filter on such a column
|
|
94
94
|
offers its `options` and puts the picked key on each condition:
|
package/docs/mutations.md
CHANGED
|
@@ -397,9 +397,8 @@ Why a full-form snapshot save is a bug, not a style choice — three independent
|
|
|
397
397
|
moment *any* field is touched — there is no value-equality pass (deep equality over stored
|
|
398
398
|
shapes would be a silent lock bypass). A snapshot touches every field, so it turns every
|
|
399
399
|
save against a locked record into a total rejection even when nothing changed.
|
|
400
|
-
2.
|
|
401
|
-
|
|
402
|
-
record — a snapshot save fires them for fields the user never edited.
|
|
400
|
+
2. **A table's checks and `after_update` workflows run on the write.** They run on every
|
|
401
|
+
write that touches the record — a snapshot save runs them for fields the user never edited.
|
|
403
402
|
3. **Snapshots clobber concurrent edits.** An update writes exactly the fields present in the
|
|
404
403
|
payload; unsent fields keep their current value. A diff therefore coexists with a
|
|
405
404
|
colleague's concurrent edit to a *different* field — a snapshot overwrites it with the
|
package/docs/queries.md
CHANGED
|
@@ -264,6 +264,13 @@ heterogeneous tables. Source addressing survives per column only when every arm
|
|
|
264
264
|
source table/field; per-row `__src_field_*` metadata still tracks which field each row's cell
|
|
265
265
|
came from.
|
|
266
266
|
|
|
267
|
+
A select column whose arms project different selects takes every arm's options: a filter value
|
|
268
|
+
matches the arm whose select holds that key, and an option **name** stands for each arm's key
|
|
269
|
+
called that. A sort reads each row by its own select's order — arms listing the same options
|
|
270
|
+
interleave place by place, a key no arm names after every option. A `{ "literal": "<key>" }`
|
|
271
|
+
source typed `select` is an option of the query's own, held by every row of its arm: a column of
|
|
272
|
+
them (a book per arm) filters by those keys and sorts in the arms' order.
|
|
273
|
+
|
|
267
274
|
### `group` — aggregate
|
|
268
275
|
|
|
269
276
|
```jsonc
|
|
@@ -474,17 +481,17 @@ derived surfaces share one implementation.
|
|
|
474
481
|
|
|
475
482
|
| Field type | Column type | Source-layer operators | Derived-layer differences | Sort / group / aggregate notes |
|
|
476
483
|
| --- | --- | --- | --- | --- |
|
|
477
|
-
| text | `text` | `equals`, `not_equals`, `contains`, `does_not_contain`, `starts_with`, `ends_with`, `is_empty`, `is_not_empty`, `is_any_of`, `is_none_of`, `contains_any_of` | same set | Sort is lexicographic in the database's default collation — no locale control. Text comparisons normalize both sides (trim + case-insensitive). The LIKE family (`contains`, `does_not_contain`, `starts_with`, `ends_with`, and `contains_any_of`, whose value is a list and which holds when ANY listed word is in the cell) additionally folds **diacritics**, so `contains "ha noi"` matches `"Hà Nội"`; the identity operators (`equals`, `not_equals`, `is_any_of`, `is_none_of`) do **not** — they compare a stored value, and `mã` is not `ma`. |
|
|
478
|
-
| number | `number` | `equals`, `not_equals`, `greater_than`, `less_than`, `greater_than_or_equal_to`, `less_than_or_equal_to`, `is_empty`, `is_not_empty` | same set — except a figure read in each row's own unit or currency (`
|
|
484
|
+
| text | `text` | `equals`, `not_equals`, `contains`, `does_not_contain`, `starts_with`, `ends_with`, `is_empty`, `is_not_empty`, `is_any_of`, `is_none_of`, `contains_any_of` | same set | Sort is lexicographic in the database's default collation — no locale control — over the trimmed, case-folded text, a column the query derives included; the value read stays as written. Text comparisons normalize both sides (trim + case-insensitive). The LIKE family (`contains`, `does_not_contain`, `starts_with`, `ends_with`, and `contains_any_of`, whose value is a list and which holds when ANY listed word is in the cell) additionally folds **diacritics**, so `contains "ha noi"` matches `"Hà Nội"`; the identity operators (`equals`, `not_equals`, `is_any_of`, `is_none_of`) do **not** — they compare a stored value, and `mã` is not `ma`. |
|
|
485
|
+
| number | `number` | `equals`, `not_equals`, `greater_than`, `less_than`, `greater_than_or_equal_to`, `less_than_or_equal_to`, `is_empty`, `is_not_empty` | same set — except a figure read in each row's own unit or currency (a `notation` whose currency or unit is `per_row`, or a rollup totalling in its row's), which a comparison matches only at the source layer: a condition on a column passing it straight through moves there, anywhere else it is refused | Full numeric aggregate set (§8). A comparison on a figure read per row states `unit_option`, the key of the option of that select its value is in — `{ "field_key": "amount", "operator": "greater_than", "value": 100, "unit_option": "opt_usd" }` — and matches only rows holding that option; rows in a measured unit of the same dimension compare converted (`≥ 1` in `t` matches `1500` in `kg`), a counted unit or a currency never. One stating none is refused. |
|
|
479
486
|
| date / datetime | `date` / `datetime` (datetime when the field's format includes time) | `on`, `before`, `after`, `on_or_before`, `on_or_after`, `between`, `time_of_day`, `is_empty`, `is_not_empty` — values are `DateTimePoint`s (below) | same set | Day-level filters on datetime values expand to the full-day window; a `date` column compares on the day alone, so a time in the filter value is ignored. A reduced-precision value (`"2026-05"`) is its period start, and a value with no time is at `00:00` for `time_of_day`. Sortable; bucketable in `group.by` (§8); `earliest`/`latest`/`min`/`max`/`date_range` aggregate. |
|
|
480
487
|
| boolean | `boolean` | `equals` (value `true`/`false`; `false` matches NULL/missing) | same | `checked`/`unchecked`/`percent_*` aggregate (`unchecked` counts false **or** empty). |
|
|
481
|
-
| select (single & multi) | `select` | `has_any_of`, `has_none_of`, `has_all_of`, `is_empty`, `is_not_empty` — values are **option keys** (`opt_*`), never labels | the **runtime** `filter` (§9) also takes option **names**, resolving each to its key — an unknown one is refused rather than matching nothing, and the value stays an array either way; a select column the query DERIVES instead of projecting from a field carries no option set, so addressing one there is refused | Cells are arrays even for single-selects. Sorting a select column orders by its options' order (a multi-select by its first option), an empty cell last; a select the query derives carries no options and orders by raw JSON. `unnest` fans option keys. |
|
|
482
|
-
| 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). |
|
|
483
|
-
| 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
|
|
488
|
+
| select (single & multi) | `select` | `has_any_of`, `has_none_of`, `has_all_of`, `is_empty`, `is_not_empty` — values are **option keys** (`opt_*`), never labels | the **runtime** `filter` (§9) also takes option **names**, resolving each to its key — an unknown one is refused rather than matching nothing, and the value stays an array either way; a select column the query DERIVES instead of projecting from a field or a select literal carries no option set, so addressing one there is refused | Cells are arrays even for single-selects. Sorting a select column orders by its options' order (a multi-select by its first option), an empty cell last, on every page, keyset included; through a `union` whose arms project different selects, each row by its own select's order (§2 `union`); a select the query derives carries no options and orders by raw JSON. `unnest` fans option keys. |
|
|
489
|
+
| 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 | Sorting orders by the first member's name as a reader is shown it (the one the organization gave, else their profile's; a member it does not name reads as empty text), folded like text, then by member id; an empty cell last, on every page, keyset included. `unnest` fans member ids. Anonymous public request: current-member binds the app owner (§1). |
|
|
490
|
+
| 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 the first linked record's display, folded like text, then by its id; an empty cell last, on every page, keyset included. `unnest` fans link ids (+ display). |
|
|
484
491
|
| 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. |
|
|
485
492
|
| 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). |
|
|
486
493
|
| 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`. |
|
|
487
|
-
| 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
|
|
494
|
+
| 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**, and a sort orders by it as its column type sorts (a link's display, a member's name, a select's option order, text folded). 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). |
|
|
488
495
|
| 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. |
|
|
489
496
|
| button | `json` | **not filterable** — buttons are not data | — | Project/ignore; only presence-counting aggregates. |
|
|
490
497
|
|
package/docs/workflows.md
CHANGED
|
@@ -344,8 +344,8 @@ try {
|
|
|
344
344
|
|
|
345
345
|
`e` is shaped `{ message, type, step_id?, detail? }`. Caught: tool errors and expression errors
|
|
346
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
|
|
348
|
-
bounds in *Traps*), and errors from steps that run on *resume* after a `wait` inside the body —
|
|
347
|
+
`return` (control flow), the run's own bounds (its time, its step count, and the compute and
|
|
348
|
+
memory bounds in *Traps*), and errors from steps that run on *resume* after a `wait` inside the body —
|
|
349
349
|
the wrapper spans one execution pass. There is no `finally`: put always-run steps after the `try`,
|
|
350
350
|
always-on-failure steps in the `catch`.
|
|
351
351
|
|
|
@@ -611,7 +611,7 @@ key preserves. So passing a possibly-null read straight through is safe. Use
|
|
|
611
611
|
`set_skip_null` is what turns a bag of optional workflow inputs into a diff-write without a guard
|
|
612
612
|
per field; naming the same field in both `set` and `set_skip_null` is an error. A call whose
|
|
613
613
|
surfaces all resolve to nothing — an all-dropped `set_skip_null`, an op object with no entries —
|
|
614
|
-
is a no-op: no records touched, no
|
|
614
|
+
is a no-op: no records touched, no table check asked. So a conditionally-built op object needs
|
|
615
615
|
no guard around the call. Why diffs and not snapshots, plus locked records and the
|
|
616
616
|
`request_locked_record_change` path: [mutations](./mutations.md#diff-before-update--send-only-what-changed).
|
|
617
617
|
|
|
@@ -791,9 +791,6 @@ The rules that are easy to get wrong because the failing code looks correct.
|
|
|
791
791
|
init/update included. Resume re-enters *after* the whole loop, so a wait inside would run
|
|
792
792
|
iteration 0 and silently skip every remaining iteration and its mutations. Move the wait out,
|
|
793
793
|
or split the loop into separately triggered runs.
|
|
794
|
-
- **Waits and `agent` steps are disallowed in `before_*` table workflows.** Those run inside the
|
|
795
|
-
mutation pipeline and must complete synchronously; an LLM loop cannot return an accept/reject
|
|
796
|
-
verdict in time.
|
|
797
794
|
- **String contexts coerce to display text.** A read inside a template or a string-typed argument
|
|
798
795
|
renders the *label*; everywhere else it is the raw key. So
|
|
799
796
|
`` `Status: ${record["fld_status"]}` `` prints "Done" while
|
|
@@ -827,11 +824,14 @@ The rules that are easy to get wrong because the failing code looks correct.
|
|
|
827
824
|
takes an array of at most 100,000. `join`, `replaceAll`, `padStart`, `padEnd`, `toString`,
|
|
828
825
|
`toJson` and a template string write at most 10,000,000 characters; `randomNumber` /
|
|
829
826
|
`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
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
827
|
+
expression error, which `try`/`catch` catches, and so does an expression whose value would write
|
|
828
|
+
more than 67,108,864 characters of JSON, or a step whose result would take what the run keeps
|
|
829
|
+
past that. One evaluation computes for at most 6 seconds — waiting on a linked row, a member's
|
|
830
|
+
name or `current_member_in_any_group` does not count — and uses at most 256 MB of memory (512 MB
|
|
831
|
+
on a private instance with 4 GB or more). The values a run's expressions answer come to at most
|
|
832
|
+
512 MB in all, counted each time one is answered, a text at every place the value holds it.
|
|
833
|
+
Past any of these, the run fails whatever catches it. A callback nested in another runs once
|
|
834
|
+
per pair of elements.
|
|
835
835
|
|
|
836
836
|
## The bright line
|
|
837
837
|
|
|
@@ -860,8 +860,7 @@ genuinely recursive.
|
|
|
860
860
|
`set_app_workflow` with `verify_only: true` gives the verdict a save would, and writes nothing. The
|
|
861
861
|
server runs every check a save runs — the subset parse, the type pass against the alias's
|
|
862
862
|
declared inputs, tool and kwarg resolution, field, option, table and group resolution, `switch`
|
|
863
|
-
case validation, the literal `formatDate` probe, waits in loops, the `
|
|
864
|
-
table reach, the `field_key` rule and the published-API guard. **Green means the save will take
|
|
863
|
+
case validation, the literal `formatDate` probe, waits in loops, table reach, the `field_key` rule and the published-API guard. **Green means the save will take
|
|
865
864
|
it, red means real** — do not push through a red check. Lint comes back as warnings, which do not
|
|
866
865
|
fail it.
|
|
867
866
|
|
|
@@ -900,7 +899,7 @@ check's message joined with `"; "`, and one `field_errors` entry per failing che
|
|
|
900
899
|
iterations of a `while` / `do_while` / `c_for`, 10 000 steps, 50 live reads, and 20 seconds — and
|
|
901
900
|
tripping any of them appends an entry naming the bound and ending "the plan below is incomplete",
|
|
902
901
|
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
|
|
902
|
+
`validate` check that throws, an evaluation past the run's compute or memory bounds (*Traps*), or a
|
|
904
903
|
return too long to write.
|
|
905
904
|
So read `evaluation_errors` before reading `planned_calls`: an entry there may be a
|
|
906
905
|
truncated plan rather than a bug in your body. A `foreach` is deliberately NOT capped at 1 000 —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/app-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.115.1",
|
|
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": {
|