@lotics/app-sdk 0.87.1 → 0.87.4
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/docs/data_fetching.md +17 -0
- package/docs/mutations.md +5 -2
- package/docs/workflows.md +98 -13
- package/package.json +1 -1
package/docs/data_fetching.md
CHANGED
|
@@ -153,6 +153,23 @@ server validates system conditions by `type` and never reads `field_key` on them
|
|
|
153
153
|
number, while `error !== null`. Note a key CHANGE resets this: a new `params`/`filter`/`sort`
|
|
154
154
|
is a fresh key with no prior rows, so the "last successful rows stay rendered" behaviour above
|
|
155
155
|
does not save you.
|
|
156
|
+
- **Never state an ABSENCE from `rows` without gating on `loading`.** The sibling of the rule
|
|
157
|
+
above, and it bites earlier: while the first request is in flight `rows` is `[]`, so
|
|
158
|
+
`rows.find(…)` returns nothing and any code shaped `if (!found) → "there is no X"` prints a
|
|
159
|
+
confident denial of something that is merely not here yet. It corrects itself when the data
|
|
160
|
+
lands, which is exactly what makes it ship: the author sees the settled screen, and only a
|
|
161
|
+
reader opening the surface cold sees the half-second where every requirement reads as missing.
|
|
162
|
+
A record drawer did this to five document rows at once — each rendering a red "missing" badge
|
|
163
|
+
and a blocking callout — so the loudest thing on the screen was, briefly, entirely false.
|
|
164
|
+
Gate the derivation, not the display: compute nothing while `loading`, and reserve the space with
|
|
165
|
+
a `Skeleton` so the layout does not jump when the answer arrives.
|
|
166
|
+
|
|
167
|
+
Watch for the second source of a legitimate empty: `enabled: false` never sends a request, so
|
|
168
|
+
`rows` is `[]` and `loading` is `false` **forever**. A detail query gated on a parent selection
|
|
169
|
+
(`{ enabled: vehicleId != null }`) therefore satisfies "not loading, no rows" while the real
|
|
170
|
+
fact is that nothing was asked. Read the gate itself — `loading || vehicleId == null` — and say
|
|
171
|
+
what is actually missing, which is the parent, not the children.
|
|
172
|
+
|
|
156
173
|
- **`refetch()`** re-runs the query. A successful `useWorkflow` call already re-reads the mounted
|
|
157
174
|
queries on its own (see [./mutations.md](./mutations.md)), so reach for this only where a write
|
|
158
175
|
cannot have told you: a poll, a value that changes without anything on this screen writing, or a
|
package/docs/mutations.md
CHANGED
|
@@ -321,7 +321,8 @@ await update_records({
|
|
|
321
321
|
|
|
322
322
|
`set` still writes every key it carries — `null` in `set` **clears** the field; `null`/absent in
|
|
323
323
|
`set_skip_null` **skips** it. The full write surface (`set`, `set_skip_null`, the surgical
|
|
324
|
-
`add_to`/`remove_from`/`replace` ops, `field_edits`, and the exact value shape per
|
|
324
|
+
`add_to`/`remove_from`/`replace` ops, `increment`, `field_edits`, and the exact value shape per
|
|
325
|
+
field type)
|
|
325
326
|
is [workflows](./workflows.md#writing-records).
|
|
326
327
|
|
|
327
328
|
## Refetch after a mutation
|
|
@@ -446,7 +447,9 @@ Why a full-form snapshot save is a bug, not a style choice — three independent
|
|
|
446
447
|
append is the system's rather than a person's — an upload settling, a workflow attaching a
|
|
447
448
|
generated document — name the ITEMS with `add_to` / `remove_from`
|
|
448
449
|
([workflows](./workflows.md#writing-records)) and the write path resolves them against the
|
|
449
|
-
record as it stands.
|
|
450
|
+
record as it stands. A running total is the scalar form of the same defect: two writers both
|
|
451
|
+
read 100, both subtract 10, both write 90, and one sale is gone with no error anywhere. Name
|
|
452
|
+
the DELTA with `increment`.
|
|
450
453
|
|
|
451
454
|
```tsx
|
|
452
455
|
// load: snapshot the editable fields from the row
|
package/docs/workflows.md
CHANGED
|
@@ -482,31 +482,38 @@ differ from `<` / `>` (code-unit order) and from the database's collation.
|
|
|
482
482
|
|
|
483
483
|
## Writing records
|
|
484
484
|
|
|
485
|
-
`update_records` carries
|
|
485
|
+
`update_records` carries five composable surfaces. A field may appear in **at most one** of them.
|
|
486
486
|
|
|
487
487
|
| Surface | Shape | Effect |
|
|
488
488
|
|---|---|---|
|
|
489
489
|
| `set` | `{ fld_x: value }` | write the whole value. `null` **clears** the field |
|
|
490
490
|
| `set_skip_null` | `{ fld_x: value }` | same shape, but `null`/`undefined` entries are **dropped** — absent means "leave unchanged" |
|
|
491
491
|
| `add_to` / `remove_from` / `replace` | `{ fld_x: [items] }` | surgical edits on multi-value fields (files, multi select, multi member, record links) |
|
|
492
|
-
| `
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
492
|
+
| `increment` | `{ fld_x: delta }` | move a **number** field by a signed amount. Negative decrements; there is no separate decrement op |
|
|
493
|
+
| `field_edits` | `[{ field, op, value }]` | the multi-value ops with the field named by a **string expression** — the only way to target a field chosen at run time. `increment` has no value-form: its payload is a scalar, not an item list |
|
|
494
|
+
|
|
495
|
+
`add_to`, `remove_from` and `increment` are RELATIVE: the server resolves them against the record
|
|
496
|
+
as it stands when the write lands, inside the lock it already takes. Two runs changing one field at
|
|
497
|
+
the same time therefore each keep their effect — which reading the field and writing the result back
|
|
498
|
+
through `set` does not, since both fold onto the value they read and the later write drops the
|
|
499
|
+
earlier, both reporting success. On a stock count that is a sale that vanishes. Parallel runs are
|
|
500
|
+
ordinary (a trigger firing twice, an agent emitting two calls in one step), so reach for the ops
|
|
501
|
+
rather than a read-modify-write whenever the workflow is CHANGING a field by an amount rather than
|
|
502
|
+
stating its value.
|
|
503
|
+
|
|
504
|
+
`increment` counts an empty or unset field as 0, so a first decrement leaves a negative — oversold,
|
|
505
|
+
and visibly so. It is rejected on formula, rollup, lookup and autonumber fields: those come from
|
|
506
|
+
their own definition, so change what they aggregate instead.
|
|
501
507
|
|
|
502
508
|
Inside `set` and `create_records.records`: `null` clears (persisted), `undefined` or an omitted
|
|
503
509
|
key preserves. So passing a possibly-null read straight through is safe. Use
|
|
504
510
|
`coalesce(x, fallback)` only when you want a real fallback, never to "strip" null.
|
|
505
511
|
|
|
506
512
|
`set_skip_null` is what turns a bag of optional workflow inputs into a diff-write without a guard
|
|
507
|
-
per field; naming the same field in both `set` and `set_skip_null` is an error
|
|
508
|
-
all-dropped `set_skip_null
|
|
509
|
-
`before_update` hooks
|
|
513
|
+
per field; naming the same field in both `set` and `set_skip_null` is an error. A call whose
|
|
514
|
+
surfaces all resolve to nothing — an all-dropped `set_skip_null`, an op object with no entries —
|
|
515
|
+
is a no-op: no records touched, no `before_update` hooks. So a conditionally-built op object needs
|
|
516
|
+
no guard around the call. Why diffs and not snapshots, plus locked records and the
|
|
510
517
|
`request_locked_record_change` path: [mutations](./mutations.md#diff-before-update--send-only-what-changed).
|
|
511
518
|
|
|
512
519
|
**A `null` optional input is a CLEAR, and `set_skip_null` drops it.** The caller has three states
|
|
@@ -602,6 +609,84 @@ if (!current_member_in_any_group(["grp_managers"])) {
|
|
|
602
609
|
|
|
603
610
|
Full model, including what a public app must never expose: [security](./security.md).
|
|
604
611
|
|
|
612
|
+
## Every `await` is a round trip
|
|
613
|
+
|
|
614
|
+
A body runs **strictly sequentially**. There is no `Promise.all`, and independent reads do not
|
|
615
|
+
overlap — three `get_record` calls that need nothing from each other still cost three trips, in
|
|
616
|
+
order. So the thing to count when a workflow feels slow is not how much data moves but **how many
|
|
617
|
+
tool calls it makes**, and the two cheapest wins are always the same: don't fetch what you already
|
|
618
|
+
have, and don't write one row at a time.
|
|
619
|
+
|
|
620
|
+
**A write costs more than a read.** A create or update drags its computed-field cascades, rollups
|
|
621
|
+
and `after_*` hooks behind it before the call returns, so trimming reads is worth less than
|
|
622
|
+
trimming writes. Reach for the write-side reductions first.
|
|
623
|
+
|
|
624
|
+
### Batch every write — and build the array INLINE
|
|
625
|
+
|
|
626
|
+
`create_records` takes an ARRAY. N rows written one call at a time is N round trips plus N cascade
|
|
627
|
+
passes; the same N rows in one call is one of each.
|
|
628
|
+
|
|
629
|
+
The catch is where you build that array, and it is not obvious. **Write the literal inside the
|
|
630
|
+
call**, so it is typed by the `records` parameter:
|
|
631
|
+
|
|
632
|
+
```js
|
|
633
|
+
// ONE trip. The array literal is contextually typed by `records`, so "opt_bVPEMB"
|
|
634
|
+
// stays the literal the table's write type wants.
|
|
635
|
+
await create_records({
|
|
636
|
+
records: concat(
|
|
637
|
+
coRaVao ? [{ fld_khoan: "opt_bVPEMB", fld_tien: giaRaVao, fld_chuyen: [i.chuyen_id] }] : [],
|
|
638
|
+
coVeSinh ? [{ fld_khoan: "opt_nx0KAL", fld_tien: giaVeSinh, fld_chuyen: [i.chuyen_id] }] : [],
|
|
639
|
+
),
|
|
640
|
+
table_id: "tbl_…",
|
|
641
|
+
});
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
Accumulating into a variable first does **not** work, and the two errors it produces point away
|
|
645
|
+
from the fix:
|
|
646
|
+
|
|
647
|
+
```js
|
|
648
|
+
let rows = [];
|
|
649
|
+
for (const g of gia.records) {
|
|
650
|
+
rows = concat(rows, [{ fld_khoan: "opt_bVPEMB", … }]); // widens to `string`
|
|
651
|
+
}
|
|
652
|
+
await create_records({ records: rows, … });
|
|
653
|
+
// TS2322: Type '{ fld_khoan: string; … }[]' is not assignable to 'readonly …RecordWrite[]'.
|
|
654
|
+
// Type 'string' is not assignable to '"opt_bVPEMB" | "opt_nx0KAL" | … | null'.
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
`concat` widens a select's option key to `string` the moment it leaves the literal, and the obvious
|
|
658
|
+
repair — `let rows: SomeRecordWrite[] = []` — is rejected by the next pass, because the stored body
|
|
659
|
+
is parsed as a JS subset and type syntax is not in it. The way through is not an annotation: it is
|
|
660
|
+
to let the call site supply the type. Keep the loop for FINDING values and put the writing after it.
|
|
661
|
+
|
|
662
|
+
### Don't buy the same row twice
|
|
663
|
+
|
|
664
|
+
- **A record you already read is already in hand.** The authorization preamble usually reads the
|
|
665
|
+
triggering record; a later `get_record` on the same id is a second trip for a value sitting in a
|
|
666
|
+
variable.
|
|
667
|
+
- **Don't read back what you just computed.** A total you derived from the rows you are writing is
|
|
668
|
+
cheaper to sum in the body than to re-read from a computed field afterwards.
|
|
669
|
+
- **`create_records` returns `{ created, record_ids }` — ids only.** When you need a
|
|
670
|
+
server-generated value (an auto-number), read it back with `get_record` on the returned id, never
|
|
671
|
+
a filtered `query_records` that scans for the row you just made: the filtered form is both an
|
|
672
|
+
extra scan and wrong under concurrency, since a row created between your write and your read
|
|
673
|
+
matches the same filter.
|
|
674
|
+
|
|
675
|
+
### Keep expensive steps off the path the caller waits on
|
|
676
|
+
|
|
677
|
+
`generate_pdf_from_template` and `agent` are the two steps that dominate a body's wall clock. Ask
|
|
678
|
+
whether the person pressing the button needs that artifact **at that instant**. A document that is
|
|
679
|
+
printed later belongs in the workflow that prints it — moving it there also removes the reads that
|
|
680
|
+
existed only to feed it, which is usually where the round trips were hiding.
|
|
681
|
+
|
|
682
|
+
### Measuring, if you do
|
|
683
|
+
|
|
684
|
+
`query_workflow_executions` records `started_at` / `completed_at` per run, which is the server's own
|
|
685
|
+
duration with your network and CLI startup excluded. **Run-to-run variance is large** — the same
|
|
686
|
+
body measured twice, minutes apart, can differ by a second — so compare medians of several runs,
|
|
687
|
+
and interleave the two versions rather than measuring one after the other. A single before/after
|
|
688
|
+
pair will happily show an improvement that is only drift.
|
|
689
|
+
|
|
605
690
|
## Traps
|
|
606
691
|
|
|
607
692
|
The rules that are easy to get wrong because the failing code looks correct.
|