@lotics/cli 0.275.0 → 0.277.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/dist/src/cli.js +2368 -970
- package/docs/app_bindings.md +181 -0
- package/docs/data_model.md +174 -8
- package/docs/filters.md +114 -0
- package/docs/workflows.md +336 -0
- package/package.json +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# App bindings — the queries, workflows and agents an app calls
|
|
2
|
+
|
|
3
|
+
A custom-code app reaches workspace data through three kinds of binding, each declared on the app
|
|
4
|
+
under an alias — a JS identifier (`/^[a-zA-Z_$][a-zA-Z0-9_$]*$/`):
|
|
5
|
+
|
|
6
|
+
| Binding | Declared with | The app calls it with | An agent calls it with |
|
|
7
|
+
|---|---|---|---|
|
|
8
|
+
| Query | `set_app_query`, `set_app_queries` | `useQuery("<alias>", params?)` | `run_app_query` |
|
|
9
|
+
| Workflow | `set_app_workflow` | `useWorkflow("<alias>")({ ...inputs })` | `run_app_workflow` |
|
|
10
|
+
| Agent | `set_app_agent` | `useAgentRun("<alias>")({ ...inputs })` | — |
|
|
11
|
+
|
|
12
|
+
Each runs under the app's authority (`{ type: "app", app_id }`), the app owner's reach — never the
|
|
13
|
+
calling member's. Declaring one needs the app's owner or an admin. A write is live: the app's next
|
|
14
|
+
call uses it. `get_app_capabilities` lists an app's aliases with their params and inputs.
|
|
15
|
+
|
|
16
|
+
## Queries
|
|
17
|
+
|
|
18
|
+
A query declaration is `{ ast, params?, description?, templates? }`:
|
|
19
|
+
|
|
20
|
+
- `ast` — a query template (a QueryNode, below). It fixes the tables, filters and columns the alias
|
|
21
|
+
reads, so a caller cannot widen it.
|
|
22
|
+
- `params` — the typed value holes the caller fills, each an input declaration (see **Inputs**),
|
|
23
|
+
named in the ast as `{{params.<name>}}`. A param fills a VALUE position only — a filter value, a
|
|
24
|
+
`search` — never a `table_id` or a field key. Every token the ast names is declared. An optional
|
|
25
|
+
param left unset drops the conditions that read it; a required param that arrives empty is
|
|
26
|
+
refused where a filter reads it.
|
|
27
|
+
- `description` — one line saying what the query returns, for an agent choosing between aliases.
|
|
28
|
+
- `templates` — the document templates an export of its rows may fill, by name:
|
|
29
|
+
`{ <name>: { template_id: "dtl_…", rows: "<key the template lists the rows under>" } }`.
|
|
30
|
+
|
|
31
|
+
A save checks what a deploy checks: every table is in this workspace and within the owner's reach,
|
|
32
|
+
every projected, filtered and sorted field resolves, every param token is declared, and a key a
|
|
33
|
+
node's kind does not take (a stray `sort`, `limit`, `filter`, `search`) is refused by name.
|
|
34
|
+
|
|
35
|
+
`set_app_query` edits ONE alias and merges: send only the fields you change, `null` clears
|
|
36
|
+
`params`, `description` or `templates`, and a new alias needs an `ast`. `set_app_queries` writes
|
|
37
|
+
several at once: an alias it names is replaced whole, an alias it omits is kept.
|
|
38
|
+
`remove_app_query` deletes one; `get_app_query` reads one back.
|
|
39
|
+
|
|
40
|
+
### The query tree
|
|
41
|
+
|
|
42
|
+
Every node has a `kind`; all but `from_table` read the node under `from` (`join`: `left`/`right`,
|
|
43
|
+
`union`: `sources`).
|
|
44
|
+
|
|
45
|
+
| `kind` | Keys |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `from_table` | `table_id`, `filter?`, `search?`, `sort?`, `limit?` |
|
|
48
|
+
| `project` | `columns` — the output columns |
|
|
49
|
+
| `filter` | `predicate` — a filter tree over the input's columns |
|
|
50
|
+
| `join` | `left`, `right`, `on: { left_column, right_column }`, `type: "inner" \| "left"` |
|
|
51
|
+
| `union` | `sources` (at least two, columns aligned by name and type) |
|
|
52
|
+
| `group` | `by` (columns, or `{ bucket: { source, granularity, output } }`), `aggregates` |
|
|
53
|
+
| `window` | `partition_by`, `order_by`, `frame?`, `aggregates?`, `functions?` |
|
|
54
|
+
| `sort` | `by: [{ field_key, order }]` |
|
|
55
|
+
| `limit` | `n`, `offset?` or `keyset?` |
|
|
56
|
+
| `unpivot` | `passthrough`, `row_columns`, `rows` — one source row into several |
|
|
57
|
+
| `unnest` | `source`, `output`, `display_output?`, `keep_empty?` — one row per element of a multi-value cell |
|
|
58
|
+
|
|
59
|
+
Filters (`from_table.filter`, `filter.predicate`, an aggregate's `filter`) are the filter grammar — the
|
|
60
|
+
`filters` reference — over the input's columns, taking `{{params.x}}` as values. `search` matches
|
|
61
|
+
every text-bearing field of the row, accent- and case-insensitive.
|
|
62
|
+
|
|
63
|
+
A `project` column is a field key as a bare string (output name and type come from the field), or
|
|
64
|
+
`{ output?, type?, source, writable_target?, limit? }` to rename, compute or bound one. A computed
|
|
65
|
+
`source` is a field key, `{ literal }`, `{ eq: [a, b] }`, `{ neq: [a, b] }`, `{ isEmpty }`,
|
|
66
|
+
`{ isNotEmpty }`, `{ coalesce: [...] }`, `{ concat: [...] }`, `{ record_id: true }`,
|
|
67
|
+
`{ link: { source, field } }` (a field of the first linked record),
|
|
68
|
+
`{ link_agg: { source, field, operation } }` (`sum`, `avg`, `min`, `max`, `count`, `string_agg`
|
|
69
|
+
over every linked record), or `{ expression }`. A computed column states its `type`: `text`,
|
|
70
|
+
`number`, `boolean`, `date`, `datetime`, `select`, `select_record_link`, `select_member`, `files`
|
|
71
|
+
or `json`. `limit` (1–10) bounds a `files` column's entries per cell.
|
|
72
|
+
|
|
73
|
+
An aggregate column is `{ output, type, operation, input_column?, filter? }` — `string_agg` also
|
|
74
|
+
takes `separator`, `distinct` and `max_values`. A `window` function is
|
|
75
|
+
`{ output, fn }` for `row_number`, `rank`, `dense_rank`, `percent_rank`, `cume_dist`, plus
|
|
76
|
+
`buckets` for `ntile` and `input_column`, `offset?`, `default?` for `lag` / `lead`; functions need
|
|
77
|
+
a non-empty `order_by`.
|
|
78
|
+
|
|
79
|
+
## Inputs
|
|
80
|
+
|
|
81
|
+
A query's `params`, a workflow's `inputs` and an agent's `inputs` are one vocabulary: a map of
|
|
82
|
+
name → `{ type, required?, description?, …per type }`. An entry is required unless it states
|
|
83
|
+
`required: false`.
|
|
84
|
+
|
|
85
|
+
| `type` | Takes |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `text`, `number`, `boolean`, `date`, `datetime`, `email` | — |
|
|
88
|
+
| `record_link` | `table_id`; `multi?`; `max?` (with `multi`, the most ids one call carries) |
|
|
89
|
+
| `select` | exactly one of `options: [{ label, value }]` or `field: "fld_…"` (a select field whose CURRENT options back it); `multi?` |
|
|
90
|
+
| `date_range` | `include_time?` |
|
|
91
|
+
| `member` | `multi?`; `group?` (a member group the value must belong to) |
|
|
92
|
+
| `file` | `multi?` — an uploaded `fil_…` id, for a files field |
|
|
93
|
+
| `json` | — any value |
|
|
94
|
+
| `object` | `fields` — a nested map of entries |
|
|
95
|
+
| `array` | `items` — one entry |
|
|
96
|
+
|
|
97
|
+
Nesting stops at depth 8. A `field`-form select tracks the field's options as they change; an
|
|
98
|
+
inline `options` set is a fixed list.
|
|
99
|
+
|
|
100
|
+
## Outputs
|
|
101
|
+
|
|
102
|
+
A workflow's and an agent's `outputs` declare the structured data handed back: a map of
|
|
103
|
+
name → `{ type, required?, description?, …per type }`, every entry present unless it states
|
|
104
|
+
`required: false`.
|
|
105
|
+
|
|
106
|
+
| `type` | Takes |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `text`, `number`, `boolean`, `date`, `datetime`, `email`, `json` | — |
|
|
109
|
+
| `record_link` | `table_id`, `multi?` |
|
|
110
|
+
| `select` | exactly one of `options: [{ label, value }]` or `field: "fld_…"`; `multi?` |
|
|
111
|
+
| `object` | `fields` |
|
|
112
|
+
| `array` | `items` |
|
|
113
|
+
|
|
114
|
+
The returned value is checked against it, and the app reads it typed. An inline `options` set is
|
|
115
|
+
also enforced on the value returned, so the producer is told the legal keys; a `field`-form select
|
|
116
|
+
keeps the type tracking the live field.
|
|
117
|
+
|
|
118
|
+
## Workflows
|
|
119
|
+
|
|
120
|
+
`set_app_workflow` binds a workflow body to an alias: `{ app_id, alias, source, inputs?, outputs?,
|
|
121
|
+
name?, description? }`.
|
|
122
|
+
|
|
123
|
+
- `source` is the body with no `on({...})` trigger — the app is the trigger. It reads each declared
|
|
124
|
+
input as `trigger.app_workflow.inputs.<name>`; no record is in scope, so it reads each one by id
|
|
125
|
+
with `get_record`.
|
|
126
|
+
The body's steps are the `workflows` reference.
|
|
127
|
+
- `inputs` is the payload the call site passes (see **Inputs**). Omitted, an existing workflow keeps
|
|
128
|
+
its inputs; on a first bind it takes an untyped payload. `{}` clears them.
|
|
129
|
+
- `outputs` is what `return({ data })` hands back (see **Outputs**). Usually omit it: the type is
|
|
130
|
+
derived from the body's `return({ data })`, and the result echoes the bound `outputs`. Declared,
|
|
131
|
+
the body does not save unless its return matches.
|
|
132
|
+
- A save checks the body — parse, type-check against the declared inputs, names, lint, structure —
|
|
133
|
+
and answers with `[source/code] location: message` lines. `verify_only: true` makes every check
|
|
134
|
+
and writes nothing. A call naming a live alias replaces its body in place, keeping its run history.
|
|
135
|
+
- A call whose payload does not match `inputs` is refused before the body runs.
|
|
136
|
+
|
|
137
|
+
`get_app_workflow` reads the body back; `dry_run_workflow` with `trigger_type: "app_workflow"`
|
|
138
|
+
runs it against sample inputs before binding; `remove_app_workflow` unbinds it.
|
|
139
|
+
|
|
140
|
+
## Agents
|
|
141
|
+
|
|
142
|
+
`set_app_agent` binds a streaming tool-loop agent to an alias. A run streams its work to the app,
|
|
143
|
+
returns a typed result and keeps a run history per session. The declaration:
|
|
144
|
+
|
|
145
|
+
- `instructions` — the task, every run. A new alias needs it.
|
|
146
|
+
- `tool_names` — the tools it may call, from these and no other: `analyze_pdf_template`,
|
|
147
|
+
`code_edit_file`, `code_exec`, `code_read_file`, `code_write_file`, `excel_create_file`,
|
|
148
|
+
`excel_find_cells`, `excel_format_range`, `excel_get_range`, `excel_update_range`,
|
|
149
|
+
`generate_bank_qr_code`, `generate_excel_from_template`, `generate_image`,
|
|
150
|
+
`generate_pdf_from_template`, `generate_qr_code`, `generate_word_from_template`, `get_template`,
|
|
151
|
+
`grep_knowledge`, `list_banks`, `list_knowledge`, `lookup_business`, `query_templates`,
|
|
152
|
+
`read_knowledge`, `run_app_query`, `run_app_workflow`, `validate_excel_template`,
|
|
153
|
+
`vietcombank_convert_currency`, `view_files`, `word_create_document`, `word_find_text`,
|
|
154
|
+
`word_get_content`, `word_get_table_data`, `word_insert_conditional`, `word_insert_loop`,
|
|
155
|
+
`word_replace_text`. `[]` is a reasoning-only agent. A few fields of a document read well through
|
|
156
|
+
the `excel_*` / `word_*` tools; a cross-check, reconciliation or transform a decision rests on
|
|
157
|
+
belongs in the code tools.
|
|
158
|
+
- `query_aliases` / `workflow_aliases` — the app's own queries and workflows it may call through
|
|
159
|
+
`run_app_query` / `run_app_workflow`. They are its whole reach over records: no agent tool takes a
|
|
160
|
+
`table_id`, so a read is bounded by the query's tables, rows and columns, and a write goes through
|
|
161
|
+
the app's own workflow.
|
|
162
|
+
- `knowledge_doc_ids` — knowledge docs it may read with `grep_knowledge` / `read_knowledge`
|
|
163
|
+
(declare them in `tool_names`). Each must be one the app owner and you can use. Nothing is
|
|
164
|
+
inlined and size does not matter. The list is the agent's whole reach — `list_knowledge` finds no
|
|
165
|
+
doc outside it — so name a doc's id in `instructions` to point it there. `code_exec` computes
|
|
166
|
+
across a corpus (counting, cross-referencing); reading needs only the knowledge tools.
|
|
167
|
+
- `model_tier` — `haiku`, `sonnet` or `opus`. Omitted, the run follows the platform's default tier
|
|
168
|
+
and moves with new models; pin one only as a tested choice, and never `haiku`, a utility tier.
|
|
169
|
+
- `effort_level` — `low`, `medium`, `high`, `xhigh` or `max`, one the pinned tier supports; it
|
|
170
|
+
needs `model_tier`.
|
|
171
|
+
- `prefix_cache_ttl` — `5m` (the default) or `1h`. `1h` doubles the cache write price and pays only
|
|
172
|
+
when runs land 5 to 60 minutes apart, as a scheduled sweep does.
|
|
173
|
+
- `inputs` — the per-run payload (see **Inputs**); omitted, the payload is untyped.
|
|
174
|
+
- `outputs` — the structured result (see **Outputs**), checked before the run is saved and typed
|
|
175
|
+
as `run.output` in the app; omitted, the result is the final message.
|
|
176
|
+
- `writes` — where the outputs land: `{ table_id, row, fields }`, `row` naming a single
|
|
177
|
+
`record_link` input to `table_id` and `fields` mapping each output name to a field key of that
|
|
178
|
+
table. A result that validates is written to that row under the app's authority.
|
|
179
|
+
|
|
180
|
+
Send only what you change: an omitted field keeps its stored value, `null` clears an optional one.
|
|
181
|
+
`get_app_agent` reads one back; `remove_app_agent` deletes it.
|
package/docs/data_model.md
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
|
-
# The data model — how
|
|
1
|
+
# The data model — tables, their fields, and how they relate
|
|
2
2
|
|
|
3
3
|
The decisions here outlive any one app, and most become expensive the moment a second screen depends
|
|
4
|
-
on them. They
|
|
5
|
-
|
|
4
|
+
on them. They stand apart from building an app on purpose: **every workspace starts with tables
|
|
5
|
+
and many never get an app**, so schema design is not a chapter of app building.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
The rules below share one signature. **Both sides read correctly on their own**, so nothing reports
|
|
8
|
+
the problem — no error, no empty column, no failing query. Each is found by looking for it. Then,
|
|
9
|
+
under **Fields**, what each field type takes.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
## One fact, one column
|
|
12
|
+
|
|
13
|
+
**Read the table's existing fields first, and add nothing that stores a fact the table already
|
|
14
|
+
stores.** A value written as text and the same value held as a `select_record_link` are one fact in
|
|
15
|
+
two columns — a customer's city typed into a text box beside a link to the city record, a status
|
|
16
|
+
word beside the select that decides it, a total beside the formula that computes it.
|
|
17
|
+
|
|
18
|
+
Two columns for one fact do not stay equal. Some writer sets only one of them, and nothing reports
|
|
19
|
+
the divergence: both rows still look correct on their own. A reader that then matches on the text
|
|
20
|
+
half treats "Acme" and "Acme Ltd" as different records, so an import creates a duplicate every time
|
|
21
|
+
it runs.
|
|
22
|
+
|
|
23
|
+
- **Prefer the link, the select, or the formula.** Text is a RENDERING of a record; compose it when
|
|
24
|
+
you read, rather than storing it a second time.
|
|
25
|
+
- **Renaming, retyping or re-pointing the existing field beats adding another.** A field's key is
|
|
26
|
+
stable, so a rename breaks nothing that addresses it by key.
|
|
27
|
+
- **Superseding a field means DELETING it**, not leaving it beside its replacement with a
|
|
28
|
+
description that says which one is real.
|
|
29
|
+
- **Empty is not the same as redundant.** A field nothing fills may still be the only home for a
|
|
30
|
+
real distinction — read what it MEANS before removing it.
|
|
13
31
|
|
|
14
32
|
## One entity, one table — and the test is measurable
|
|
15
33
|
|
|
@@ -69,6 +87,16 @@ it falls back to comparing displayed text — which is how one company arrives t
|
|
|
69
87
|
spellings. Name the key: a reference number, a tax id, a link plus a period. Then match on `rec_…`
|
|
70
88
|
and `opt_…`, never on rendered labels.
|
|
71
89
|
|
|
90
|
+
## A state's HISTORY is rows, not columns
|
|
91
|
+
|
|
92
|
+
A `changed at` column says only how long a row has been where it is now — the next move overwrites
|
|
93
|
+
it — and a date column per state holds until something re-enters a state it already left.
|
|
94
|
+
|
|
95
|
+
Measuring time-in-state or conversion needs one ROW per move: a link to the subject, the state left,
|
|
96
|
+
the state entered, when. Hold those states as the source field's own `opt_` keys so the log carries
|
|
97
|
+
no second vocabulary, and write the rows from that table's own lifecycle workflows, which covers
|
|
98
|
+
every writer rather than one app's.
|
|
99
|
+
|
|
72
100
|
## Keep derived chains shallow
|
|
73
101
|
|
|
74
102
|
Formulas and rollups are computed and STORED when a row is written, and one that reads another
|
|
@@ -90,3 +118,141 @@ the numbers themselves:
|
|
|
90
118
|
And when a formula gains a new field, **test that the value IS the one you want, never that it
|
|
91
119
|
differs from it** — an empty cell reads as `""`, which differs from every option key, so the inverted
|
|
92
120
|
spelling silently zeroes every row written before the field existed.
|
|
121
|
+
|
|
122
|
+
## Fields
|
|
123
|
+
|
|
124
|
+
What `create_table` and `update_table` take in `add_fields`, and `update_table` in `update_fields`.
|
|
125
|
+
|
|
126
|
+
### Types and formats
|
|
127
|
+
|
|
128
|
+
`type` is one of `text`, `number`, `date`, `boolean`, `select`, `select_member`,
|
|
129
|
+
`select_record_link`, `files`, `formula`, `rollup`, `lookup`, `autonumber`. `button` is retired: an
|
|
130
|
+
existing button field keeps running, but none is created, converted to or edited.
|
|
131
|
+
|
|
132
|
+
A URL, an email, markdown, a checkbox, a datetime, a currency or a percentage is a `format` on
|
|
133
|
+
another type, never a `type`:
|
|
134
|
+
|
|
135
|
+
| Wanted | Field |
|
|
136
|
+
|---|---|
|
|
137
|
+
| URL or external link | `{ type: "text", format: "link" }` |
|
|
138
|
+
| Email or phone | `{ type: "text" }` |
|
|
139
|
+
| Markdown | `{ type: "text", format: "markdown" }` |
|
|
140
|
+
| Checkbox | `{ type: "boolean" }` |
|
|
141
|
+
| Money | `{ type: "number", format: "currency", currency: "USD" }` |
|
|
142
|
+
| Percentage | `{ type: "number", format: "percentage" }` |
|
|
143
|
+
| Datetime | `{ type: "date", format: "datetime" }` |
|
|
144
|
+
| Date range | `{ type: "date", format: "date_range" }` |
|
|
145
|
+
|
|
146
|
+
### Properties
|
|
147
|
+
|
|
148
|
+
A type's properties sit **directly on the field object** — there is no `config` wrapper. A property
|
|
149
|
+
may itself hold an object (`formula`, `aggregate_option`, `filter`, `order_by`); its inner keys stay
|
|
150
|
+
inside it. Each type takes only its own:
|
|
151
|
+
|
|
152
|
+
- **text** — `format?` (`"text"` | `"link"` | `"markdown"`), `unique?`, `default_value?` (a string).
|
|
153
|
+
- **number** — `format?` (`"number"` | `"currency"` | `"percentage"`), `currency?` (an ISO 4217
|
|
154
|
+
code), `unit?`, `unit_field?`, `currency_field?`, `default_value?` (a number). On `update_fields`,
|
|
155
|
+
null clears `currency`, `unit`, `unit_field` or `currency_field`.
|
|
156
|
+
- `unit`, beside format `"number"`: a measured code — g, kg, t, l, m3, cbm, mm, cm, m, km, m2,
|
|
157
|
+
min, h, day — or a counted noun such as `kiện`. A change between two units of one dimension
|
|
158
|
+
converts every stored figure; any other change relabels.
|
|
159
|
+
- `unit_field`: A single select on the same row, or a lookup of one through a one-link, whose chosen option is that row's unit: every option label is a unit as `unit` takes one. In place of `unit`; only beside format "number".
|
|
160
|
+
- `currency_field`: A single select on the same row, or a lookup of one through a one-link, whose chosen option is that row's currency: every option label is an ISO 4217 code. In place of `currency`; only beside format "currency".
|
|
161
|
+
- **date** — `format?` (`"date"` | `"datetime"` | `"date_range"` | `"datetime_range"`), `timezone?`,
|
|
162
|
+
`derive_from?`, `default_value?` (a date string). `derive_from: "created_at"` stamps the row's
|
|
163
|
+
creation once, `"updated_at"` re-stamps on every update; the field is then read-only, takes no
|
|
164
|
+
`default_value`, and holds only the `date` and `datetime` formats.
|
|
165
|
+
- **boolean** — `default_value?` (`true` | `false`).
|
|
166
|
+
- **select** — `options` `[{ name, color?, mark? }]`, `multi?`, `default_value?`.
|
|
167
|
+
- **select_member** — `multi?`, `default_value?` (member ids).
|
|
168
|
+
- **select_record_link** — `table_id`, `display_field_keys?`, `sync_both_ways?`, `cardinality?`,
|
|
169
|
+
`paired_field_name?`, `paired_field_display_field_keys?`. Without `display_field_keys` a link shows
|
|
170
|
+
the target's autonumber, else its first text field. `sync_both_ways: true` on an existing one-way
|
|
171
|
+
link makes it two-way — never add a second link for that. `cardinality: "one"` marks the child
|
|
172
|
+
side of a parent-child pair (the linked record is this one's parent); two paired sides cannot both
|
|
173
|
+
be `"one"`. The `paired_*` keys name and label the back-reference the target gets.
|
|
174
|
+
- **files** — no properties.
|
|
175
|
+
- **autonumber** — `template` (`"INV-{YEAR}-{N:4}"`; tokens `{N}`, `{N:W}` zero-padded to W,
|
|
176
|
+
`{YEAR}`, `{YEAR:2}`, `{MONTH}`, `{DAY}`), or `prefix` + `padding` (1–20) for `PREFIX-0001`.
|
|
177
|
+
- **any type**, at create — `confirm_before_update?`: a person confirms each later edit.
|
|
178
|
+
|
|
179
|
+
`default_value` fills the field on a NEW record that states no value; existing records are never
|
|
180
|
+
backfilled, and null clears it. A select's or a member field's default is an ARRAY even when the
|
|
181
|
+
field holds one (`["Unread"]`, never `"Unread"`) — option names in `add_fields`, where the keys do
|
|
182
|
+
not exist yet, and option keys (`opt_…`) in `update_fields`.
|
|
183
|
+
|
|
184
|
+
### Computed fields
|
|
185
|
+
|
|
186
|
+
Read-only in records. `formula` is one key holding an object; a rollup's and a lookup's properties
|
|
187
|
+
are separate keys on the field — `rollup: {…}` and `lookup: {…}` are refused as unrecognized.
|
|
188
|
+
|
|
189
|
+
formula: { expression, format?, currency?, unit?, unit_field?, currency_field? }
|
|
190
|
+
|
|
191
|
+
- `{Field Name}` or `{fld_key}` names a field of the same table, stored as its key, so a rename never
|
|
192
|
+
breaks the formula. A reference to no field, or a call to something that is no helper, is refused.
|
|
193
|
+
- A select reads as an ARRAY of option keys: `{Status}[0]` for a single select,
|
|
194
|
+
`includes({Tags}, "opt_…")` for a multi.
|
|
195
|
+
- The operators, the helpers and how an empty cell reads are the `model` reference, section `formula` — a model
|
|
196
|
+
names fields by alias where a table tool names them by name or key; the language is the same.
|
|
197
|
+
- On `update_fields` a key left out keeps its stored value, and null clears `currency`, `unit`,
|
|
198
|
+
`unit_field` or `currency_field`. `get_table` marks a formula that is null when every field it
|
|
199
|
+
reads is empty.
|
|
200
|
+
|
|
201
|
+
rollup — `source_field_key`, `aggregate_option: { field_key?, operation }`, `filter?`
|
|
202
|
+
|
|
203
|
+
- `source_field_key` is a `select_record_link` of this table; `aggregate_option.field_key` is a
|
|
204
|
+
field of the linked table, required by every operation but `count`, which counts linked records.
|
|
205
|
+
- Operations by the aggregated field's type — none: `count`; any: `empty`, `filled`,
|
|
206
|
+
`percent_empty`, `percent_filled`, `unique`, `percent_unique`; number: `sum`, `avg`, `median`,
|
|
207
|
+
`min`, `max`, `range`; date: `earliest`, `latest`, `date_range`.
|
|
208
|
+
- The cell's type comes from the OPERATION: `earliest` and `latest` hold a date, every other
|
|
209
|
+
operation a number (`date_range` counts days). A "most recent linked date" is `latest` — `min` and
|
|
210
|
+
`max` are numeric and refuse a date. A rollup takes no format, currency or unit of its own: `sum`
|
|
211
|
+
over money carries the aggregated field's currency, over a weight its unit.
|
|
212
|
+
- Over a figure read in each row's own unit or currency (`unit_field` / `currency_field`), `sum`,
|
|
213
|
+
`avg`, `median`, `min`, `max` and `range` hold only where that select is a lookup, through the
|
|
214
|
+
link paired with `source_field_key`, of a single select on this table — the total reads in this
|
|
215
|
+
row's option of it. A lookup of such a field has no unit.
|
|
216
|
+
- `filter` aggregates only the linked records it matches: a full group over the LINKED table's
|
|
217
|
+
fields — `{ node_type: "group", logic, children: [{ node_type: "condition", type, field_key,
|
|
218
|
+
operator, value }] }`, a select condition using `has_any_of` with an ARRAY value.
|
|
219
|
+
|
|
220
|
+
lookup — `source_field_key`, `lookup_field_key`, `order_by?`
|
|
221
|
+
|
|
222
|
+
- `lookup_field_key` is a field of the linked table. Without `order_by` the cell holds every linked
|
|
223
|
+
record's value; with `order_by: { field_key, direction }` (`desc` latest, `asc` earliest) it holds
|
|
224
|
+
the picked field of the single extreme record — a "latest linked X" that maintains itself.
|
|
225
|
+
Ordered lookups sharing one `order_by` resolve to the SAME record.
|
|
226
|
+
|
|
227
|
+
### Examples
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
{ name: "Giá bán", type: "number", format: "currency", currency: "VND" }
|
|
231
|
+
{ name: "Trọng lượng", type: "number", unit: "kg" }
|
|
232
|
+
{ name: "Số lượng", type: "number", unit_field: "ĐVT" }
|
|
233
|
+
{ name: "Trạng thái", type: "select", options: [{ name: "Mới" }, { name: "Xong" }] }
|
|
234
|
+
{ name: "Mã đơn", type: "autonumber", template: "SR-{YEAR}-{N:4}" }
|
|
235
|
+
{ name: "Khách hàng", type: "select_record_link", table_id: "tbl_x", sync_both_ways: true }
|
|
236
|
+
{ name: "Total", type: "formula", formula: { expression: "{Price} * {Qty}", format: "currency", currency: "VND" } }
|
|
237
|
+
{ name: "SL đã giao", type: "rollup", source_field_key: "Giao hàng", aggregate_option: { operation: "sum", field_key: "Số lượng" } }
|
|
238
|
+
{ name: "Customer Name", type: "lookup", source_field_key: "Customer", lookup_field_key: "Name" }
|
|
239
|
+
{ name: "Latest note", type: "lookup", source_field_key: "Calls", lookup_field_key: "Note", order_by: { field_key: "At", direction: "desc" } }
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Changing a field
|
|
243
|
+
|
|
244
|
+
A field added by `update_table` shows in a view only when `add_to_views` names it, at the view's far
|
|
245
|
+
right; `update_view` with `move_field` places it beside the columns it belongs with.
|
|
246
|
+
|
|
247
|
+
An `update_fields` entry is `{ field_key, name?, description?, convert_to?, … }` with the same flat
|
|
248
|
+
properties as `add_fields`, plus what only an update does:
|
|
249
|
+
|
|
250
|
+
- A select's options change through `add_options` `[{ name, color?, mark? }]`, `update_options`
|
|
251
|
+
`[{ key, name?, color?, mark? }]`, `remove_option_keys` and `reorder_options` (every existing
|
|
252
|
+
option once, in order; options added in the same call follow). An option is named by its `opt_…`
|
|
253
|
+
key (its name also resolves); a rename keeps every record's value. `mark` is the option's brand
|
|
254
|
+
(`{ kind: "brand", name: "tiktok" }`) or kit glyph (`{ kind: "icon", name: "wrench" }`), drawn in
|
|
255
|
+
place of its colour dot — every option of a field has one or none does, so marking sets them all
|
|
256
|
+
in one call and `mark: null` on each clears them.
|
|
257
|
+
- A link's `sync_both_ways: false` disconnects the pair; a new `table_id` re-points it and clears its
|
|
258
|
+
record data; `cardinality` is `"one"` for the child side of a parent-child pair, `"many"` for peers.
|
package/docs/filters.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Filters — choosing records by their fields
|
|
2
|
+
|
|
3
|
+
One grammar for every tool that takes a filter: `query_records`, `aggregate_records`, `update_records`
|
|
4
|
+
and `delete_records` by filter, a view's filter, a rollup's filter, and the filters a caller passes
|
|
5
|
+
to an app's query.
|
|
6
|
+
|
|
7
|
+
## The shape
|
|
8
|
+
|
|
9
|
+
A filter is a single condition, or a group of conditions.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
{ "node_type": "condition", "field_key": "Status", "operator": "has_any_of", "value": ["Done"] }
|
|
13
|
+
{ "node_type": "group", "logic": "and" | "or", "children": [ …conditions or groups ] }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Groups nest up to 4 levels. AND(OR(status = A, status = B), age > 40):
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
{ "node_type": "group", "logic": "and", "children": [
|
|
20
|
+
{ "node_type": "group", "logic": "or", "children": [cond1, cond2] },
|
|
21
|
+
cond3
|
|
22
|
+
] }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Every `field_key` resolves against the table being filtered — read each table's fields (`get_table`)
|
|
26
|
+
and carry no name across tables: sibling tables holding the same concept routinely name it
|
|
27
|
+
differently. A field is named by its `fld_…` key or by its name (`"Status"`).
|
|
28
|
+
|
|
29
|
+
## Text
|
|
30
|
+
|
|
31
|
+
| Operators | Value |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `equals`, `not_equals`, `contains`, `does_not_contain`, `starts_with`, `ends_with` | a string |
|
|
34
|
+
| `is_any_of`, `is_none_of` | a string array — exact, case-insensitive |
|
|
35
|
+
| `contains_any_of` | a string array — the cell contains any of them, case- and accent-insensitive |
|
|
36
|
+
| `is_empty`, `is_not_empty` | none |
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
{ "node_type": "condition", "field_key": "Title", "operator": "is_any_of", "value": ["hello", "world"] }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A text value is a string, never an array; `is_any_of` matches several. Its array has no length cap —
|
|
43
|
+
pass the whole list in one condition rather than splitting it across calls.
|
|
44
|
+
|
|
45
|
+
## Number
|
|
46
|
+
|
|
47
|
+
| Operators | Value |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `equals`, `not_equals`, `greater_than`, `less_than`, `greater_than_or_equal_to`, `less_than_or_equal_to` | a number |
|
|
50
|
+
| `is_empty`, `is_not_empty` | none |
|
|
51
|
+
|
|
52
|
+
A number whose field states `unit_field` or `currency_field` is read in each row's own unit or
|
|
53
|
+
currency, so a comparison on it states `unit_option`: the option of that select its value is in.
|
|
54
|
+
Rows holding another option are out; measured units of one dimension convert.
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
{ "node_type": "condition", "field_key": "Amount", "operator": "greater_than", "value": 100, "unit_option": "USD" }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Boolean
|
|
61
|
+
|
|
62
|
+
`equals` with `true` or `false`.
|
|
63
|
+
|
|
64
|
+
## Select
|
|
65
|
+
|
|
66
|
+
| Operators | Value |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `has_any_of` (any of them — "A or B"), `has_none_of`, `has_all_of` (a multi-select only) | an ARRAY of option names, even for one |
|
|
69
|
+
| `is_empty`, `is_not_empty` | none |
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
{ "node_type": "condition", "field_key": "Status", "operator": "has_any_of", "value": ["Done", "In Progress"] }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Member
|
|
76
|
+
|
|
77
|
+
| Operators | Value |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `has_any_of`, `has_none_of`, `has_all_of` | an array of member ids |
|
|
80
|
+
| `is_current_member`, `is_not_current_member`, `is_empty`, `is_not_empty` | none |
|
|
81
|
+
|
|
82
|
+
## Date
|
|
83
|
+
|
|
84
|
+
| Operators | Value |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `before`, `after`, `on_or_before`, `on_or_after`, `on`, `starts_before`, `starts_after`, `ends_before`, `ends_after` | a point |
|
|
87
|
+
| `between`, `overlaps`, `contains`, `within` | `{ start: point \| null, end: point \| null }` |
|
|
88
|
+
| `time_of_day` | `{ start_time: "HH:mm" \| null, end_time: "HH:mm" \| null }` |
|
|
89
|
+
| `duration_equals`, `duration_greater_than`, `duration_less_than` | `{ amount, unit: "minutes" \| "hours" \| "days" }` |
|
|
90
|
+
| `is_empty`, `is_not_empty` | none |
|
|
91
|
+
|
|
92
|
+
A point is one of:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
{ "type": "exact", "date": "2025-03-14", "time": "09:00" } time optional
|
|
96
|
+
{ "type": "relative", "offset": -7, "unit": "minutes" | "hours" | "days" | "weeks" | "months" | "years" }
|
|
97
|
+
{ "type": "period", "period": "day" | "week" | "month" | "quarter" | "year", "boundary": "start" | "end", "offset": 0 }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Link
|
|
101
|
+
|
|
102
|
+
| Operators | Value |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `has_any_of` (linked to any — filters by record identity), `has_none_of`, `has_all_of` (a multi-link only) | an array of record ids (`rec_…`) |
|
|
105
|
+
| `contains`, `not_contains`, `starts_with`, `ends_with` | a string, matched against the linked record's display text |
|
|
106
|
+
| `is_empty`, `is_not_empty` | none |
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
{ "node_type": "condition", "field_key": "Customer", "operator": "has_any_of", "value": ["rec_abc123"] }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Sort
|
|
113
|
+
|
|
114
|
+
A view's sort is a list, applied in order: `[{ "field_key": "Due", "order": "asc" | "desc" }]`.
|