@lotics/app-sdk 0.62.3 → 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 +7 -1
- package/docs/data_fetching.md +5 -0
- package/docs/files.md +3 -0
- package/docs/members_and_options.md +10 -5
- 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
|
@@ -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
|
|
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/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"
|