@lotics/cli 0.289.0 → 0.291.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/src/cli.js +3635 -3384
- package/dist/src/client.js +26 -10
- package/docs/cli_reference.md +1 -1
- package/docs/data_model.md +31 -23
- package/docs/examples.md +12 -11
- package/docs/filters.md +3 -2
- package/docs/workflows.md +76 -37
- package/package.json +1 -1
package/dist/src/client.js
CHANGED
|
@@ -62,6 +62,9 @@ async function uploadParts(options) {
|
|
|
62
62
|
var CLI_CAPABILITIES_HEADER = "x-lotics-cli-capabilities";
|
|
63
63
|
var CLI_CAPABILITIES = ["docs-fallback"];
|
|
64
64
|
|
|
65
|
+
// ../shared/src/proxy_upload_limit.ts
|
|
66
|
+
var PROXY_UPLOAD_MAX_BYTES = 100 * 1024 * 1024;
|
|
67
|
+
|
|
65
68
|
// src/client.ts
|
|
66
69
|
import crypto from "node:crypto";
|
|
67
70
|
import fs from "node:fs";
|
|
@@ -126,6 +129,7 @@ function getMimeType(filename) {
|
|
|
126
129
|
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
127
130
|
}
|
|
128
131
|
var MULTIPART_THRESHOLD_BYTES = 8 * 1024 * 1024;
|
|
132
|
+
var PROXY_BATCH_MAX_BYTES = PROXY_UPLOAD_MAX_BYTES / 2;
|
|
129
133
|
var LoticsRequestError = class extends Error {
|
|
130
134
|
constructor(message, status, body) {
|
|
131
135
|
super(message);
|
|
@@ -578,25 +582,37 @@ var LoticsClient = class {
|
|
|
578
582
|
);
|
|
579
583
|
}
|
|
580
584
|
async uploadFiles(filePaths, options) {
|
|
581
|
-
const
|
|
585
|
+
const files = [];
|
|
586
|
+
const errors = [];
|
|
582
587
|
const large = [];
|
|
588
|
+
let batch = [];
|
|
589
|
+
let batchBytes = 0;
|
|
590
|
+
const sendBatch = async () => {
|
|
591
|
+
const sent = batch;
|
|
592
|
+
batch = [];
|
|
593
|
+
batchBytes = 0;
|
|
594
|
+
try {
|
|
595
|
+
const result = await this.uploadFileBytes(sent);
|
|
596
|
+
files.push(...result.files);
|
|
597
|
+
errors.push(...result.errors);
|
|
598
|
+
} catch (error) {
|
|
599
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
600
|
+
errors.push(...sent.map((item) => ({ filename: item.filename, error: message })));
|
|
601
|
+
}
|
|
602
|
+
};
|
|
583
603
|
for (let i = 0; i < filePaths.length; i++) {
|
|
584
604
|
const absolutePath = path.resolve(filePaths[i]);
|
|
585
605
|
const filename = options?.filenames?.[i] ?? path.basename(absolutePath);
|
|
586
606
|
const { size } = await fs.promises.stat(absolutePath);
|
|
587
607
|
if (size >= MULTIPART_THRESHOLD_BYTES) {
|
|
588
608
|
large.push({ absolutePath, filename, size });
|
|
589
|
-
|
|
590
|
-
small.push({ bytes: await fs.promises.readFile(absolutePath), filename });
|
|
609
|
+
continue;
|
|
591
610
|
}
|
|
611
|
+
if (batch.length > 0 && batchBytes + size > PROXY_BATCH_MAX_BYTES) await sendBatch();
|
|
612
|
+
batch.push({ bytes: await fs.promises.readFile(absolutePath), filename });
|
|
613
|
+
batchBytes += size;
|
|
592
614
|
}
|
|
593
|
-
|
|
594
|
-
const errors = [];
|
|
595
|
-
if (small.length > 0) {
|
|
596
|
-
const result = await this.uploadFileBytes(small);
|
|
597
|
-
files.push(...result.files);
|
|
598
|
-
errors.push(...result.errors);
|
|
599
|
-
}
|
|
615
|
+
if (batch.length > 0) await sendBatch();
|
|
600
616
|
for (const item of large) {
|
|
601
617
|
try {
|
|
602
618
|
files.push(await this.uploadLargeFile(item));
|
package/docs/cli_reference.md
CHANGED
|
@@ -31,7 +31,7 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
31
31
|
| — | **The exit code reports the WORK, not just the call — for the two tools that RUN one.** `run_app_workflow` and `run_app_agent` whose envelope carries a failed `status` (`error`/`failed`/`cancelled`) exit non-zero and print `<tool> → <status>: <message>` to stderr, so `lotics run … && next-step` cannot walk past a refused run. The rule is an allowlist of FAILURE — an unrecognized status exits 0, so a status added later never turns a working script red. A parked run (`awaiting_input`) is not a failure: it is waiting for an answer and the work is still live. Only a TOP-LEVEL `status` counts; one inside the data belongs to the data. Any OTHER tool's `status` is data, and exits 0. |
|
|
32
32
|
| `lotics run <tool> --print-created` | Report the records the call created, grouped by table, with a paste-ready `delete_records` per table and the mandatory caveat naming what cannot be auto-undone (external integrations, notifications, possible sub-workflows). Works for any tool that returns a `side_effects` block, not workflows alone. |
|
|
33
33
|
| `lotics run <tool> --cleanup` | Implies `--print-created`, then runs those deletes — harvested records **only**, never files / external calls / notifications. **Not a rollback**; a rollback is structurally impossible here. A partial cleanup exits non-zero so a script cannot read it as success. |
|
|
34
|
-
| `lotics file upload <file\|dir...>` (alias `lotics upload`) · `--stdin` · `--base64` · `--url <url>` | Upload files/directories. **The transport is chosen by size and is not a flag**: under 8 MiB the file is POSTed to `/v1/files
|
|
34
|
+
| `lotics file upload <file\|dir...>` (alias `lotics upload`) · `--stdin` · `--base64` · `--url <url>` | Upload files/directories. **The transport is chosen by size and is not a flag**: under 8 MiB the file is POSTed to `/v1/files`, several such files to a request up to half that route's 100 MiB cap; at or above it the CLI takes presigned part URLs and PUTs the bytes straight to object storage, so they never pass through the API. That threshold matches the AWS CLI's own `multipart_threshold`, and the number matters less than there being nothing to choose — one verb, any size, up to the 2 GiB a workspace may store. A large upload reads one part at a time, so memory stays flat regardless of file size, and a failure part-way abandons the parts already sent rather than leaving them billable and invisible. A directory expands to its immediate files; `--as <name>` renames a single upload. **Three alternative byte sources, for a caller that never had the bytes on disk** — an attachment decoded in memory, a generated document, a signed download link — each mutually exclusive with the others and with a path argument: `--stdin` takes raw bytes on stdin, `--base64` takes base64 on stdin (the shape attachments arrive in), `--url <url>` fetches the URL first. `--stdin`/`--base64` REQUIRE `--as`, because stdin carries no filename and the mime type is derived from it; `--url` falls back to `Content-Disposition` then the URL's last path segment. `--base64` decodes STRICTLY — `Buffer.from(s, "base64")` silently skips invalid characters and truncates on bad padding, so a corrupted pipe would otherwise store a short file that only fails when a human opens it. The `--url` fetch happens in the CLI, not the server: the URL comes from the operator running the command, so routing it through the backend would add an SSRF surface to buy what `curl` already does. |
|
|
35
35
|
| `lotics file download <file_id> [<path>]` · `-o <dir>` | (alias `lotics download`) Download a stored file: `GET /v1/files/{id}/signed_url` → fetch the presigned URL and write it where you asked. **The two spellings mean two different things, and neither is read by shape: the positional `<path>` is the FILE to write, `-o <dir>` is the DIRECTORY to save into.** That is `cp` and `curl -o`, so nothing here consults an extension. A named file is written as named, its parent created, overwriting what is there — the point of naming it is that the next command opens that exact path. A directory is created if missing and written into under the stored filename (the response's `Content-Disposition`), taking a free spelling beside a file of that name already there so a repeat download never clobbers the first; with no destination at all, that filename lands in cwd. Give the destination once — a positional and `-o` together is refused, as is a positional that names an existing directory or ends in a separator (`a directory goes in -o`). The first argument is a **file id**, so a path in that slot is refused rather than sent as an id. The written path goes to **stdout** (under `--json`, `{file_id, path, filename, stored_filename}`) and the narration to stderr, so a download pipes into whatever opens it. `lotics file download record <record_id> <field_key> [-o <dir>]` spreads every file on a record's file field over a DIRECTORY — there is no single file for N files to be. |
|
|
36
36
|
| `lotics file list [--limit <n>] [--cursor <token>]` | The workspace's files, newest first — id, upload time, bytes, MIME type, filename on stdout, one per line (`--json` for the object). `GET /v1/files` with no `file_ids`. A file holding the content of a knowledge doc or template you cannot use is left out. **A page, not a dump**: the store only ever grows, so the last line prints the command for the next page and `next_cursor` is null on the last one. The cursor is opaque and keyset — pass it back as given — so an upload landing mid-sweep cannot make a walk skip or repeat a row. Every other file verb takes an id, so this is the only answer to "what is in here" short of reading Postgres. |
|
|
37
37
|
| `lotics file delete <file_id>` | Archive a stored file, over the `delete_file` tool. **Refused while a record cell, a comment, a knowledge doc, a document template or a voice session still references it** — the refusal names the referents, so this is safe to try. The bytes are left in object storage; the row no longer serves them, which is what "deleted" means here. There is no `lotics delete`: the verb needs its noun. |
|
package/docs/data_model.md
CHANGED
|
@@ -129,8 +129,8 @@ What `create_table` (on the CLI or in chat) and `update_table` take in `add_fiel
|
|
|
129
129
|
`select_record_link`, `files`, `formula`, `rollup`, `lookup`, `autonumber`. `button` is retired: an
|
|
130
130
|
existing button field keeps running, but none is created, converted to or edited.
|
|
131
131
|
|
|
132
|
-
A URL, an email, markdown, a checkbox, a datetime, a
|
|
133
|
-
another type, never a `type`:
|
|
132
|
+
A URL, an email, markdown, a checkbox, a datetime, money, a quantity or a percentage is a `format`
|
|
133
|
+
or a `notation` on another type, never a `type`:
|
|
134
134
|
|
|
135
135
|
| Wanted | Field |
|
|
136
136
|
|---|---|
|
|
@@ -138,8 +138,9 @@ another type, never a `type`:
|
|
|
138
138
|
| Email or phone | `{ type: "text" }` |
|
|
139
139
|
| Markdown | `{ type: "text", format: "markdown" }` |
|
|
140
140
|
| Checkbox | `{ type: "boolean" }` |
|
|
141
|
-
| Money | `{ type: "number",
|
|
142
|
-
|
|
|
141
|
+
| Money | `{ type: "number", notation: { style: "currency", currency: "USD" } }` |
|
|
142
|
+
| Quantity | `{ type: "number", notation: { style: "unit", unit: "kg" } }` |
|
|
143
|
+
| Percentage | `{ type: "number", notation: { style: "unit", unit: "percent" } }` |
|
|
143
144
|
| Datetime | `{ type: "date", format: "datetime" }` |
|
|
144
145
|
| Date range | `{ type: "date", format: "date_range" }` |
|
|
145
146
|
|
|
@@ -150,14 +151,18 @@ may itself hold an object (`formula`, `aggregate_option`, `filter`, `order_by`);
|
|
|
150
151
|
inside it. Each type takes only its own:
|
|
151
152
|
|
|
152
153
|
- **text** — `format?` (`"text"` | `"link"` | `"markdown"`), `unique?`, `default_value?` (a string).
|
|
153
|
-
- **number** — `
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
- `
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
154
|
+
- **number** — `notation?`, `default_value?` (a number). `notation` is how the figure reads, one
|
|
155
|
+
of three; on `update_fields` a stated one replaces the field's whole.
|
|
156
|
+
- `{ style: "decimal" }` — a plain number, as a field stating none reads.
|
|
157
|
+
- `{ style: "currency", currency: "VND" }` — money in an ISO 4217 code.
|
|
158
|
+
- `{ style: "unit", unit }` — a quantity: `"percent"` (10 reads "10%", never multiplied), a
|
|
159
|
+
measured code — g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day — or a counted noun such
|
|
160
|
+
as `kiện`. A change between two units of one dimension converts every stored figure; any other
|
|
161
|
+
change relabels.
|
|
162
|
+
- A currency or unit read on each row is `{ per_row: "ĐVT" }` in its place: a single select on
|
|
163
|
+
the same row, or a lookup of one through a one-link, named by name or key, whose chosen option
|
|
164
|
+
is that row's currency or unit — every option label an ISO 4217 code, or a unit as `unit`
|
|
165
|
+
takes one.
|
|
161
166
|
- **date** — `format?` (`"date"` | `"datetime"` | `"date_range"` | `"datetime_range"`), `timezone?`,
|
|
162
167
|
`derive_from?`, `default_value?` (a date string). `derive_from: "created_at"` stamps the row's
|
|
163
168
|
creation once, `"updated_at"` re-stamps on every update; the field is then read-only, takes no
|
|
@@ -186,7 +191,7 @@ not exist yet, and option keys (`opt_…`) in `update_fields`.
|
|
|
186
191
|
Read-only in records. `formula` is one key holding an object; a rollup's and a lookup's properties
|
|
187
192
|
are separate keys on the field — `rollup: {…}` and `lookup: {…}` are refused as unrecognized.
|
|
188
193
|
|
|
189
|
-
formula: { expression,
|
|
194
|
+
formula: { expression, notation?, link? }
|
|
190
195
|
|
|
191
196
|
- `{Field Name}` or `{fld_key}` names a field of the same table, stored as its key, so a rename never
|
|
192
197
|
breaks the formula. A reference to no field, or a call to something that is no helper, is refused.
|
|
@@ -194,9 +199,10 @@ formula: { expression, format?, currency?, unit?, unit_field?, currency_field? }
|
|
|
194
199
|
`includes({Tags}, "opt_…")` for a multi.
|
|
195
200
|
- The operators, the helpers and how an empty cell reads are the `model` reference, section `formula` — a model
|
|
196
201
|
names fields by alias where a table tool names them by name or key; the language is the same.
|
|
197
|
-
-
|
|
198
|
-
|
|
199
|
-
|
|
202
|
+
- `notation` is how a number result reads, as a number field's; `link: true` says the result is a
|
|
203
|
+
URL, drawn as a link that opens.
|
|
204
|
+
- On `update_fields` a key left out keeps its stored value, and a stated `notation` replaces the
|
|
205
|
+
stored one whole. `get_table` marks a formula that is null when every field it reads is empty.
|
|
200
206
|
|
|
201
207
|
rollup — `source_field_key`, `aggregate_option: { field_key?, operation }`, `filter?`
|
|
202
208
|
|
|
@@ -207,9 +213,10 @@ rollup — `source_field_key`, `aggregate_option: { field_key?, operation }`, `f
|
|
|
207
213
|
`min`, `max`, `range`; date: `earliest`, `latest`, `date_range`.
|
|
208
214
|
- The cell's type comes from the OPERATION: `earliest` and `latest` hold a date, every other
|
|
209
215
|
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
|
|
211
|
-
|
|
212
|
-
|
|
216
|
+
`max` are numeric and refuse a date. A rollup takes no notation of its own: `sum` over money
|
|
217
|
+
reads in the aggregated field's currency, over a weight in its unit, a count as a plain number
|
|
218
|
+
and a `percent_*` in the unit `percent`.
|
|
219
|
+
- Over a figure read in each row's own unit or currency (a notation's `per_row`), `sum`,
|
|
213
220
|
`avg`, `median`, `min`, `max` and `range` hold only where that select is a lookup, through the
|
|
214
221
|
link paired with `source_field_key`, of a single select on this table — the total reads in this
|
|
215
222
|
row's option of it. A lookup of such a field has no unit.
|
|
@@ -227,13 +234,14 @@ lookup — `source_field_key`, `lookup_field_key`, `order_by?`
|
|
|
227
234
|
### Examples
|
|
228
235
|
|
|
229
236
|
```
|
|
230
|
-
{ name: "Giá bán", type: "number",
|
|
231
|
-
{ name: "Trọng lượng", type: "number", unit: "kg" }
|
|
232
|
-
{ name: "Số lượng", type: "number",
|
|
237
|
+
{ name: "Giá bán", type: "number", notation: { style: "currency", currency: "VND" } }
|
|
238
|
+
{ name: "Trọng lượng", type: "number", notation: { style: "unit", unit: "kg" } }
|
|
239
|
+
{ name: "Số lượng", type: "number", notation: { style: "unit", unit: { per_row: "ĐVT" } } }
|
|
240
|
+
{ name: "Chiết khấu", type: "number", notation: { style: "unit", unit: "percent" } }
|
|
233
241
|
{ name: "Trạng thái", type: "select", options: [{ name: "Mới" }, { name: "Xong" }] }
|
|
234
242
|
{ name: "Mã đơn", type: "autonumber", template: "SR-{YEAR}-{N:4}" }
|
|
235
243
|
{ 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}",
|
|
244
|
+
{ name: "Total", type: "formula", formula: { expression: "{Price} * {Qty}", notation: { style: "currency", currency: "VND" } } }
|
|
237
245
|
{ name: "SL đã giao", type: "rollup", source_field_key: "Giao hàng", aggregate_option: { operation: "sum", field_key: "Số lượng" } }
|
|
238
246
|
{ name: "Customer Name", type: "lookup", source_field_key: "Customer", lookup_field_key: "Name" }
|
|
239
247
|
{ name: "Latest note", type: "lookup", source_field_key: "Calls", lookup_field_key: "Note", order_by: { field_key: "At", direction: "desc" } }
|
package/docs/examples.md
CHANGED
|
@@ -35,7 +35,7 @@ Every key: the `model` reference, at the pages each section names under **Keys**
|
|
|
35
35
|
{"alias": "booked_on", "type": "date"},
|
|
36
36
|
{"alias": "parts_ready_on", "type": "date"},
|
|
37
37
|
{"alias": "arrived_on", "type": "date"},
|
|
38
|
-
{"alias": "hours", "type": "number", "
|
|
38
|
+
{"alias": "hours", "type": "number", "notation": {"style": "unit", "unit": "h"}},
|
|
39
39
|
{"alias": "report", "type": "text", "format": "markdown"},
|
|
40
40
|
{"alias": "completed_on", "type": "date"}
|
|
41
41
|
]
|
|
@@ -120,7 +120,7 @@ Every key: the `model` reference, at the pages each section names under **Keys**
|
|
|
120
120
|
{"alias": "booked_on", "type": "date"},
|
|
121
121
|
{"alias": "parts_ready_on", "type": "date"},
|
|
122
122
|
{"alias": "arrived_on", "type": "date"},
|
|
123
|
-
{"alias": "hours", "type": "number", "
|
|
123
|
+
{"alias": "hours", "type": "number", "notation": {"style": "unit", "unit": "h"}},
|
|
124
124
|
{"alias": "completed_on", "type": "date"}
|
|
125
125
|
]
|
|
126
126
|
},
|
|
@@ -379,7 +379,7 @@ Every key: the `model` reference, at the pages each section names under **Keys**
|
|
|
379
379
|
},
|
|
380
380
|
{"alias": "on_hand", "type": "number"},
|
|
381
381
|
{"alias": "minimum", "type": "number"},
|
|
382
|
-
{"alias": "price", "type": "number", "
|
|
382
|
+
{"alias": "price", "type": "number", "notation": {"style": "currency", "currency": "USD"}},
|
|
383
383
|
{"alias": "supplier_page", "type": "text", "format": "link"},
|
|
384
384
|
{"alias": "last_counted", "type": "date"},
|
|
385
385
|
{"alias": "counted_by", "type": "select_member"},
|
|
@@ -394,7 +394,7 @@ Every key: the `model` reference, at the pages each section names under **Keys**
|
|
|
394
394
|
{"alias": "quantity", "type": "number"},
|
|
395
395
|
{"alias": "shipped", "type": "number"},
|
|
396
396
|
{"alias": "picked", "type": "number"},
|
|
397
|
-
{"alias": "unit_price", "type": "number", "
|
|
397
|
+
{"alias": "unit_price", "type": "number", "notation": {"style": "currency", "currency": "USD"}},
|
|
398
398
|
{"alias": "amount", "type": "formula"},
|
|
399
399
|
{"alias": "category", "type": "lookup"}
|
|
400
400
|
]
|
|
@@ -493,7 +493,7 @@ Every key: the `model` reference, at the pages each section names under **Keys**
|
|
|
493
493
|
{"alias": "phone", "type": "text"},
|
|
494
494
|
{"alias": "website", "type": "text", "format": "link"},
|
|
495
495
|
{"alias": "account_manager", "type": "select_member"},
|
|
496
|
-
{"alias": "credit_limit", "type": "number", "
|
|
496
|
+
{"alias": "credit_limit", "type": "number", "notation": {"style": "currency", "currency": "USD"}},
|
|
497
497
|
{"alias": "outstanding", "type": "rollup"},
|
|
498
498
|
{"alias": "over_limit", "type": "formula"},
|
|
499
499
|
{
|
|
@@ -746,7 +746,7 @@ Every key: the `model` reference, at the pages each section names under **Keys**
|
|
|
746
746
|
{"alias": "sale", "type": "select_member"},
|
|
747
747
|
{"alias": "xu_ly", "type": "select_member"},
|
|
748
748
|
{"alias": "can_bo_sung", "type": "boolean"},
|
|
749
|
-
{"alias": "phi_dich_vu", "type": "number", "
|
|
749
|
+
{"alias": "phi_dich_vu", "type": "number", "notation": {"style": "currency", "currency": "VND"}},
|
|
750
750
|
{
|
|
751
751
|
"alias": "loai_can",
|
|
752
752
|
"type": "select",
|
|
@@ -758,7 +758,7 @@ Every key: the `model` reference, at the pages each section names under **Keys**
|
|
|
758
758
|
]
|
|
759
759
|
},
|
|
760
760
|
{"alias": "can_boc", "type": "text"},
|
|
761
|
-
{"alias": "hoa_hong_sale", "type": "number", "
|
|
761
|
+
{"alias": "hoa_hong_sale", "type": "number", "notation": {"style": "currency", "currency": "VND"}},
|
|
762
762
|
{"alias": "ngay_thu_phi", "type": "date"},
|
|
763
763
|
{"alias": "ngay_nhan_giay_to", "type": "date"},
|
|
764
764
|
{"alias": "ngay_lap_ho_so", "type": "date"},
|
|
@@ -812,7 +812,7 @@ Every key: the `model` reference, at the pages each section names under **Keys**
|
|
|
812
812
|
},
|
|
813
813
|
{"alias": "ngay_sinh", "type": "date"},
|
|
814
814
|
{"alias": "cccd", "type": "text"},
|
|
815
|
-
{"alias": "thu_nhap", "type": "number", "
|
|
815
|
+
{"alias": "thu_nhap", "type": "number", "notation": {"style": "currency", "currency": "VND"}}
|
|
816
816
|
]
|
|
817
817
|
},
|
|
818
818
|
{
|
|
@@ -828,7 +828,7 @@ Every key: the `model` reference, at the pages each section names under **Keys**
|
|
|
828
828
|
]
|
|
829
829
|
},
|
|
830
830
|
{"alias": "ho_so", "type": "select_record_link", "target_entity": "ho_so", "cardinality": "one"},
|
|
831
|
-
{"alias": "so_tien", "type": "number", "
|
|
831
|
+
{"alias": "so_tien", "type": "number", "notation": {"style": "currency", "currency": "VND"}},
|
|
832
832
|
{
|
|
833
833
|
"alias": "hinh_thuc",
|
|
834
834
|
"type": "select",
|
|
@@ -1471,7 +1471,7 @@ Every key: the `model` reference, at the pages each section names under **Keys**
|
|
|
1471
1471
|
"target_entity": "nha_cung_cap",
|
|
1472
1472
|
"cardinality": "one"
|
|
1473
1473
|
},
|
|
1474
|
-
{"alias": "so_tien", "type": "number", "
|
|
1474
|
+
{"alias": "so_tien", "type": "number", "notation": {"style": "currency", "currency": "VND"}},
|
|
1475
1475
|
{"alias": "han_thanh_toan", "type": "date"},
|
|
1476
1476
|
{
|
|
1477
1477
|
"alias": "trang_thai",
|
|
@@ -1520,6 +1520,7 @@ Every key: the `model` reference, at the pages each section names under **Keys**
|
|
|
1520
1520
|
"register": {
|
|
1521
1521
|
"columns": ["nguoi_de_nghi"],
|
|
1522
1522
|
"filters": ["nguoi_de_nghi", "du_an"],
|
|
1523
|
+
"group": "du_an",
|
|
1523
1524
|
"create": ["noi_dung", "loai", "du_an", "nha_cung_cap", "so_tien", "han_thanh_toan", "nguoi_de_nghi"],
|
|
1524
1525
|
"readings": [
|
|
1525
1526
|
{
|
|
@@ -1595,7 +1596,7 @@ Every key: the `model` reference, at the pages each section names under **Keys**
|
|
|
1595
1596
|
"type": "select",
|
|
1596
1597
|
"options": [{"alias": "thu", "color": "green"}, {"alias": "chi", "color": "rose"}]
|
|
1597
1598
|
},
|
|
1598
|
-
{"alias": "so_tien", "type": "number", "
|
|
1599
|
+
{"alias": "so_tien", "type": "number", "notation": {"style": "currency", "currency": "VND"}},
|
|
1599
1600
|
{"alias": "bien_dong", "type": "formula"},
|
|
1600
1601
|
{
|
|
1601
1602
|
"alias": "doi_tuong",
|
package/docs/filters.md
CHANGED
|
@@ -49,8 +49,9 @@ pass the whole list in one condition rather than splitting it across calls.
|
|
|
49
49
|
| `equals`, `not_equals`, `greater_than`, `less_than`, `greater_than_or_equal_to`, `less_than_or_equal_to` | a number |
|
|
50
50
|
| `is_empty`, `is_not_empty` | none |
|
|
51
51
|
|
|
52
|
-
A number whose
|
|
53
|
-
|
|
52
|
+
A number whose `notation` reads its unit or currency on each row (`{ "per_row": <select> }`) is
|
|
53
|
+
read in each row's own, so a comparison on it states `unit_option`: the option of that select its
|
|
54
|
+
value is in.
|
|
54
55
|
Rows holding another option are out; measured units of one dimension convert.
|
|
55
56
|
|
|
56
57
|
```
|
package/docs/workflows.md
CHANGED
|
@@ -6,9 +6,10 @@ Only the forms below are accepted; anything else is refused at save with the lin
|
|
|
6
6
|
what to write instead. A save that succeeds may still return `warnings` — advisory hints such as a
|
|
7
7
|
loop that may never end. Read them.
|
|
8
8
|
|
|
9
|
-
The same grammar serves an automation, a table's lifecycle workflow and an app's workflow
|
|
10
|
-
automation's source opens with a trigger declaration; a
|
|
11
|
-
table and event, an app workflow from the app that calls
|
|
9
|
+
The same grammar serves an automation, a table's lifecycle workflow and an app's workflow, and the
|
|
10
|
+
expressions of a table check. Only an automation's source opens with a trigger declaration; a
|
|
11
|
+
lifecycle workflow takes its trigger from its table and event, an app workflow from the app that calls
|
|
12
|
+
it.
|
|
12
13
|
|
|
13
14
|
## Triggers
|
|
14
15
|
|
|
@@ -63,10 +64,10 @@ await send_email({
|
|
|
63
64
|
| return | `return({ status: "success" \| "error", message: <expr>, field_errors: { ... }? });` |
|
|
64
65
|
| validate | `validate({ checks: [{ fail_when: <expr>, field_key: "...", message: <expr> }, ...] });` |
|
|
65
66
|
|
|
66
|
-
`return` is a call, not a JavaScript `return` statement. `validate`
|
|
67
|
-
`fail_when` is truthy; the check's `field_key` names the control its message lands on —
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
`return` is a call, not a JavaScript `return` statement. `validate` ends the workflow with an error
|
|
68
|
+
when a check's `fail_when` is truthy; the check's `field_key` names the control its message lands on —
|
|
69
|
+
in an app workflow a declared input, or the `fld_` key of a field on a table the body names; a field of
|
|
70
|
+
the trigger table in a lifecycle workflow; in an automation it is not checked.
|
|
70
71
|
|
|
71
72
|
The name in `const x = await tool({...})` is the step's id; later expressions read its output as
|
|
72
73
|
`x.records`, `x.id` and so on. A `// id: my_step` comment directly above a statement sets an explicit
|
|
@@ -122,8 +123,7 @@ step — summarize a record, classify, draft text.
|
|
|
122
123
|
- `output` — `{ mode: "text" }` resolves to a string; `{ mode: "object", schema }` to a typed object.
|
|
123
124
|
|
|
124
125
|
Only `x` comes back: in text mode `x` is the string itself, in object mode `x.field` reads a field the
|
|
125
|
-
`schema` declares. The agent's own tool calls are not readable.
|
|
126
|
-
synchronous verdict, so a `before_*` lifecycle workflow refuses it.
|
|
126
|
+
`schema` declares. The agent's own tool calls are not readable.
|
|
127
127
|
|
|
128
128
|
## Keys, not names
|
|
129
129
|
|
|
@@ -264,25 +264,6 @@ field when there is none. Use `coalesce(x, fallback)` only for a real fallback v
|
|
|
264
264
|
|
|
265
265
|
## Examples
|
|
266
266
|
|
|
267
|
-
Refuse a duplicate before it is created (a `before_create` lifecycle workflow; the keys come from
|
|
268
|
-
`get_table`):
|
|
269
|
-
|
|
270
|
-
```
|
|
271
|
-
const dup = await query_records({
|
|
272
|
-
table_id: "tbl_orders",
|
|
273
|
-
filters: { node_type: "group", logic: "and", children: [
|
|
274
|
-
{ node_type: "condition", field_key: "fld_ref", operator: "equals", value: record["fld_ref"] },
|
|
275
|
-
{ node_type: "condition", field_key: "fld_closed_at", operator: "is_empty" },
|
|
276
|
-
]},
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
validate({ checks: [{
|
|
280
|
-
fail_when: size(dup.records) > 0,
|
|
281
|
-
field_key: "fld_ref",
|
|
282
|
-
message: "An open order already has this reference.",
|
|
283
|
-
}]});
|
|
284
|
-
```
|
|
285
|
-
|
|
286
267
|
Compare a link by id. Display text is not unique, and a link reads as a list of ids — `==` against a
|
|
287
268
|
string is a type error:
|
|
288
269
|
|
|
@@ -302,16 +283,14 @@ if (customer && includes(record["fld_customer"], customer.id)) {
|
|
|
302
283
|
|
|
303
284
|
A lifecycle workflow is bound to one table and one event, and its source has no `on({...})` line:
|
|
304
285
|
|
|
305
|
-
- `
|
|
306
|
-
- `
|
|
307
|
-
|
|
308
|
-
- `before_delete` / `after_delete`.
|
|
286
|
+
- `after_create` — a create, and a draft's submit (the moment it becomes a record).
|
|
287
|
+
- `after_update` — every field edit, a draft's included.
|
|
288
|
+
- `after_delete`.
|
|
309
289
|
|
|
310
|
-
A
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
workflow that writes its own table saves with a `loop_potential` warning: its write fires it again.
|
|
290
|
+
A lifecycle workflow runs after the write commits, with every step; its errors are logged and never
|
|
291
|
+
fail the write. What refuses a write before it lands is a table check (see **Table
|
|
292
|
+
checks**). An `after_update` workflow that writes its own table saves with a `loop_potential`
|
|
293
|
+
warning: its write fires it again.
|
|
315
294
|
|
|
316
295
|
| reads | on |
|
|
317
296
|
|---|---|
|
|
@@ -334,3 +313,63 @@ that cares about some events only skips the rest:
|
|
|
334
313
|
|
|
335
314
|
In `if_source`, `runtime.change_origin` is the origin of the write that fired it; in the body, it is
|
|
336
315
|
the workflow's own.
|
|
316
|
+
|
|
317
|
+
## Table checks
|
|
318
|
+
|
|
319
|
+
A table check is one condition its table's record writes must not meet, declared beside the table's
|
|
320
|
+
`unique`. It is not a workflow: it reads the write and a few lookups and answers yes or no, so it
|
|
321
|
+
cannot change data. It runs inside the write, on every create, update or delete it is `on`, whichever
|
|
322
|
+
surface makes it — a person, the API, the CLI, an agent, a workflow. A refusal writes nothing and
|
|
323
|
+
answers with the check's message, under its field when the check names one, and names the check.
|
|
324
|
+
|
|
325
|
+
On the CLI or in chat, `set_table_check` creates one, or changes one when given its `table_check_id`
|
|
326
|
+
and only the parts that change, and `remove_table_check` removes one. `get_table` lists a table's
|
|
327
|
+
checks in the source they are written in.
|
|
328
|
+
|
|
329
|
+
| part | what it holds |
|
|
330
|
+
|---|---|
|
|
331
|
+
| `on` | the writes it checks: any of `create`, `update`, `delete` |
|
|
332
|
+
| `when` | optional expression; the check runs only on a write where it is truthy. It cannot read lookups. |
|
|
333
|
+
| `lookups` | optional, by name, the rows to read: `{ table_id, filter, limit }`, the filter as `query_records` takes it, `limit` from 1 to 50 (50 when left out). A filter value may read the record. |
|
|
334
|
+
| `fail_when` | expression; truthy refuses the write |
|
|
335
|
+
| `message` | expression for the refusal's text — a fixed text is a quoted string |
|
|
336
|
+
| `field_key` | optional; the field the refusal is shown under |
|
|
337
|
+
|
|
338
|
+
Every expression is written in the grammar above and reads:
|
|
339
|
+
|
|
340
|
+
- `record` — the record as it will be stored; on a delete, the record being deleted.
|
|
341
|
+
- `prev_record` — on an update or a delete, the record before.
|
|
342
|
+
- `changes` — on an update, the fields it changes.
|
|
343
|
+
- `lookups.<name>` — in `fail_when` and `message`, the rows that lookup read.
|
|
344
|
+
- `runtime.timezone`, `runtime.workspace_id`, `runtime.organization_id`, `runtime.change_origin` (the
|
|
345
|
+
write's origin), `runtime.triggered_by_member_id` (the member writing; null for a write no member
|
|
346
|
+
made), and `now()`.
|
|
347
|
+
|
|
348
|
+
A check on several operations reads only what all of them carry, so one on creates and updates reads
|
|
349
|
+
neither `prev_record` nor `changes`.
|
|
350
|
+
|
|
351
|
+
A lookup reads with the access of the member who last saved the check, so saving one requires reading
|
|
352
|
+
every table its lookups read. It reads committed rows: two writes at the same moment can each pass a
|
|
353
|
+
check the other would fail, so values no two rows may share are the table's `unique`, not a check.
|
|
354
|
+
|
|
355
|
+
A `when`, a lookup or a `fail_when` that cannot decide — it fails, or the write's time runs out —
|
|
356
|
+
refuses the write. A draft is checked when it is submitted, as a create; its edits before that are
|
|
357
|
+
not. A restored record is checked as a create. Cloning a table copies its checks to the member who
|
|
358
|
+
clones it, and is refused when that member cannot read a table a check's lookup reads.
|
|
359
|
+
|
|
360
|
+
One open order per reference, a row never counting against itself:
|
|
361
|
+
|
|
362
|
+
```
|
|
363
|
+
{
|
|
364
|
+
"table_id": "tbl_orders",
|
|
365
|
+
"name": "One open order per reference",
|
|
366
|
+
"on": ["create", "update"],
|
|
367
|
+
"when": "isNull(record[\"fld_closed_at\"])",
|
|
368
|
+
"lookups": {
|
|
369
|
+
"same_ref": "{ table_id: \"tbl_orders\", filter: { node_type: \"group\", logic: \"and\", children: [ { field_key: \"fld_ref\", operator: \"equals\", value: record[\"fld_ref\"] }, { field_key: \"fld_closed_at\", operator: \"is_empty\" } ] }, limit: 2 }"
|
|
370
|
+
},
|
|
371
|
+
"fail_when": "some(lookups.same_ref, (r) => r.id != record.id)",
|
|
372
|
+
"message": "\"An open order already has this reference.\"",
|
|
373
|
+
"field_key": "fld_ref"
|
|
374
|
+
}
|
|
375
|
+
```
|