@lotics/app-sdk 0.91.1 → 0.93.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 +5 -2
- package/dist/src/hooks.d.ts +57 -22
- package/dist/src/hooks.js +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/row.js +10 -2
- package/docs/data_fetching.md +20 -5
- package/docs/members_and_options.md +15 -6
- package/docs/mutations.md +5 -0
- package/docs/queries.md +3 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -16,11 +16,11 @@ signature; open the file.**
|
|
|
16
16
|
|---|---|
|
|
17
17
|
| [docs/recipes.md](./docs/recipes.md) | Task-shaped how-tos for the actions whose mechanism is not guessable from the hooks — returning a generated file, returning structured data, parameterized lookups, composable optional filters, cell decoding, testing an AI action without spending credits. |
|
|
18
18
|
| [docs/queries.md](./docs/queries.md) | **The query engine authoring reference** — AST node kinds, per-field-type operator support, filters/params/pruning, free-text search, combining tables (join/union/link/`unnest`/`record_id`), shaping (aggregates, date buckets, windows), runtime refinement bounds, limits & the efficiency playbook. |
|
|
19
|
-
| [docs/data_fetching.md](./docs/data_fetching.md) | The four read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`/`useCount` — the last for a number with no rows, sharing the `(alias, params, filter)` count key the paginated hook uses, so a list and a badge over one set buy one count; a count is a full scan and stays its OWN request so rows paint without waiting for it, and `rows.length` is never a count since rows truncate at 10,000 — `useQuery` says so with `truncated`), the `
|
|
19
|
+
| [docs/data_fetching.md](./docs/data_fetching.md) | The four read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`/`useCount` — the last for a number with no rows, sharing the `(alias, params, filter)` count key the paginated hook uses, so a list and a badge over one set buy one count; a count is a full scan and stays its OWN request so rows paint without waiting for it, and `rows.length` is never a count since rows truncate at 10,000 — `useQuery` says so with `truncated`), the ROW type (`RowOf` — the alias's projected columns and nothing else, values `unknown`; `__source_record_id`/`__source_table_id` typed but optional), runtime `sort`/`filter` keys AND the `useFieldOptions` map keyed against that same projection (`AppQueryColumns`, `ColumnKeyOf`), cell readers (`row.*` — `row.num` answers `null` for an EMPTY cell, so none is distinguishable from zero — `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`, and `readCreatedAt`/`readUpdatedAt` for the record timestamps every row-level result carries), caching — **arrival revalidates** (a re-mount renders cache *and* refreshes it in the background, `loading` never flips) — **realtime push** (a table one of your queries reads changes and that query refetches within about a second, alias-precise, records-only, host-embedded apps only) — and the fourth source, **this app's own successful write**, which re-reads every mounted query immediately rather than waiting on that push (→ [mutations](./docs/mutations.md)), data discipline, the pagination count as a second full execution (and `total` to suppress it), the search-as-you-type + record-picker patterns. |
|
|
20
20
|
| [docs/mutations.md](./docs/mutations.md) | `useWorkflow` (the ONLY write path), the `WorkflowResult` resolve-never-throw contract (`field_errors` locates a refusal on the control it belongs to, where `message` can only say it at the dialog's scope), typed inputs, the automatic re-read a SUCCESSFUL write triggers over every mounted query (so a screen never waits on the host push to see its own write), diff-before-update, locked records, `useOptimistic`, `useNewRecord` (client-minted `rec_*` id so a new-record surface never remounts on its first save), read-after-write ordering (a re-read must not overtake an in-flight write). |
|
|
21
21
|
| [docs/workflows.md](./docs/workflows.md) | **The workflow-BODY authoring reference** — the JS subset a `src/workflows/<alias>.ts` body may use: the parse-at-save/never-execute model, opaque `fld_*`/`opt_*` keys, expression sources + explicit `linked()` descent, every step form (tool call, `agent`, waits, `validate`, `return`), the accepted sugar and its canonical lowering, helpers + callback rules, record-write surfaces, the traps, the bright line, and the verify loop — `check` (the only local gate: the app's own `npm run typecheck` never sees a body) → `dry_run_workflow` (static green is not a run) → `set`. |
|
|
22
22
|
| [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload`, `useAttachments`, `readFiles`/presigned URLs (**a bearer credential for the bytes** — never logged, reported, or persisted), workflow-generated files, **naming a zip's entries** (`{ id, name }` per file — a file name, never a path), preview pairing, filter operators, the server-side delivery bounds. **Uploads declare a `fidelity`** (`standard` / `high` / `original`) — the app picks how much of the image survives storage; use `high` whenever text must stay legible. |
|
|
23
|
-
| [docs/members_and_options.md](./docs/members_and_options.md) | People + select options + comments — `useMembers`, `useFieldOptions
|
|
23
|
+
| [docs/members_and_options.md](./docs/members_and_options.md) | People + select options + comments — `useMembers`, `useFieldOptions` (keyed by the alias's OWN columns, every key optional), `useViewer`, `useComments` (each comment carries its own resolved `author`, so a thread crossing a role boundary is legible without declaring member access), and the `@lotics/ui` components they feed. |
|
|
24
24
|
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model), `useUrlState` + `urlParam` codecs, `useRecents`. |
|
|
25
25
|
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming ai-sdk `parts` → `AgentRun`, the agent's ask-back — `pendingChoice`/`answerChoice` over the parked `awaiting_input` state — and the `AgentRunLanding` every leg resolves), `askAi` — plus the fields-vs-file razor for choosing between them — and `useAiContext` (push the current screen's view state to the member's ambient chat agent; caps, push-only semantics; a chat mutation refetches your queries through the realtime channel, not a separate poke). **A `file` input carries its own content** — no reader tool to declare. **An agent reaches record DATA only through its declared `query_aliases` / `workflow_aliases`.** Also **what the member's own chat agent can do with your app while it is open** — the alias catalog it reads and how to shape a mutating alias for it. |
|
|
26
26
|
| [docs/security.md](./docs/security.md) | **Read before shipping** — the owner-principal model, `is_current_member` scoping, write attribution, group gates, public-app bounds, what runtime refinement cannot widen, and why a per-input bound is a tenancy floor rather than an authorization check (a caller-supplied id must be intersected with the record server-side). |
|
|
@@ -44,6 +44,9 @@ signature; open the file.**
|
|
|
44
44
|
column, including files. → [queries](./docs/queries.md)
|
|
45
45
|
- **Never hand-roll the serialization contract** — decode cells with the typed readers.
|
|
46
46
|
→ [data_fetching](./docs/data_fetching.md)
|
|
47
|
+
- **Read only the columns the alias projects** — its option sets too, from the alias that CARRIES
|
|
48
|
+
the column. Anything else is `undefined`, which every reader draws as a blank cell, so the row
|
|
49
|
+
type and the option map are bounded to the projection. → [data_fetching](./docs/data_fetching.md)
|
|
47
50
|
- **Narrow `__source_record_id` / `__source_table_id` before use** — a grouped query emits neither,
|
|
48
51
|
so an unchecked read hands a workflow (or an AI record ref) `undefined`.
|
|
49
52
|
→ [data_fetching](./docs/data_fetching.md)
|
package/dist/src/hooks.d.ts
CHANGED
|
@@ -7,12 +7,14 @@ import type { ResolvedOption } from "./select.js";
|
|
|
7
7
|
export type { AgentRunState, AgentUIPart, PendingChoice, ChoiceQuestion, ChoiceOption, AskUserChoiceOutput, AgentRunLanding } from "./agent_stream.js";
|
|
8
8
|
export { buildChoiceOutput } from "./agent_stream.js";
|
|
9
9
|
/**
|
|
10
|
-
* One row of a query result:
|
|
11
|
-
*
|
|
10
|
+
* One row of a query result whose COLUMNS are not known: a dynamic alias, or a
|
|
11
|
+
* query whose AST names no projection (a bare `from_table`). Where codegen could
|
|
12
|
+
* read them, `RowOf` states them instead and there is no index signature — see
|
|
13
|
+
* that type, which is where the reason lives.
|
|
12
14
|
*
|
|
13
|
-
* The projected values stay `unknown`
|
|
14
|
-
* the
|
|
15
|
-
*
|
|
15
|
+
* The projected values stay `unknown` either way: a column's TYPE is the field's
|
|
16
|
+
* and the manifest does not carry it, so the cell readers (`row.text`,
|
|
17
|
+
* `readLinks`, `readFiles`, …) are what narrow them.
|
|
16
18
|
*
|
|
17
19
|
* The `__source_*` columns are different in kind: the compiler injects them at
|
|
18
20
|
* every layer, and they are the only way to address the RECORD a row came from
|
|
@@ -41,6 +43,27 @@ export interface QueryRow {
|
|
|
41
43
|
/** The table that record lives in. Absent on a grouped/aggregated row. */
|
|
42
44
|
__source_table_id?: string;
|
|
43
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The row type of alias `K`: the columns that alias PROJECTS, plus the two
|
|
48
|
+
* addressing columns, and nothing else.
|
|
49
|
+
*
|
|
50
|
+
* `.lotics/app_queries.d.ts` already names every projected column
|
|
51
|
+
* (`AppQueryColumns`), read off the query's AST by the same rule the server
|
|
52
|
+
* names them by. A read is bounded to that union for the same reason a
|
|
53
|
+
* `filter`/`sort` key is — except the failure is quieter. A key the query does
|
|
54
|
+
* not carry is refused by the server; a COLUMN it does not carry comes back
|
|
55
|
+
* `undefined`, which every cell reader answers with its empty value, so a
|
|
56
|
+
* misspelt column, a renamed field and a projection moved to another alias all
|
|
57
|
+
* render as a blank cell in a member's browser with no error anywhere. Removing
|
|
58
|
+
* the index signature is what turns that silence into a `tsc` error, which is
|
|
59
|
+
* what `lotics app check` refuses to ship.
|
|
60
|
+
*
|
|
61
|
+
* An alias with no `AppQueryColumns` entry keeps `QueryRow` — a union that could
|
|
62
|
+
* be wrong is worse than none, the rule `ColumnKeyOf` already follows.
|
|
63
|
+
*/
|
|
64
|
+
export type RowOf<K extends string> = K extends keyof AppQueryColumns ? {
|
|
65
|
+
[C in AppQueryColumns[K] & string]: unknown;
|
|
66
|
+
} & Pick<QueryRow, "__source_record_id" | "__source_table_id"> : QueryRow;
|
|
44
67
|
/** Fields shared by every query hook's return value. */
|
|
45
68
|
interface QueryStateBase {
|
|
46
69
|
/**
|
|
@@ -344,9 +367,10 @@ type QueryArgs<K extends string, O> = K extends keyof AppQueries ? AppQueries[K]
|
|
|
344
367
|
*
|
|
345
368
|
* Per-app CLI codegen writes `.lotics/app_queries.d.ts` augmenting `AppQueries`
|
|
346
369
|
* with the declared alias → param-type map, so an undeclared alias is a
|
|
347
|
-
* compile-time error and params are typed per the manifest.
|
|
370
|
+
* compile-time error and params are typed per the manifest. The same file names
|
|
371
|
+
* the alias's projected columns, which is what bounds a ROW read — see `RowOf`.
|
|
348
372
|
*/
|
|
349
|
-
export declare function useQuery<K extends string>(alias: K, ...args: QueryArgs<K, QueryOptions<ColumnKeyOf<K>>>): QueryState<
|
|
373
|
+
export declare function useQuery<K extends string>(alias: K, ...args: QueryArgs<K, QueryOptions<ColumnKeyOf<K>>>): QueryState<RowOf<K>>;
|
|
350
374
|
/** The resolved option set of one select column, plus an index for value
|
|
351
375
|
* rendering. The companion to a query row, for select fields. */
|
|
352
376
|
export interface FieldOptions {
|
|
@@ -366,14 +390,20 @@ export interface FieldOptions {
|
|
|
366
390
|
*/
|
|
367
391
|
byKey: (key: string) => ResolvedOption | undefined;
|
|
368
392
|
}
|
|
369
|
-
/** Return value of `useFieldOptions`.
|
|
370
|
-
|
|
393
|
+
/** Return value of `useFieldOptions`. `C` is the column key's type — the alias's
|
|
394
|
+
* projected outputs, see `ColumnKeyOf`. */
|
|
395
|
+
export interface FieldOptionsState<C extends string = string> {
|
|
371
396
|
/**
|
|
372
|
-
* Resolved option sets keyed by the query's OUTPUT column name
|
|
373
|
-
*
|
|
374
|
-
*
|
|
397
|
+
* Resolved option sets keyed by the query's OUTPUT column name — the alias's
|
|
398
|
+
* own columns, so a key belonging to another query is a `tsc` error rather
|
|
399
|
+
* than a `[]` picker nobody can open.
|
|
400
|
+
*
|
|
401
|
+
* EVERY key is optional, and that is the shape rather than a hedge: a select
|
|
402
|
+
* column the server could not resolve to a source field (a UNION output whose
|
|
403
|
+
* arms disagree, a computed column) is genuinely absent, and a non-select
|
|
404
|
+
* column never had options. Read through it (`fields.status?.options ?? []`).
|
|
375
405
|
*/
|
|
376
|
-
fields: Record<
|
|
406
|
+
fields: Partial<Record<C, FieldOptions>>;
|
|
377
407
|
loading: boolean;
|
|
378
408
|
isValidating: boolean;
|
|
379
409
|
error: string | null;
|
|
@@ -395,8 +425,11 @@ export interface FieldOptionsOptions {
|
|
|
395
425
|
* added/removed option flows through with no app change.
|
|
396
426
|
*
|
|
397
427
|
* Addressed by the same alias you query: the option sets resolve from the named
|
|
398
|
-
* query's output columns, scoped exactly like running it
|
|
399
|
-
*
|
|
428
|
+
* query's output columns, scoped exactly like running it, and `fields` is keyed
|
|
429
|
+
* by that same union — the alias that CARRIES the column is the one to ask, and
|
|
430
|
+
* asking another is a compile error instead of an empty picker. A column the
|
|
431
|
+
* server can't map to a source select field (UNION output, computed column) is
|
|
432
|
+
* absent, so every key is optional.
|
|
400
433
|
*
|
|
401
434
|
* ```tsx
|
|
402
435
|
* const { fields } = useFieldOptions("records");
|
|
@@ -407,7 +440,7 @@ export interface FieldOptionsOptions {
|
|
|
407
440
|
* <Status option={fields.status?.byKey(readSelect(r.status)[0]?.key ?? "")} />
|
|
408
441
|
* ```
|
|
409
442
|
*/
|
|
410
|
-
export declare function useFieldOptions<K extends keyof AppQueries & string>(alias: K, opts?: FieldOptionsOptions): FieldOptionsState
|
|
443
|
+
export declare function useFieldOptions<K extends keyof AppQueries & string>(alias: K, opts?: FieldOptionsOptions): FieldOptionsState<ColumnKeyOf<K>>;
|
|
411
444
|
export declare function useFieldOptions(alias: string, opts?: FieldOptionsOptions): FieldOptionsState;
|
|
412
445
|
/**
|
|
413
446
|
* Like `useQuery` but append/load-more: the first render loads one page and
|
|
@@ -418,7 +451,7 @@ export declare function useFieldOptions(alias: string, opts?: FieldOptionsOption
|
|
|
418
451
|
* const { rows, loadMore, hasMore } = useInfiniteQuery("feed", {}, { pageSize: 30 });
|
|
419
452
|
* ```
|
|
420
453
|
*/
|
|
421
|
-
export declare function useInfiniteQuery<K extends string>(alias: K, ...args: QueryArgs<K, InfiniteQueryOptions<ColumnKeyOf<K>>>): InfiniteQueryState<
|
|
454
|
+
export declare function useInfiniteQuery<K extends string>(alias: K, ...args: QueryArgs<K, InfiniteQueryOptions<ColumnKeyOf<K>>>): InfiniteQueryState<RowOf<K>>;
|
|
422
455
|
/**
|
|
423
456
|
* Page-model query with a total — the data hook behind a numbered, jumpable
|
|
424
457
|
* table (pairs with `@lotics/ui/pagination`). It owns the page
|
|
@@ -433,7 +466,7 @@ export declare function useInfiniteQuery<K extends string>(alias: K, ...args: Qu
|
|
|
433
466
|
* usePaginatedQuery("orders", { q }, { pageSize: 25, sort, filter });
|
|
434
467
|
* ```
|
|
435
468
|
*/
|
|
436
|
-
export declare function usePaginatedQuery<K extends string>(alias: K, ...args: QueryArgs<K, PaginatedQueryOptions<ColumnKeyOf<K>>>): PaginatedQueryState<
|
|
469
|
+
export declare function usePaginatedQuery<K extends string>(alias: K, ...args: QueryArgs<K, PaginatedQueryOptions<ColumnKeyOf<K>>>): PaginatedQueryState<RowOf<K>>;
|
|
437
470
|
/**
|
|
438
471
|
* HOW MANY rows a query matches — one number, no rows fetched.
|
|
439
472
|
*
|
|
@@ -617,9 +650,11 @@ interface MembersState {
|
|
|
617
650
|
/** Options for `useMembers`. */
|
|
618
651
|
export interface MembersOptions {
|
|
619
652
|
/**
|
|
620
|
-
* Restrict to one member group
|
|
621
|
-
* `
|
|
622
|
-
*
|
|
653
|
+
* Restrict to one member group, as `GRP.<group>` from `.lotics/app_fields.ts`
|
|
654
|
+
* (a pasted `grp_…` id is refused by `lotics app check` — it resolves to
|
|
655
|
+
* nothing in a copy of the app). The group must be declared on a `member`
|
|
656
|
+
* workflow input's `group` — listing an undeclared group errors. Omit to list
|
|
657
|
+
* the whole org roster.
|
|
623
658
|
*/
|
|
624
659
|
group?: string;
|
|
625
660
|
}
|
|
@@ -636,7 +671,7 @@ export interface MembersOptions {
|
|
|
636
671
|
* `group` — so an app can only list (and assign into) groups it declares.
|
|
637
672
|
*
|
|
638
673
|
* ```tsx
|
|
639
|
-
* const { members } = useMembers({ group:
|
|
674
|
+
* const { members } = useMembers({ group: GRP.sale });
|
|
640
675
|
* // <Select variant="native" options={members.map((m) => ({
|
|
641
676
|
* // value: m.id, label: m.name || m.email || m.id, image: m.image,
|
|
642
677
|
* // }))} />
|
package/dist/src/hooks.js
CHANGED
|
@@ -557,7 +557,7 @@ export function useAiContext(slot, context) {
|
|
|
557
557
|
* `group` — so an app can only list (and assign into) groups it declares.
|
|
558
558
|
*
|
|
559
559
|
* ```tsx
|
|
560
|
-
* const { members } = useMembers({ group:
|
|
560
|
+
* const { members } = useMembers({ group: GRP.sale });
|
|
561
561
|
* // <Select variant="native" options={members.map((m) => ({
|
|
562
562
|
* // value: m.id, label: m.name || m.email || m.id, image: m.image,
|
|
563
563
|
* // }))} />
|
package/dist/src/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
export { mount } from "./mount.js";
|
|
17
17
|
export type { MountOptions } from "./mount.js";
|
|
18
18
|
export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useCount, useFieldOptions, useFileUpload, useAttachments, useMembers, useAgentRun, useAgentRuns, useAiContext, buildChoiceOutput, } from "./hooks.js";
|
|
19
|
-
export type { QueryRow, UploadedFile, AttachedFile, BaseQueryOptions, QueryOptions, InfiniteQueryOptions, PaginatedQueryOptions, CountOptions, ColumnKeyOf, QuerySortKey, QueryFilter, QueryFilterCondition, QueryFilterFieldCondition, QueryFilterRecordIdCondition, QueryFilterGroup, WorkflowResult, MembersOptions, AgentRunOptions, UseAgentRun, AgentRunLanding, AgentRunRecord, AgentRunState, AgentUIPart, PendingChoice, ChoiceQuestion, ChoiceOption, AskUserChoiceOutput, FieldOptions, FieldOptionsState, FieldOptionsOptions, } from "./hooks.js";
|
|
19
|
+
export type { QueryRow, RowOf, UploadedFile, AttachedFile, BaseQueryOptions, QueryOptions, InfiniteQueryOptions, PaginatedQueryOptions, CountOptions, ColumnKeyOf, QuerySortKey, QueryFilter, QueryFilterCondition, QueryFilterFieldCondition, QueryFilterRecordIdCondition, QueryFilterGroup, WorkflowResult, MembersOptions, AgentRunOptions, UseAgentRun, AgentRunLanding, AgentRunRecord, AgentRunState, AgentUIPart, PendingChoice, ChoiceQuestion, ChoiceOption, AskUserChoiceOutput, FieldOptions, FieldOptionsState, FieldOptionsOptions, } from "./hooks.js";
|
|
20
20
|
export { useComments, useCommentCounts } from "./comments.js";
|
|
21
21
|
export type { AppComment, AppCommentAuthor, AppCommentFile, CommentsState, UseCommentsArgs, CommentCountsState, UseCommentCountsArgs, } from "./comments.js";
|
|
22
22
|
export { useViewer } from "./viewer.js";
|
package/dist/src/row.js
CHANGED
|
@@ -87,7 +87,15 @@ function datetime(v) {
|
|
|
87
87
|
return null;
|
|
88
88
|
return new Date(Number(m[1]), (m[2] ? Number(m[2]) : 1) - 1, m[3] ? Number(m[3]) : 1, m[4] ? Number(m[4]) : 0, m[5] ? Number(m[5]) : 0);
|
|
89
89
|
}
|
|
90
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* One `{ id, display }` object → ResolvedLink, or null if absent/malformed.
|
|
92
|
+
*
|
|
93
|
+
* A MISSING `display` IS NOT A LINK. Other cells carry `{ id }` without one — a
|
|
94
|
+
* member is the everyday case — and answering `display: ""` for them made every
|
|
95
|
+
* assignee read as a blank-named link wherever a reader tried links first. A
|
|
96
|
+
* link the server resolved always states its display, even as the empty string,
|
|
97
|
+
* so requiring the KEY costs a real link nothing.
|
|
98
|
+
*/
|
|
91
99
|
function asLink(v) {
|
|
92
100
|
if (!v || typeof v !== "object")
|
|
93
101
|
return null;
|
|
@@ -95,7 +103,7 @@ function asLink(v) {
|
|
|
95
103
|
if (typeof id !== "string" || !id)
|
|
96
104
|
return null;
|
|
97
105
|
const display = v.display;
|
|
98
|
-
return
|
|
106
|
+
return typeof display === "string" ? { id, display } : null;
|
|
99
107
|
}
|
|
100
108
|
/**
|
|
101
109
|
* select_record_link → the FIRST linked record as `{ id, display }` (or null).
|
package/docs/data_fetching.md
CHANGED
|
@@ -116,6 +116,11 @@ sort for a typed alias names its keys `ColumnKeyOf<"alias">` — `QueryFilter<Co
|
|
|
116
116
|
`QuerySortKey<…>`; `QueryFilterCondition` and `QueryFilterGroup` take the same parameter — so the
|
|
117
117
|
check reaches the helper that derives the key, not only the hook call that sends it.
|
|
118
118
|
|
|
119
|
+
**The option map is keyed the same way.** `useFieldOptions(alias).fields` is keyed by that alias's
|
|
120
|
+
projected columns too — ask the alias that CARRIES the column, because a picker fed from a sibling
|
|
121
|
+
query's map is otherwise a control that renders empty with nothing reporting it. Every key is
|
|
122
|
+
optional: [./members_and_options.md](./members_and_options.md).
|
|
123
|
+
|
|
119
124
|
`@lotics/ui`'s `columnFilterToConditions` carries the same parameter (`FilterableColumn<C>` →
|
|
120
125
|
`FilterConditionNode<C>`), so a per-column filter UI built over a typed alias composes without a
|
|
121
126
|
cast.
|
|
@@ -411,10 +416,17 @@ workflows), `__created_at` / `__updated_at` (record timestamps), and per-project
|
|
|
411
416
|
metadata. A grouped query collapses rows and emits none of these. Details:
|
|
412
417
|
[./queries.md](./queries.md).
|
|
413
418
|
|
|
414
|
-
A row is typed
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
419
|
+
A row is typed from the alias's own projection. Codegen names the columns each query carries
|
|
420
|
+
(`AppQueryColumns` in `.lotics/app_queries.d.ts`) and a row read is bounded to them, so a misspelt
|
|
421
|
+
column, a renamed field and a projection moved to a sibling alias are one `tsc` error rather than a
|
|
422
|
+
blank cell — the server answers an unprojected column with `undefined`, and every reader below
|
|
423
|
+
answers `undefined` with its empty value, so nothing else reports it. The values stay `unknown` (a
|
|
424
|
+
column's TYPE is the field's, which the manifest does not carry, so the readers are the narrowing),
|
|
425
|
+
while `__source_record_id` / `__source_table_id` are typed `string | undefined` — reachable without
|
|
426
|
+
a cast, but only after narrowing, because "a grouped query emits none of these" is a fact the type
|
|
427
|
+
states rather than one you have to remember. Write the row type down as `RowOf<"alias">`; an alias
|
|
428
|
+
whose columns cannot be read off its AST keeps the open `QueryRow`, where any column compiles and
|
|
429
|
+
the server's answer is the only check — the rule the filter keys follow too.
|
|
418
430
|
|
|
419
431
|
### The readers
|
|
420
432
|
|
|
@@ -473,7 +485,10 @@ never carry avatar images — the avatar lives on the `useMembers` roster
|
|
|
473
485
|
|
|
474
486
|
**`readLinks` / `row.link`.** `display` is the linked record's primary-field text — render it;
|
|
475
487
|
use `id` to correlate, filter (link `has_any_of`), or fetch detail. `row.link` reads the first
|
|
476
|
-
entry only — on a multi-link field use `readLinks`.
|
|
488
|
+
entry only — on a multi-link field use `readLinks`. An entry carrying an `id` and **no
|
|
489
|
+
`display` key** is not a link and is skipped: a member cell has that shape, so reading one as a
|
|
490
|
+
link would otherwise render every assignee under a blank name. Read a member cell with
|
|
491
|
+
`readMembers`.
|
|
477
492
|
|
|
478
493
|
**`readFiles`.** Each entry's `url` (and `thumbnail_url` for images) is **presigned with a 24-hour
|
|
479
494
|
TTL** — it renders directly in an `<Image>`/preview and works from the sandboxed iframe and for
|
|
@@ -68,8 +68,11 @@ const { fields } = useFieldOptions("orders"); // same alias you query
|
|
|
68
68
|
|
|
69
69
|
### What comes back
|
|
70
70
|
|
|
71
|
-
`fields` is
|
|
72
|
-
|
|
71
|
+
`fields` is keyed by the query's **output column name**, not the field key — and by *that alias's*
|
|
72
|
+
columns: `Partial<Record<ColumnKeyOf<"alias">, FieldOptions>>`. A key the alias does not project is
|
|
73
|
+
a compile error, so a picker fed from a sibling query's option map fails at your desk instead of
|
|
74
|
+
rendering with no options; and every key is optional, so the read is through `?.` and never a bare
|
|
75
|
+
`.options`. Each `FieldOptions`:
|
|
73
76
|
|
|
74
77
|
| Property | Meaning |
|
|
75
78
|
| --- | --- |
|
|
@@ -80,8 +83,8 @@ name**, not the field key. Each `FieldOptions`:
|
|
|
80
83
|
- `color` is a named palette token (e.g. `"blue"`, `"emerald"`). Pass the option straight to
|
|
81
84
|
`@lotics/ui`'s `Status`; a missing/unrecognized token degrades to a neutral badge.
|
|
82
85
|
- **A column the server can't map to a single source select field is simply absent** from
|
|
83
|
-
`fields` — a UNION output whose arms disagree on the source field, or a computed column.
|
|
84
|
-
|
|
86
|
+
`fields` — a UNION output whose arms disagree on the source field, or a computed column. That is
|
|
87
|
+
why the type makes every key optional: `fields.status?.options ?? []`.
|
|
85
88
|
- Addressed by the same alias you query, and scoped exactly like running that query — it exposes
|
|
86
89
|
nothing the query itself doesn't. Params are irrelevant (they only fill filter values, never
|
|
87
90
|
change the projection), so no params argument exists.
|
|
@@ -147,7 +150,7 @@ contract as `readSelect`: `[]` for null/empty/unexpected cells, malformed entrie
|
|
|
147
150
|
The candidate set for an "assign to a member" picker (`dist/src/hooks.d.ts`):
|
|
148
151
|
|
|
149
152
|
```tsx
|
|
150
|
-
const { members, loading, error } = useMembers({ group:
|
|
153
|
+
const { members, loading, error } = useMembers({ group: GRP.fulfillment });
|
|
151
154
|
<MemberSelect members={members} value={assignee} onValueChange={setAssignee} />
|
|
152
155
|
```
|
|
153
156
|
|
|
@@ -175,11 +178,17 @@ three gates are observable as an `error` on the hook:
|
|
|
175
178
|
member declaration gets: *"This app has not declared member access — listing members requires a
|
|
176
179
|
declared workflow `member` input or query `member` param."* Member access is a declared,
|
|
177
180
|
auditable surface, like queries and workflows.
|
|
178
|
-
3. **Declared groups only.** `{ group
|
|
181
|
+
3. **Declared groups only.** `{ group }` restricts the roster to one member group — but
|
|
179
182
|
only a group declared on some member input's `group` property. An undeclared group errors; an
|
|
180
183
|
app can only enumerate groups it actually assigns into. A declared group with no members
|
|
181
184
|
resolves to `{ members: [] }`.
|
|
182
185
|
|
|
186
|
+
Name the group `GRP.<group>`, from the generated `.lotics/app_fields.ts` — `GRP` is that workspace's
|
|
187
|
+
group directory keyed by slugified display name, so the same source resolves in every copy of the
|
|
188
|
+
app. A pasted `grp_…` id is an id one workspace minted: it resolves to nothing anywhere else, and
|
|
189
|
+
`lotics app check` refuses it. (The manifest DECLARATION below is the exception — it carries the
|
|
190
|
+
concrete id, and publish inverts it.)
|
|
191
|
+
|
|
183
192
|
```jsonc
|
|
184
193
|
// package.json → "lotics" — the declaration that unlocks useMembers
|
|
185
194
|
"workflows": {
|
package/docs/mutations.md
CHANGED
|
@@ -81,6 +81,11 @@ convert every failure into a resolved `{ status: "error", message }`:
|
|
|
81
81
|
| Alias not bound / workflow deleted | `status: "error"` with the explanatory message |
|
|
82
82
|
| Gateway timeout (a 524 on a long run), any 5xx, a non-JSON error page | `status: "error"` with a friendly, **body-free** message — never raw gateway HTML |
|
|
83
83
|
|
|
84
|
+
A workflow call is **synchronous end to end** — about 100 s at the edge, 30 s through the bridged
|
|
85
|
+
host — and a timed-out call may still have committed. So model work that can run for minutes belongs
|
|
86
|
+
in an agent run, which is asynchronous by construction and polls to completion, never in a workflow
|
|
87
|
+
step.
|
|
88
|
+
|
|
84
89
|
So the correct handling is:
|
|
85
90
|
|
|
86
91
|
```tsx
|
package/docs/queries.md
CHANGED
|
@@ -114,7 +114,9 @@ A result is `{ rows, columns, source_fields_by_table_id }` (`+ total` for a coun
|
|
|
114
114
|
`columns` is the statically computed output schema — `{ name, type, nullable?,
|
|
115
115
|
source_table_id?, source_field_key? }` per column. Rows are plain objects keyed by output
|
|
116
116
|
column name; decode cells with the SDK readers (`row.*`, `readSelect`, `readMembers`,
|
|
117
|
-
`readLinks`, `readFiles` — see [data_fetching.md](./data_fetching.md)).
|
|
117
|
+
`readLinks`, `readFiles` — see [data_fetching.md](./data_fetching.md)). Those output names are also
|
|
118
|
+
the row's TYPE: codegen writes them into `AppQueryColumns`, so reading a column this query does not
|
|
119
|
+
project is a `tsc` error.
|
|
118
120
|
|
|
119
121
|
**`source_fields_by_table_id` is always `{}`** — the key is part of the shape, the map is not
|
|
120
122
|
filled. The server reads the source tables' field definitions to resolve option and member
|