@lotics/app-sdk 0.62.2 → 0.62.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/AGENTS.md +4 -1
- package/dist/src/comments.d.ts +11 -3
- package/dist/src/comments.js +11 -3
- package/dist/src/hooks.d.ts +52 -8
- package/dist/src/hooks.js +8 -1
- package/dist/src/index.d.ts +1 -1
- package/docs/ai.md +15 -2
- package/docs/data_fetching.md +5 -0
- package/docs/files.md +3 -0
- package/docs/members_and_options.md +10 -5
- package/docs/queries.md +12 -4
- package/package.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -15,7 +15,7 @@ signature; open the file.**
|
|
|
15
15
|
| Doc | Read it for |
|
|
16
16
|
|---|---|
|
|
17
17
|
| [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. |
|
|
18
|
-
| [docs/data_fetching.md](./docs/data_fetching.md) | The three read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`), cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`), `useFieldOptions`, data discipline, the search-as-you-type + record-picker patterns. |
|
|
18
|
+
| [docs/data_fetching.md](./docs/data_fetching.md) | The three read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`), the `QueryRow` shape (projected columns `unknown`; `__source_record_id`/`__source_table_id` typed but optional), cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`), `useFieldOptions`, data discipline, the search-as-you-type + record-picker patterns. |
|
|
19
19
|
| [docs/mutations.md](./docs/mutations.md) | `useWorkflow` (the ONLY write path), the `WorkflowResult` resolve-never-throw contract, typed inputs, diff-before-update, locked records, `useOptimistic`, read-after-write ordering (a re-read must not overtake an in-flight write). |
|
|
20
20
|
| [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 + link 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`. |
|
|
21
21
|
| [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. |
|
|
@@ -38,6 +38,9 @@ signature; open the file.**
|
|
|
38
38
|
column, including files. → [queries](./docs/queries.md)
|
|
39
39
|
- **Never hand-roll the serialization contract** — decode cells with the typed readers.
|
|
40
40
|
→ [data_fetching](./docs/data_fetching.md)
|
|
41
|
+
- **Narrow `__source_record_id` / `__source_table_id` before use** — a grouped query emits neither,
|
|
42
|
+
so an unchecked read hands a workflow (or an AI record ref) `undefined`.
|
|
43
|
+
→ [data_fetching](./docs/data_fetching.md)
|
|
41
44
|
- **Errors fail loud** — no swallowed catches, no silent fallbacks.
|
|
42
45
|
|
|
43
46
|
## Keeping this reference current
|
package/dist/src/comments.d.ts
CHANGED
|
@@ -62,10 +62,18 @@ export interface UseCommentsArgs {
|
|
|
62
62
|
record_id: string;
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
+
* `record_id` must be a REAL id. The fetch keys on `available` alone, so an
|
|
66
|
+
* empty string is not skipped — it fetches comments for nothing. When the id
|
|
67
|
+
* comes off a row's `__source_record_id` addressing column, narrow it first: a
|
|
68
|
+
* grouped query carries none, and a hook cannot be called conditionally, so the
|
|
69
|
+
* guard belongs at the component that renders the panel.
|
|
70
|
+
*
|
|
65
71
|
* ```tsx
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
72
|
+
* // In a record screen, where an id definitionally exists:
|
|
73
|
+
* const { comments, available, createComment } = useComments({ record_id: recordId });
|
|
74
|
+
*
|
|
75
|
+
* // From a row, gate the whole panel rather than passing a placeholder:
|
|
76
|
+
* {row.__source_record_id && <CommentsPanel recordId={row.__source_record_id} />}
|
|
69
77
|
* ```
|
|
70
78
|
*/
|
|
71
79
|
export declare function useComments(args: UseCommentsArgs): CommentsState;
|
package/dist/src/comments.js
CHANGED
|
@@ -38,10 +38,18 @@ function toStorageFiles(files) {
|
|
|
38
38
|
}
|
|
39
39
|
let optimisticCounter = 0;
|
|
40
40
|
/**
|
|
41
|
+
* `record_id` must be a REAL id. The fetch keys on `available` alone, so an
|
|
42
|
+
* empty string is not skipped — it fetches comments for nothing. When the id
|
|
43
|
+
* comes off a row's `__source_record_id` addressing column, narrow it first: a
|
|
44
|
+
* grouped query carries none, and a hook cannot be called conditionally, so the
|
|
45
|
+
* guard belongs at the component that renders the panel.
|
|
46
|
+
*
|
|
41
47
|
* ```tsx
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
48
|
+
* // In a record screen, where an id definitionally exists:
|
|
49
|
+
* const { comments, available, createComment } = useComments({ record_id: recordId });
|
|
50
|
+
*
|
|
51
|
+
* // From a row, gate the whole panel rather than passing a placeholder:
|
|
52
|
+
* {row.__source_record_id && <CommentsPanel recordId={row.__source_record_id} />}
|
|
45
53
|
* ```
|
|
46
54
|
*/
|
|
47
55
|
export function useComments(args) {
|
package/dist/src/hooks.d.ts
CHANGED
|
@@ -6,6 +6,41 @@ import type { ResolvedMember } from "./members.js";
|
|
|
6
6
|
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
|
+
/**
|
|
10
|
+
* One row of a query result: the projected columns, plus the platform's
|
|
11
|
+
* ADDRESSING columns.
|
|
12
|
+
*
|
|
13
|
+
* The projected values stay `unknown` — which columns a row carries is fixed by
|
|
14
|
+
* the query's manifest declaration, not by this type, so the cell readers
|
|
15
|
+
* (`row.text`, `readLinks`, `readFiles`, …) are what narrow them.
|
|
16
|
+
*
|
|
17
|
+
* The `__source_*` columns are different in kind: the compiler injects them at
|
|
18
|
+
* every layer, and they are the only way to address the RECORD a row came from
|
|
19
|
+
* — what `useComments`, a workflow's `record_id` input, and `useAiContext`'s
|
|
20
|
+
* record refs all need. Typed as `unknown` they were unusable without a cast,
|
|
21
|
+
* which is how this SDK's own examples came to show code that does not compile.
|
|
22
|
+
*
|
|
23
|
+
* They are OPTIONAL, and that is the honest shape rather than a hedge: a
|
|
24
|
+
* grouped query collapses rows, so its output has no originating record and the
|
|
25
|
+
* compiler emits no addressing columns for it (the platform's own `record_id`
|
|
26
|
+
* filter refuses such a query for exactly this reason). Narrow before use —
|
|
27
|
+
* on an aggregate row these are genuinely absent, not merely unproven.
|
|
28
|
+
*
|
|
29
|
+
* The line for what belongs here is "consumed RAW", not "starts with
|
|
30
|
+
* `__source_`". A row also carries `__source_locked`, `__created_at` /
|
|
31
|
+
* `__updated_at` and per-projection `__src_field_*`, and each of those has a
|
|
32
|
+
* reader that owns its decoding (`readLocked`, the `row.*` helpers) — a reader
|
|
33
|
+
* IS the narrowing, so a type here would duplicate it. Only these two are
|
|
34
|
+
* handed straight to a `record_id` / `table_id` parameter with nothing in
|
|
35
|
+
* between, which is why only these two needed a type.
|
|
36
|
+
*/
|
|
37
|
+
export interface QueryRow {
|
|
38
|
+
[column: string]: unknown;
|
|
39
|
+
/** The record this row came from. Absent on a grouped/aggregated row. */
|
|
40
|
+
__source_record_id?: string;
|
|
41
|
+
/** The table that record lives in. Absent on a grouped/aggregated row. */
|
|
42
|
+
__source_table_id?: string;
|
|
43
|
+
}
|
|
9
44
|
/** Fields shared by every query hook's return value. */
|
|
10
45
|
interface QueryStateBase {
|
|
11
46
|
/**
|
|
@@ -152,7 +187,9 @@ export interface PaginatedQueryOptions extends BaseQueryOptions {
|
|
|
152
187
|
*
|
|
153
188
|
* ```tsx
|
|
154
189
|
* const issue = useWorkflow("issueInvoiceStorageDrop");
|
|
155
|
-
*
|
|
190
|
+
* // Guard the addressing column: a grouped query's rows carry none, and the
|
|
191
|
+
* // workflow would be handed `undefined` where it declares a record.
|
|
192
|
+
* if (row.__source_record_id) await issue({ record_id: row.__source_record_id });
|
|
156
193
|
* ```
|
|
157
194
|
*/
|
|
158
195
|
export declare function useWorkflow<K extends keyof AppWorkflows & string>(alias: K): UseWorkflowFn<K>;
|
|
@@ -199,8 +236,8 @@ type QueryArgs<K extends keyof AppQueries & string, O> = AppQueries[K] extends R
|
|
|
199
236
|
* with the declared alias → param-type map, so an undeclared alias is a
|
|
200
237
|
* compile-time error and params are typed per the manifest.
|
|
201
238
|
*/
|
|
202
|
-
export declare function useQuery<K extends keyof AppQueries & string>(alias: K, ...args: QueryArgs<K, QueryOptions>): QueryState<
|
|
203
|
-
export declare function useQuery(alias: string, params?: Record<string, unknown>, opts?: QueryOptions): QueryState<
|
|
239
|
+
export declare function useQuery<K extends keyof AppQueries & string>(alias: K, ...args: QueryArgs<K, QueryOptions>): QueryState<QueryRow>;
|
|
240
|
+
export declare function useQuery(alias: string, params?: Record<string, unknown>, opts?: QueryOptions): QueryState<QueryRow>;
|
|
204
241
|
/** The resolved option set of one select column, plus an index for value
|
|
205
242
|
* rendering. The companion to a query row, for select fields. */
|
|
206
243
|
export interface FieldOptions {
|
|
@@ -272,8 +309,8 @@ export declare function useFieldOptions(alias: string, opts?: FieldOptionsOption
|
|
|
272
309
|
* const { rows, loadMore, hasMore } = useInfiniteQuery("feed", {}, { pageSize: 30 });
|
|
273
310
|
* ```
|
|
274
311
|
*/
|
|
275
|
-
export declare function useInfiniteQuery<K extends keyof AppQueries & string>(alias: K, ...args: QueryArgs<K, InfiniteQueryOptions>): InfiniteQueryState<
|
|
276
|
-
export declare function useInfiniteQuery(alias: string, params?: Record<string, unknown>, opts?: InfiniteQueryOptions): InfiniteQueryState<
|
|
312
|
+
export declare function useInfiniteQuery<K extends keyof AppQueries & string>(alias: K, ...args: QueryArgs<K, InfiniteQueryOptions>): InfiniteQueryState<QueryRow>;
|
|
313
|
+
export declare function useInfiniteQuery(alias: string, params?: Record<string, unknown>, opts?: InfiniteQueryOptions): InfiniteQueryState<QueryRow>;
|
|
277
314
|
/**
|
|
278
315
|
* Page-model query with a total — the data hook behind a numbered, jumpable
|
|
279
316
|
* table (pairs with `@lotics/ui/pagination`). It owns the page
|
|
@@ -288,8 +325,8 @@ export declare function useInfiniteQuery(alias: string, params?: Record<string,
|
|
|
288
325
|
* usePaginatedQuery("orders", { q }, { pageSize: 25, sort, filter });
|
|
289
326
|
* ```
|
|
290
327
|
*/
|
|
291
|
-
export declare function usePaginatedQuery<K extends keyof AppQueries & string>(alias: K, ...args: QueryArgs<K, PaginatedQueryOptions>): PaginatedQueryState<
|
|
292
|
-
export declare function usePaginatedQuery(alias: string, params?: Record<string, unknown>, opts?: PaginatedQueryOptions): PaginatedQueryState<
|
|
328
|
+
export declare function usePaginatedQuery<K extends keyof AppQueries & string>(alias: K, ...args: QueryArgs<K, PaginatedQueryOptions>): PaginatedQueryState<QueryRow>;
|
|
329
|
+
export declare function usePaginatedQuery(alias: string, params?: Record<string, unknown>, opts?: PaginatedQueryOptions): PaginatedQueryState<QueryRow>;
|
|
293
330
|
/** A file the host has stored and resolved serving URLs for. */
|
|
294
331
|
export interface UploadedFile {
|
|
295
332
|
id: string;
|
|
@@ -412,7 +449,14 @@ export declare function useAttachments(): AttachmentsState;
|
|
|
412
449
|
* ```tsx
|
|
413
450
|
* useAiContext("orders_list", {
|
|
414
451
|
* description: `Viewing ${rows.length} orders filtered to status=open, sorted by due date.`,
|
|
415
|
-
*
|
|
452
|
+
* // `flatMap` + the guard, not `map`: the addressing columns are absent on a
|
|
453
|
+
* // GROUPED query's rows, so a ref built without checking carries `undefined`
|
|
454
|
+
* // and points the agent at nothing.
|
|
455
|
+
* records: rows.flatMap((r) =>
|
|
456
|
+
* r.__source_table_id && r.__source_record_id
|
|
457
|
+
* ? [{ table_id: r.__source_table_id, record_id: r.__source_record_id }]
|
|
458
|
+
* : [],
|
|
459
|
+
* ),
|
|
416
460
|
* data: { filter: "status=open", sort: "due_date desc" },
|
|
417
461
|
* });
|
|
418
462
|
* ```
|
package/dist/src/hooks.js
CHANGED
|
@@ -373,7 +373,14 @@ function serializeAiContext(context) {
|
|
|
373
373
|
* ```tsx
|
|
374
374
|
* useAiContext("orders_list", {
|
|
375
375
|
* description: `Viewing ${rows.length} orders filtered to status=open, sorted by due date.`,
|
|
376
|
-
*
|
|
376
|
+
* // `flatMap` + the guard, not `map`: the addressing columns are absent on a
|
|
377
|
+
* // GROUPED query's rows, so a ref built without checking carries `undefined`
|
|
378
|
+
* // and points the agent at nothing.
|
|
379
|
+
* records: rows.flatMap((r) =>
|
|
380
|
+
* r.__source_table_id && r.__source_record_id
|
|
381
|
+
* ? [{ table_id: r.__source_table_id, record_id: r.__source_record_id }]
|
|
382
|
+
* : [],
|
|
383
|
+
* ),
|
|
377
384
|
* data: { filter: "status=open", sort: "due_date desc" },
|
|
378
385
|
* });
|
|
379
386
|
* ```
|
package/dist/src/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
export { mount } from "./mount.js";
|
|
18
18
|
export type { MountOptions } from "./mount.js";
|
|
19
19
|
export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useFieldOptions, useFileUpload, useAttachments, useMembers, useAgentRun, useAgentRuns, useAiContext, buildChoiceOutput, } from "./hooks.js";
|
|
20
|
-
export type { UploadedFile, AttachedFile, BaseQueryOptions, QueryOptions, InfiniteQueryOptions, PaginatedQueryOptions, QuerySortKey, QueryFilter, QueryFilterCondition, QueryFilterGroup, WorkflowResult, MembersOptions, AgentRunOptions, UseAgentRun, AgentRunLanding, AgentRunRecord, AgentRunState, AgentUIPart, PendingChoice, ChoiceQuestion, ChoiceOption, AskUserChoiceOutput, FieldOptions, FieldOptionsState, FieldOptionsOptions, } from "./hooks.js";
|
|
20
|
+
export type { QueryRow, UploadedFile, AttachedFile, BaseQueryOptions, QueryOptions, InfiniteQueryOptions, PaginatedQueryOptions, QuerySortKey, QueryFilter, QueryFilterCondition, QueryFilterGroup, WorkflowResult, MembersOptions, AgentRunOptions, UseAgentRun, AgentRunLanding, AgentRunRecord, AgentRunState, AgentUIPart, PendingChoice, ChoiceQuestion, ChoiceOption, AskUserChoiceOutput, FieldOptions, FieldOptionsState, FieldOptionsOptions, } from "./hooks.js";
|
|
21
21
|
export { useComments, useCommentCounts } from "./comments.js";
|
|
22
22
|
export type { AppComment, AppCommentFile, CommentsState, UseCommentsArgs, CommentCountsState, UseCommentCountsArgs, } from "./comments.js";
|
|
23
23
|
export { useViewer } from "./viewer.js";
|
package/docs/ai.md
CHANGED
|
@@ -22,7 +22,7 @@ A declaration carries:
|
|
|
22
22
|
| `instructions` | System instructions — the task the agent performs per run |
|
|
23
23
|
| `tool_names` | The tools the agent may call, resolved against the platform's automation tool registry. The capability boundary for everything EXCEPT workspace data — the run can use nothing else. May be empty — including for an agent that reads documents, since a [`file` input carries its own content](#file-inputs--what-the-agent-can-actually-see) |
|
|
24
24
|
| `knowledge_doc_ids` | The knowledge docs the agent may read, and the whole set it can reach — a doc absent from this list is unreadable even if the agent names its id. Declare `grep_knowledge` / `read_knowledge` in `tool_names` to read them. There is no size limit and nothing is inlined, so a multi-megabyte reference corpus (a full tariff, a regulation set) is a normal declaration. Reach for `code_exec` only to COMPUTE across the corpus — counting, cross-referencing — never merely to read it |
|
|
25
|
-
| `query_aliases` | The app's own named queries the agent may run via `run_app_query` — its **entire read surface** over records |
|
|
25
|
+
| `query_aliases` | The app's own named queries the agent may run via `run_app_query` — its **entire read surface** over records. You list them; there is no "all of them" shorthand, and adding a query to the app never widens an existing agent. Omitted means the agent reads nothing |
|
|
26
26
|
| `workflow_aliases` | The app's own workflows the agent may invoke via `run_app_workflow` — its **entire write surface** |
|
|
27
27
|
| `model_id` | Optional chat model pin. Omit (preferred) to follow the platform default chat model, resolved at run time — the agent tracks model generations with no rewrite. Pin only a deliberate, tested choice |
|
|
28
28
|
| `effort_level` | Optional reasoning depth for adaptive-thinking models. Requires an explicit `model_id` pin — effort is tuned per model |
|
|
@@ -277,7 +277,13 @@ import { useAiContext } from "@lotics/app-sdk";
|
|
|
277
277
|
// A list screen publishes what it rendered:
|
|
278
278
|
useAiContext("orders_list", {
|
|
279
279
|
description: `Viewing ${rows.length} orders filtered to status=open, sorted by due date.`,
|
|
280
|
-
|
|
280
|
+
// Guarded, not `map`: the addressing columns are absent on a GROUPED query's
|
|
281
|
+
// rows, so an unchecked ref carries `undefined` and points the agent at nothing.
|
|
282
|
+
records: rows.flatMap((r) =>
|
|
283
|
+
r.__source_table_id && r.__source_record_id
|
|
284
|
+
? [{ table_id: r.__source_table_id, record_id: r.__source_record_id }]
|
|
285
|
+
: [],
|
|
286
|
+
),
|
|
281
287
|
data: { filter: "status=open", sort: "due_date desc" },
|
|
282
288
|
});
|
|
283
289
|
|
|
@@ -337,6 +343,13 @@ gated per turn on `app:use` for the app in the member's workspace, and runs
|
|
|
337
343
|
under the app's OWNER authority with `is_current_member` bound to the MEMBER
|
|
338
344
|
(exactly as the app's own UI does).
|
|
339
345
|
|
|
346
|
+
The agent is handed a **catalog** of those aliases — each one's params and its
|
|
347
|
+
`description` (a query's own, [declared in the manifest](./queries.md); a
|
|
348
|
+
workflow's, off its workflow row) — so it picks the right one instead of
|
|
349
|
+
guessing an alias and learning the list from a refusal. Your descriptions are
|
|
350
|
+
what it reads, on this surface and inside your own agents' runs. They enter the
|
|
351
|
+
prompt as a capability listing, never as instructions.
|
|
352
|
+
|
|
340
353
|
**Writes carry a second gate that reads do not: `run_app_workflow` asks the
|
|
341
354
|
member to approve every call from chat.** A wrong read is fixed by asking
|
|
342
355
|
again; a wrong write is not. The alias list bounds what an agent *could* reach;
|
package/docs/data_fetching.md
CHANGED
|
@@ -246,6 +246,11 @@ workflows), `__created_at` / `__updated_at` (record timestamps), and per-project
|
|
|
246
246
|
metadata. A grouped query collapses rows and emits none of these. Details:
|
|
247
247
|
[./queries.md](./queries.md).
|
|
248
248
|
|
|
249
|
+
A row is typed `QueryRow`: projected columns are `unknown` (the manifest declares them, so the
|
|
250
|
+
readers below are what narrow them), while `__source_record_id` / `__source_table_id` are typed
|
|
251
|
+
`string | undefined` — reachable without a cast, but only after narrowing, because "a grouped query
|
|
252
|
+
emits none of these" is a fact the type states rather than one you have to remember.
|
|
253
|
+
|
|
249
254
|
### The readers
|
|
250
255
|
|
|
251
256
|
| Reader | In → out | Semantics |
|
package/docs/files.md
CHANGED
|
@@ -265,6 +265,9 @@ with `files` absent.
|
|
|
265
265
|
|
|
266
266
|
```tsx
|
|
267
267
|
const generate = useWorkflow("generateInvoice");
|
|
268
|
+
// `__source_record_id` is optional — a grouped query carries none — so narrow it
|
|
269
|
+
// rather than handing the workflow `undefined` where it declares a record.
|
|
270
|
+
if (!row.__source_record_id) return;
|
|
268
271
|
const result = await generate({ record_id: row.__source_record_id });
|
|
269
272
|
if (result.status === "success" && result.files?.length) {
|
|
270
273
|
await openExternal(result.files[0].url);
|
|
@@ -241,13 +241,18 @@ client-side up front, and re-checked server-side.
|
|
|
241
241
|
### Reading & writing
|
|
242
242
|
|
|
243
243
|
```tsx
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
// In a record screen, where an id definitionally exists:
|
|
245
|
+
const { comments, available, createComment } = useComments({ record_id: recordId });
|
|
246
|
+
|
|
247
|
+
// From a row, gate the whole panel rather than passing a placeholder:
|
|
248
|
+
{row.__source_record_id && <CommentsPanel recordId={row.__source_record_id} />}
|
|
247
249
|
```
|
|
248
250
|
|
|
249
|
-
`record_id`
|
|
250
|
-
|
|
251
|
+
`record_id` must be a **real** id — the fetch keys on `available` alone, so an empty string is not
|
|
252
|
+
skipped, it fetches comments for nothing. When the id comes from a row's `__source_record_id`
|
|
253
|
+
addressing column (see [queries](./queries.md)) narrow it first: a grouped query emits no addressing
|
|
254
|
+
columns, and a hook cannot be called conditionally, so the guard belongs at the component that
|
|
255
|
+
renders the panel. State: `{ comments, loading, error, available, createComment,
|
|
251
256
|
updateComment, deleteComment, refetch }`.
|
|
252
257
|
|
|
253
258
|
- `comments` — newest first on the wire (server order). Pass the array as-is to `@lotics/ui`'s
|
package/docs/queries.md
CHANGED
|
@@ -35,7 +35,8 @@ Queries live in the app's `package.json` under `lotics.queries` — an alias →
|
|
|
35
35
|
},
|
|
36
36
|
"orderByCode": {
|
|
37
37
|
"ast": { /* … a filter with "{{params.code}}" … */ },
|
|
38
|
-
"params": { "code": { "type": "text" } }
|
|
38
|
+
"params": { "code": { "type": "text" } },
|
|
39
|
+
"description": "One order by its code, with its customer and line total."
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -48,9 +49,15 @@ Queries live in the app's `package.json` under `lotics.queries` — an alias →
|
|
|
48
49
|
(`text`, `number`, `boolean`, `date`, `datetime`, `email`, `select`, `member`, `record_link`,
|
|
49
50
|
`date_range`, `file`, `json`, `object`, `array`); each param may set `required: false`
|
|
50
51
|
(default is required) and `description`. Nesting is capped at depth 8.
|
|
52
|
+
- **`description`** — one line saying what the query returns, capped at 300 characters. No app
|
|
53
|
+
code reads it. It is for the **agents** that reach this app's data — the app's own declared
|
|
54
|
+
agents, and a member's chat while they have the app open — which otherwise see only an alias,
|
|
55
|
+
a JS identifier that names a query without saying what it covers. Write it for whoever has to
|
|
56
|
+
choose between your aliases; longer guidance belongs in an agent's own `instructions`.
|
|
51
57
|
- Aliases must be valid JS identifiers (`useQuery("openOrders")` and codegen depend on it).
|
|
52
58
|
|
|
53
|
-
`lotics app
|
|
59
|
+
`lotics app query set <alias>` (or `--all`) pushes this map to the server — a **deploy does
|
|
60
|
+
not**, it echoes the live row back unchanged. The **server holds the canonical template**;
|
|
54
61
|
the app never sends a raw AST over the wire. This is the exposure model: a public app can read
|
|
55
62
|
exactly what its author's queries project — params fill declared value holes and can never
|
|
56
63
|
widen the query's reach (a token in a `table_id` or field-key position fails deploy validation).
|
|
@@ -59,7 +66,8 @@ widen the query's reach (a token in a `table_id` or field-key position fails dep
|
|
|
59
66
|
|
|
60
67
|
Deploy fails — with the compiler's own message, never raw SQL/Postgres text — when any alias:
|
|
61
68
|
|
|
62
|
-
1. isn't a valid identifier, or the declaration isn't `{ ast, params? }
|
|
69
|
+
1. isn't a valid identifier, or the declaration isn't `{ ast, params?, description? }` (a
|
|
70
|
+
description over 300 characters is refused here too);
|
|
63
71
|
2. references `{{params.x}}` without declaring param `x` (typos can't silently widen);
|
|
64
72
|
3. doesn't parse as a `QueryNode` **as the raw template** (the runtime parses the stored
|
|
65
73
|
template before substitution, so a template that only parses after substitution would fail
|
|
@@ -99,7 +107,7 @@ executes it inside a bounded transaction.
|
|
|
99
107
|
(`private_filters`) are excluded on every reach — base scans, link extraction, and
|
|
100
108
|
non-self-scoped traversals alike. (Row scopes don't apply when the app owner is an admin.)
|
|
101
109
|
- **Dev loop**: `lotics app dev` forwards query RPCs to the **deployed** manifest. Editing
|
|
102
|
-
`lotics.queries` locally does nothing until you `lotics app
|
|
110
|
+
`lotics.queries` locally does nothing until you `lotics app query set`.
|
|
103
111
|
|
|
104
112
|
### Results
|
|
105
113
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/app-sdk",
|
|
3
|
-
"version": "0.62.
|
|
3
|
+
"version": "0.62.4",
|
|
4
4
|
"description": "Runtime SDK for Lotics custom-code apps — typed hooks, postMessage bridge, mount entry point",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"docs"
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
|
-
"build": "tsgo",
|
|
23
|
+
"build": "tsgo -p tsconfig.build.json",
|
|
24
24
|
"typecheck": "tsgo --noEmit",
|
|
25
25
|
"test": "vitest run",
|
|
26
26
|
"prepublishOnly": "npm run build"
|