@lotics/app-sdk 0.101.2 → 0.102.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 +1 -1
- package/dist/{chunk-ARV5FAU5.js → chunk-ZOFKWF6V.js} +6 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +42 -15
- package/dist/mock.d.ts +14 -5
- package/dist/queries.d.ts +36 -1
- package/dist/router.js +1 -1
- package/dist/rpc.d.ts +1 -1
- package/dist/shared_types.d.ts +11 -0
- package/docs/data_fetching.md +2 -1
- package/docs/members_and_options.md +1 -0
- package/docs/queries.md +9 -1
- package/docs/runtime.md +10 -6
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -15,7 +15,7 @@ This file is the index. The **exact type** of anything is its shipped declaratio
|
|
|
15
15
|
|
|
16
16
|
| Doc | Read it for |
|
|
17
17
|
|---|---|
|
|
18
|
-
| [docs/data_fetching.md](./docs/data_fetching.md) | The reads: `useQuery(alias, params, opts)` — its rows (`limit`), numbered pages (`page`), a keyset feed (`more`), its `total` or the total alone, with `total: { by }` a count per value of one column — `useQueries` for reads known only at render, `queryAll` outside React; every hook answers one `QueryState`. The ROW type (`RowOf` — the alias's projected columns and nothing else), runtime `sort`/`filter` keys, cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`, `readCreatedAt`/`readUpdatedAt`), the SDK's own cache — **arrival revalidates** — **realtime push**, a write drawn on every read from the press, a count as its own full scan, and the search-as-you-type and record-picker patterns. |
|
|
18
|
+
| [docs/data_fetching.md](./docs/data_fetching.md) | The reads: `useQuery(alias, params, opts)` — its rows (`limit`), numbered pages (`page`), a keyset feed (`more`), its `total` or the total alone, with `total: { by }` a count per value of one column — `useQueries` for reads known only at render, `queryAll` outside React, `exportQuery` for a file of them the server makes; every hook answers one `QueryState`. The ROW type (`RowOf` — the alias's projected columns and nothing else), runtime `sort`/`filter` keys, cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`, `readCreatedAt`/`readUpdatedAt`), the SDK's own cache — **arrival revalidates** — **realtime push**, a write drawn on every read from the press, a count as its own full scan, and the search-as-you-type and record-picker patterns. |
|
|
19
19
|
| [docs/queries.md](./docs/queries.md) | **The query engine reference** — AST node kinds, per-field-type operators, filters/params/pruning, free-text search, combining tables, shaping (aggregates, date buckets, windows), runtime refinement bounds, limits and the efficiency playbook. |
|
|
20
20
|
| [docs/mutations.md](./docs/mutations.md) | `useWorkflow` (the ONLY write path; `useWorkflows` for writes that are data), the `WorkflowResult` resolve-never-throw contract (`field_errors`), typed inputs, every write drawn on every read from the press — predicted from the workflow's own steps, taken back on a refusal, `pending` meanwhile — the re-read a successful write triggers over the tables its body names, diff-before-update, locked records, `useNewRecord`, read-after-write ordering (`writesSettled()`), `useRecording`. |
|
|
21
21
|
| [docs/workflows.md](./docs/workflows.md) | **The workflow-BODY reference** — the JS subset a body may use, opaque `fld_*`/`opt_*` keys, every step form, the accepted sugar, helpers, record-write surfaces, the traps and the verify loop. |
|
|
@@ -898,6 +898,8 @@ function rpcStandalone(op, payload) {
|
|
|
898
898
|
switch (op) {
|
|
899
899
|
case "query":
|
|
900
900
|
return standaloneQuery(payload);
|
|
901
|
+
case "export":
|
|
902
|
+
return standaloneExport(payload);
|
|
901
903
|
case "field_options":
|
|
902
904
|
return standaloneFieldOptions(payload);
|
|
903
905
|
case "workflow":
|
|
@@ -1029,6 +1031,10 @@ async function standaloneQuery(payload) {
|
|
|
1029
1031
|
const { app_id } = await boot();
|
|
1030
1032
|
return apiCall("POST", `/v1/apps/${app_id}/query`, payload, { appId: app_id });
|
|
1031
1033
|
}
|
|
1034
|
+
async function standaloneExport(payload) {
|
|
1035
|
+
const { app_id } = await boot();
|
|
1036
|
+
return apiCall("POST", `/v1/apps/${app_id}/export`, payload, { appId: app_id });
|
|
1037
|
+
}
|
|
1032
1038
|
async function standaloneFieldOptions(p) {
|
|
1033
1039
|
const { app_id } = await boot();
|
|
1034
1040
|
return apiCall("POST", `/v1/apps/${app_id}/field-options`, { alias: p.alias }, { appId: app_id });
|
package/dist/hooks.d.ts
CHANGED
|
@@ -151,6 +151,8 @@ export interface MembersOptions {
|
|
|
151
151
|
* declares. Omit for the whole roster.
|
|
152
152
|
*/
|
|
153
153
|
group?: string;
|
|
154
|
+
/** `false` reads no roster: `members` stays empty and `loading` false. Default `true`. */
|
|
155
|
+
enabled?: boolean;
|
|
154
156
|
}
|
|
155
157
|
/**
|
|
156
158
|
* The organization's members, for an assign picker. Member-only, and gated on
|
package/dist/index.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ export { reportAppError } from "./error_report.js";
|
|
|
5
5
|
export type { AppErrorKind } from "./error_report.js";
|
|
6
6
|
export { useWorkflow, useWorkflows, writesSettled, useFileUpload, useAttachments, useAttachmentPiles, useMembers, useAgentRun, useAgentRuns, useAiContext, buildChoiceOutput, } from "./hooks.js";
|
|
7
7
|
export type { UploadedFile, WorkflowResult, MembersOptions, AgentRunOptions, UseAgentRun, AgentRunLanding, AgentRunRecord, AgentRunState, AgentUIPart, PendingChoice, ChoiceQuestion, ChoiceOption, AskUserChoiceOutput, } from "./hooks.js";
|
|
8
|
-
export { useQuery, useQueries, queryAll, useFieldOptions } from "./queries.js";
|
|
9
|
-
export type { QueryRow, QueryCall, QueryState, RowOf, QueryOptions, ColumnKeyOf, QuerySortKey, QueryFilter, QueryFilterCondition, QueryFilterFieldCondition, QueryFilterRecordIdCondition, QueryFilterGroup, FieldOptions, FieldOptionsState, FieldOptionsOptions, FigureUnits, } from "./queries.js";
|
|
8
|
+
export { useQuery, useQueries, queryAll, exportQuery, useFieldOptions } from "./queries.js";
|
|
9
|
+
export type { QueryRow, QueryCall, QueryState, RowOf, QueryOptions, ColumnKeyOf, QuerySortKey, QueryFilter, QueryFilterCondition, QueryFilterFieldCondition, QueryFilterRecordIdCondition, QueryFilterGroup, FieldOptions, FieldOptionsState, FieldOptionsOptions, FigureUnits, ExportCall, ExportFile, ExportReport, } from "./queries.js";
|
|
10
10
|
export type { AttachedFile, AttachmentPiles, AttachmentPilesOptions, AttachmentsOptions, AttachmentsState } from "./attachments.js";
|
|
11
11
|
export { useComments, useCommentCounts } from "./comments.js";
|
|
12
12
|
export type { AppComment, AppCommentAuthor, AppCommentFile, CommentsState, UseCommentsArgs, CommentCountsState, UseCommentCountsArgs, } from "./comments.js";
|
|
@@ -27,7 +27,7 @@ export { readMembers } from "./members.js";
|
|
|
27
27
|
export type { ResolvedMember } from "./members.js";
|
|
28
28
|
export { readSelect } from "./select.js";
|
|
29
29
|
export type { ResolvedOption } from "./select.js";
|
|
30
|
-
export type { AppFixture, MockQuery, MockQueryCall, MockWorkflow } from "./mock.js";
|
|
30
|
+
export type { AppFixture, MockExport, MockQuery, MockQueryCall, MockWorkflow } from "./mock.js";
|
|
31
31
|
export type { AppWorkflows, AppWorkflowResults, AppQueries, AppQueryColumns, AppAgents, AppAgentResults } from "./types.js";
|
|
32
32
|
export { row, readLinks, readFiles, readLocked, readCreatedAt, readUpdatedAt, PENDING } from "./row.js";
|
|
33
33
|
export type { ResolvedLink, AppFile } from "./row.js";
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
subscribeRecordings,
|
|
22
22
|
subscribeUrlParams,
|
|
23
23
|
urlParam
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-ZOFKWF6V.js";
|
|
25
25
|
|
|
26
26
|
// src/mount.tsx
|
|
27
27
|
import { createRoot } from "react-dom/client";
|
|
@@ -59,6 +59,10 @@ function getMockWorkflow(alias) {
|
|
|
59
59
|
if (!isMockMode()) return null;
|
|
60
60
|
return registeredFixture?.workflows?.[alias] ?? null;
|
|
61
61
|
}
|
|
62
|
+
function getMockExport(alias) {
|
|
63
|
+
if (!isMockMode()) return null;
|
|
64
|
+
return registeredFixture?.exports?.[alias] ?? null;
|
|
65
|
+
}
|
|
62
66
|
function getMockRecordings() {
|
|
63
67
|
if (!isMockMode()) return null;
|
|
64
68
|
return registeredFixture?.recordings ?? null;
|
|
@@ -24134,6 +24138,7 @@ function appWorkflowOutputDepth(output) {
|
|
|
24134
24138
|
if (output.type === "array") return 1 + appWorkflowOutputDepth(output.items);
|
|
24135
24139
|
return 1;
|
|
24136
24140
|
}
|
|
24141
|
+
var APP_ALIAS_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
24137
24142
|
var MAX_APP_CAPABILITY_DESCRIPTION = 300;
|
|
24138
24143
|
var APP_ANONYMOUS_REQUEST_MAX_BYTES = 256 * 1024;
|
|
24139
24144
|
var APP_UPLOAD_MAX_BYTES = 25 * 1024 * 1024;
|
|
@@ -24166,6 +24171,13 @@ var appWorkflowContractSchema = zod_default.object({
|
|
|
24166
24171
|
inputs: zod_default.record(zod_default.string(), appWorkflowInputSchema).optional(),
|
|
24167
24172
|
outputs: zod_default.record(zod_default.string(), appWorkflowOutputSchema).optional()
|
|
24168
24173
|
});
|
|
24174
|
+
var APP_EXPORT_REPORT_KEYS = ["title", "lines", "readings"];
|
|
24175
|
+
var appExportTemplateSchema = zod_default.object({
|
|
24176
|
+
template_id: zod_default.string().min(1).describe("The document template (`dtl_\u2026`) filled: an Excel workbook, or an HTML page made a PDF."),
|
|
24177
|
+
rows: zod_default.string().regex(APP_ALIAS_REGEX).refine((key) => !APP_EXPORT_REPORT_KEYS.some((taken) => taken === key), {
|
|
24178
|
+
message: `The rows are listed under a key of their own, never ${APP_EXPORT_REPORT_KEYS.join(", ")}.`
|
|
24179
|
+
}).describe(`The key the template lists the rows under, beside ${APP_EXPORT_REPORT_KEYS.join(", ")}.`)
|
|
24180
|
+
}).strict();
|
|
24169
24181
|
var appQueryDeclarationSchema = zod_default.object({
|
|
24170
24182
|
ast: zod_default.unknown().describe(
|
|
24171
24183
|
"Query AST template (a QueryNode). Validated server-side via parseQueryNode at deploy. May embed {{params.<name>}} tokens in filter value positions."
|
|
@@ -24175,6 +24187,9 @@ var appQueryDeclarationSchema = zod_default.object({
|
|
|
24175
24187
|
),
|
|
24176
24188
|
description: zod_default.string().optional().describe(
|
|
24177
24189
|
`What this query returns, in one line \u2014 read by an agent choosing between the app's aliases. An alias is a JS identifier, which names a query without saying what it covers. Capped at ${MAX_APP_CAPABILITY_DESCRIPTION} characters.`
|
|
24190
|
+
),
|
|
24191
|
+
templates: zod_default.record(zod_default.string().regex(APP_ALIAS_REGEX), appExportTemplateSchema).optional().describe(
|
|
24192
|
+
"Document templates an export of this query's rows may fill, by the name the export asks for. An export names one of these or none; a template it does not declare is refused."
|
|
24178
24193
|
)
|
|
24179
24194
|
});
|
|
24180
24195
|
var appAgentDeclarationSchema = zod_default.object({
|
|
@@ -28006,7 +28021,7 @@ function useAiContext(slot, context) {
|
|
|
28006
28021
|
function useMembers(opts) {
|
|
28007
28022
|
const group = opts?.group;
|
|
28008
28023
|
const roster = useKey(
|
|
28009
|
-
JSON.stringify(["members", group ?? null]),
|
|
28024
|
+
opts?.enabled === false ? null : JSON.stringify(["members", group ?? null]),
|
|
28010
28025
|
async () => {
|
|
28011
28026
|
const members2 = (await rpc("members", { group })).members ?? [];
|
|
28012
28027
|
noteMembers(members2);
|
|
@@ -30649,23 +30664,27 @@ function overlayAsked(alias, params, narrowing, askedAt, totalsOf, rows, back, a
|
|
|
30649
30664
|
}
|
|
30650
30665
|
|
|
30651
30666
|
// src/queries.ts
|
|
30652
|
-
function
|
|
30653
|
-
const mocked = getMockRows(alias, call);
|
|
30654
|
-
if (mocked === null) return void 0;
|
|
30667
|
+
function pageOf(mocked, call) {
|
|
30655
30668
|
const start = call.keyset === true ? Number(call.cursor ?? 0) : call.offset ?? 0;
|
|
30656
30669
|
const end = call.limit === void 0 ? mocked.length : start + call.limit;
|
|
30657
30670
|
const rows = mocked.slice(start, end);
|
|
30658
30671
|
const more = end < mocked.length;
|
|
30659
30672
|
return { rows, truncated: call.keyset !== true && more, next_cursor: call.keyset === true && more ? String(end) : null };
|
|
30660
30673
|
}
|
|
30661
|
-
function
|
|
30674
|
+
function countOf(mocked, by) {
|
|
30675
|
+
return { total: mocked.length, ...by === void 0 ? {} : { counts: countsOf(mocked, by) } };
|
|
30676
|
+
}
|
|
30677
|
+
function knownRows(alias, call) {
|
|
30678
|
+
const mocked = getMockRows(alias, call);
|
|
30679
|
+
return Array.isArray(mocked) ? pageOf(mocked, call) : void 0;
|
|
30680
|
+
}
|
|
30681
|
+
function knownCount(alias, call) {
|
|
30662
30682
|
const mocked = getMockRows(alias, { params: call.params, filter: call.filter });
|
|
30663
|
-
|
|
30664
|
-
return { total: mocked.length, ...call.by === void 0 ? {} : { counts: countsOf(mocked, call.by) } };
|
|
30683
|
+
return Array.isArray(mocked) ? countOf(mocked, call.by) : void 0;
|
|
30665
30684
|
}
|
|
30666
30685
|
async function askRows(key, alias, askedAt, call, since = askedAt) {
|
|
30667
|
-
const mocked =
|
|
30668
|
-
if (mocked !==
|
|
30686
|
+
const mocked = getMockRows(alias, call);
|
|
30687
|
+
if (mocked !== null) return pageOf(await mocked, call);
|
|
30669
30688
|
const answer = await rpc("query", {
|
|
30670
30689
|
alias,
|
|
30671
30690
|
params: call.params,
|
|
@@ -30678,8 +30697,8 @@ async function askRows(key, alias, askedAt, call, since = askedAt) {
|
|
|
30678
30697
|
return answer;
|
|
30679
30698
|
}
|
|
30680
30699
|
async function askCount(key, alias, askedAt, call) {
|
|
30681
|
-
const mocked =
|
|
30682
|
-
if (mocked !==
|
|
30700
|
+
const mocked = getMockRows(alias, { params: call.params, filter: call.filter });
|
|
30701
|
+
if (mocked !== null) return countOf(await mocked, call.by);
|
|
30683
30702
|
const answer = await rpc("query", {
|
|
30684
30703
|
alias,
|
|
30685
30704
|
params: call.params,
|
|
@@ -30720,7 +30739,7 @@ function useQuery(alias, params = {}, opts = {}) {
|
|
|
30720
30739
|
limit: opts.page ?? opts.limit,
|
|
30721
30740
|
offset: opts.page === void 0 ? 0 : page * opts.page
|
|
30722
30741
|
}),
|
|
30723
|
-
{ focus, aliases: [alias], known: mocking ? () =>
|
|
30742
|
+
{ focus, aliases: [alias], known: mocking ? () => knownRows(alias, { params, filter: filter2, sort, limit: opts.page ?? opts.limit, offset: opts.page === void 0 ? 0 : page * opts.page }) : void 0 }
|
|
30724
30743
|
);
|
|
30725
30744
|
const feedKey = !enabled || !wantsRows || opts.more === void 0 ? null : JSON.stringify(["feed", alias, call, opts.more]);
|
|
30726
30745
|
const feedSize = opts.more ?? 0;
|
|
@@ -30761,7 +30780,7 @@ function useQuery(alias, params = {}, opts = {}) {
|
|
|
30761
30780
|
const count = useKey(countKey, (askedAt) => askCount(countKey ?? "", alias, askedAt, { params, filter: filter2, by }), {
|
|
30762
30781
|
focus,
|
|
30763
30782
|
aliases: [alias],
|
|
30764
|
-
known: mocking ? () =>
|
|
30783
|
+
known: mocking ? () => knownCount(alias, { params, filter: filter2, by }) : void 0
|
|
30765
30784
|
});
|
|
30766
30785
|
const version3 = useWrittenVersion();
|
|
30767
30786
|
const drawn2 = useMemo3(() => {
|
|
@@ -30823,7 +30842,7 @@ async function settle(read2) {
|
|
|
30823
30842
|
}
|
|
30824
30843
|
async function askGroups(key, alias, askedAt, aggregate, call) {
|
|
30825
30844
|
const mocked = getMockRows(alias, { ...call, aggregate });
|
|
30826
|
-
if (mocked !== null) return { rows: aggregateRows(mocked, aggregate), truncated: false };
|
|
30845
|
+
if (mocked !== null) return { rows: aggregateRows(await mocked, aggregate), truncated: false };
|
|
30827
30846
|
const answer = await rpc("query", { alias, params: call.params, aggregate, filter: call.filter, sort: call.sort });
|
|
30828
30847
|
noteAnswer(key, alias, askedAt, []);
|
|
30829
30848
|
return answer;
|
|
@@ -30892,6 +30911,13 @@ async function queryAll(alias, params = {}, opts = {}) {
|
|
|
30892
30911
|
if (!page.truncated || page.rows.length === 0) return pages.flat();
|
|
30893
30912
|
}
|
|
30894
30913
|
}
|
|
30914
|
+
async function exportQuery(alias, call) {
|
|
30915
|
+
const sort = call.sort === void 0 || call.sort.length === 0 ? void 0 : [...call.sort];
|
|
30916
|
+
const body = { alias, params: call.params ?? {}, filter: call.filter, sort, report: call.report, file: call.file };
|
|
30917
|
+
const mocked = getMockExport(alias);
|
|
30918
|
+
if (mocked !== null) return mocked(body);
|
|
30919
|
+
return (await rpc("export", body)).file;
|
|
30920
|
+
}
|
|
30895
30921
|
var NO_UNITS = {};
|
|
30896
30922
|
function useFieldOptions(alias, opts) {
|
|
30897
30923
|
const enabled = opts?.enabled ?? true;
|
|
@@ -31317,6 +31343,7 @@ export {
|
|
|
31317
31343
|
askAi,
|
|
31318
31344
|
buildChoiceOutput,
|
|
31319
31345
|
downloadFile,
|
|
31346
|
+
exportQuery,
|
|
31320
31347
|
isEmbedded,
|
|
31321
31348
|
isWithinZone,
|
|
31322
31349
|
mount,
|
package/dist/mock.d.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* `useFileUpload` is not mocked.
|
|
7
7
|
*/
|
|
8
8
|
import type { QueryAggregate } from "./shared_types.js";
|
|
9
|
-
import type { WorkflowResult } from "./hooks.js";
|
|
10
|
-
import type { QuerySortKey } from "./queries.js";
|
|
9
|
+
import type { UploadedFile, WorkflowResult } from "./hooks.js";
|
|
10
|
+
import type { ExportCall, QuerySortKey } from "./queries.js";
|
|
11
11
|
import type { RecordingState } from "./recording_state.js";
|
|
12
12
|
/** A result, or a function of the inputs — return a slow promise to review the pending state. */
|
|
13
13
|
export type MockWorkflow = WorkflowResult | ((inputs: Record<string, unknown>) => WorkflowResult | Promise<WorkflowResult>);
|
|
@@ -19,11 +19,18 @@ export interface MockQueryCall {
|
|
|
19
19
|
/** The groups the call asks for: a fixture answers the query's rows, and they are folded into these after it. */
|
|
20
20
|
aggregate?: QueryAggregate;
|
|
21
21
|
}
|
|
22
|
-
/** Rows, or a function of the call — the only way to honour `filter`/`sort`/`limit`, which the fixture path has no
|
|
23
|
-
|
|
22
|
+
/** Rows, or a function of the call — the only way to honour `filter`/`sort`/`limit`, which the fixture path has no
|
|
23
|
+
* engine for. Return a promise to hold the read loading until it settles. */
|
|
24
|
+
export type MockQuery = Array<Record<string, unknown>> | ((call: MockQueryCall) => Array<Record<string, unknown>> | Promise<Array<Record<string, unknown>>>);
|
|
25
|
+
/** The file an export of a query makes, as a function of the call — return a slow promise to review the pending state. */
|
|
26
|
+
export type MockExport = (call: ExportCall & {
|
|
27
|
+
alias: string;
|
|
28
|
+
}) => UploadedFile | Promise<UploadedFile>;
|
|
24
29
|
export interface AppFixture {
|
|
25
30
|
queries?: Record<string, MockQuery>;
|
|
26
31
|
workflows?: Record<string, MockWorkflow>;
|
|
32
|
+
/** By the query exported. */
|
|
33
|
+
exports?: Record<string, MockExport>;
|
|
27
34
|
/** `useRecording(alias)` reads as available; `start`/`stop` change nothing. */
|
|
28
35
|
recordings?: Record<string, RecordingState>;
|
|
29
36
|
}
|
|
@@ -31,7 +38,9 @@ export declare function registerMockFixture(fixture: AppFixture | undefined): vo
|
|
|
31
38
|
/** The raw `?__mock=1` flag, fixture or not; false where `window.location` is absent. */
|
|
32
39
|
export declare function hasMockFlag(): boolean;
|
|
33
40
|
/** Null for an unmocked alias, which reads real data. */
|
|
34
|
-
export declare function getMockRows(alias: string, call: MockQueryCall): Array<Record<string, unknown>> | null;
|
|
41
|
+
export declare function getMockRows(alias: string, call: MockQueryCall): Array<Record<string, unknown>> | Promise<Array<Record<string, unknown>>> | null;
|
|
35
42
|
/** Null for an unmocked alias, which executes for real. */
|
|
36
43
|
export declare function getMockWorkflow(alias: string): MockWorkflow | null;
|
|
44
|
+
/** Null for an unmocked alias, which the server exports. */
|
|
45
|
+
export declare function getMockExport(alias: string): MockExport | null;
|
|
37
46
|
export declare function getMockRecordings(): Record<string, RecordingState> | null;
|
package/dist/queries.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { QueryAggregate } from "./shared_types.js";
|
|
2
2
|
import type { AppQueries, AppQueryColumns } from "./types.js";
|
|
3
3
|
import type { ResolvedOption } from "./select.js";
|
|
4
|
+
import type { UploadedFile } from "./hooks.js";
|
|
5
|
+
import type { ReportLanguage } from "./shared_types.js";
|
|
6
|
+
import type { ReportColumn, ReportReading } from "./shared_types.js";
|
|
4
7
|
/**
|
|
5
8
|
* One row of a query whose columns codegen could not read (a dynamic alias, a
|
|
6
9
|
* bare `from_table`). Values stay `unknown` — the cell readers narrow them.
|
|
@@ -169,12 +172,44 @@ export interface QueryCall {
|
|
|
169
172
|
export declare function useQueries(calls: readonly QueryCall[], opts?: Pick<QueryOptions, "enabled" | "revalidateOnFocus">): QueryState<QueryRow>[];
|
|
170
173
|
/**
|
|
171
174
|
* Every row a declared query matches, read a page at a time to the end — for what must hold every row
|
|
172
|
-
* (
|
|
175
|
+
* (an act over every row in view), never for a screen, which pages, nor for a file, which `exportQuery` makes on the
|
|
176
|
+
* server. The server's rows as stored: no write is drawn over them.
|
|
173
177
|
*/
|
|
174
178
|
export declare function queryAll(alias: string, params?: Record<string, unknown>, opts?: {
|
|
175
179
|
filter?: QueryFilter;
|
|
176
180
|
sort?: readonly QuerySortKey[];
|
|
177
181
|
}): Promise<QueryRow[]>;
|
|
182
|
+
/** What an export states of its rows beside them: the page's title, one line per thing narrowing them, and the
|
|
183
|
+
* figures over them as the screen drew them. */
|
|
184
|
+
export interface ExportReport {
|
|
185
|
+
title: string;
|
|
186
|
+
lines: string[];
|
|
187
|
+
readings: ReportReading[];
|
|
188
|
+
}
|
|
189
|
+
/** The file an export makes: the default report workbook — its words in `language`, the rows under `columns` on the
|
|
190
|
+
* tab `sheet` — or a template the query declares under `templates`, by its name there. */
|
|
191
|
+
export type ExportFile = {
|
|
192
|
+
kind: "workbook";
|
|
193
|
+
language: ReportLanguage;
|
|
194
|
+
sheet: string;
|
|
195
|
+
columns: ReportColumn[];
|
|
196
|
+
} | {
|
|
197
|
+
kind: "template";
|
|
198
|
+
template: string;
|
|
199
|
+
};
|
|
200
|
+
export interface ExportCall {
|
|
201
|
+
params?: Record<string, unknown>;
|
|
202
|
+
filter?: QueryFilter;
|
|
203
|
+
sort?: readonly QuerySortKey[];
|
|
204
|
+
report: ExportReport;
|
|
205
|
+
file: ExportFile;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Every row a declared query matches — narrowed by `filter` and ordered by `sort` as a page of it is — made into one
|
|
209
|
+
* file by the server, so no row reaches the browser. Past the most rows one export holds it is refused, never cut.
|
|
210
|
+
* Resolves the stored file, its URL signed for the caller.
|
|
211
|
+
*/
|
|
212
|
+
export declare function exportQuery(alias: string, call: ExportCall): Promise<UploadedFile>;
|
|
178
213
|
/** One select column's full option set. */
|
|
179
214
|
export interface FieldOptions {
|
|
180
215
|
/** The source field's display name. */
|
package/dist/router.js
CHANGED
package/dist/rpc.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { type UrlParams, type UrlParamsPatch } from "./url_params.js";
|
|
|
8
8
|
* app → host: { id, op, payload }
|
|
9
9
|
* host → app: { id, type: "result", data } | { id, type: "error", message }
|
|
10
10
|
*/
|
|
11
|
-
export type RpcOp = "query" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "file.rename" | "members" | "context" | "write_model" | "prediction_miss" | "openExternal" | "openApp" | "askAi" | "urlState.get" | "urlState.set" | "folder.get" | "folder.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts" | "recording.start" | "recording.stop";
|
|
11
|
+
export type RpcOp = "query" | "export" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "file.rename" | "members" | "context" | "write_model" | "prediction_miss" | "openExternal" | "openApp" | "askAi" | "urlState.get" | "urlState.set" | "folder.get" | "folder.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts" | "recording.start" | "recording.stop";
|
|
12
12
|
export interface AgentRunPayload {
|
|
13
13
|
alias: string;
|
|
14
14
|
session_id: string;
|
package/dist/shared_types.d.ts
CHANGED
|
@@ -4,5 +4,16 @@
|
|
|
4
4
|
* tarball. Written at every build, never edited.
|
|
5
5
|
*/
|
|
6
6
|
export type QueryAggregate = { by?: readonly string[]; day?: string; sum?: string; };
|
|
7
|
+
export type ReportColumn = { key: string; header: string; type: "number" | "text" | "date"; };
|
|
8
|
+
export type ReportLanguage = "vi" | "en";
|
|
9
|
+
export type ReportPart = { label: string; value: number; share: number | null; };
|
|
10
|
+
export type ReportPivotColumn = { label: string; total: number | null; };
|
|
11
|
+
export type ReportPivotRow = { label: string; values: number[]; total: number | null; };
|
|
12
|
+
export type ReportPoint = { label: string; value: number; partial: boolean; };
|
|
13
|
+
export type ReportReading = (ReportReadingHead & { kind: "metric"; value: number | null; }) | (ReportReadingHead & { kind: "breakdown"; byLabel: string; parts: ReportPart[]; total: number | null; }) | (ReportReadingHead & { kind: "trend"; points: ReportPoint[]; }) | (ReportReadingHead & { kind: "pivot"; rowsLabel: string; columnsLabel: string; columns: ReportPivotColumn[]; rows: ReportPivotRow[]; total: number | null; }) | { kind: "failed"; label: string; };
|
|
14
|
+
export type ReportReadingHead = { label: string; valueLabel: string; unit: string | null; truncated: boolean; };
|
|
7
15
|
export type TableField = { key: string; name: string; description: string; type: "number"; format: "number" | "currency" | "percentage"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; currency?: string | null | undefined; unit?: string | null | undefined; unit_field?: string | null | undefined; currency_field?: string | null | undefined; default_value?: number | null | undefined; } | { key: string; name: string; description: string; type: "text"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; unique?: boolean | undefined; format?: "text" | "link" | "markdown" | undefined; default_value?: string | null | undefined; } | { key: string; name: string; description: string; type: "date"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; format?: "date" | "datetime" | "date_range" | "datetime_range" | undefined; timezone?: string | undefined; derive_from?: "created_at" | "updated_at" | undefined; default_value?: string | null | undefined; } | { key: string; name: string; description: string; type: "boolean"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; default_value?: boolean | null | undefined; } | { key: string; name: string; description: string; type: "select"; options: { key: string; name: string; color: "red" | "orange" | "amber" | "yellow" | "lime" | "green" | "emerald" | "teal" | "cyan" | "sky" | "blue" | "indigo" | "violet" | "purple" | "fuchsia" | "pink" | "rose" | "slate" | "gray" | "zinc" | "neutral" | "stone"; mark?: { kind: string; name: string; } | undefined; }[]; confirm_before_update?: boolean | undefined; required?: boolean | undefined; multi?: boolean | undefined; default_value?: string[] | null | undefined; } | { key: string; name: string; description: string; type: "select_member"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; multi?: boolean | undefined; default_value?: string[] | null | undefined; } | { key: string; name: string; description: string; type: "select_record_link"; table_id: string; display_field_keys: string[]; confirm_before_update?: boolean | undefined; required?: boolean | undefined; display_field_widths?: Record<string, number> | undefined; paired_field_key?: string | undefined; cardinality?: "one" | "many" | undefined; } | { key: string; name: string; description: string; type: "rollup"; source_field_key: string; aggregate_option: { operation: "count"; field_key?: string | undefined; } | { operation: "unique" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique"; field_key: string; } | { operation: "min" | "unique" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "sum" | "avg" | "median" | "max" | "range"; field_key: string; } | { operation: "unique" | "date_range" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "earliest" | "latest"; field_key: string; } | { operation: "unique" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "selected"; field_key: string; } | { operation: "unique" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "selected"; field_key: string; } | { operation: "empty" | "filled" | "percent_empty" | "percent_filled" | "selected"; field_key: string; } | { operation: "checked" | "unchecked" | "percent_checked" | "percent_unchecked"; field_key: string; } | { operation: "empty" | "filled" | "percent_empty" | "percent_filled" | "selected"; field_key: string; } | { operation: "min" | "unique" | "date_range" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "sum" | "avg" | "median" | "max" | "range" | "earliest" | "latest" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked"; field_key: string; } | { operation: "min" | "unique" | "date_range" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "sum" | "avg" | "median" | "max" | "range" | "earliest" | "latest"; field_key: string; }; confirm_before_update?: boolean | undefined; required?: boolean | undefined; filter?: TableRecordFiltersGroupNode | undefined; aggregate_field_type?: "number" | "boolean" | "text" | "date" | "select" | "select_member" | "select_record_link" | "rollup" | "files" | "formula" | "lookup" | "button" | "autonumber" | undefined; aggregate_field_format?: string | undefined; aggregate_field_currency?: string | undefined; aggregate_field_unit?: string | undefined; aggregate_field_unit_field?: string | undefined; aggregate_field_currency_field?: string | undefined; } | { key: string; name: string; description: string; type: "lookup"; source_field_key: string; lookup_field_key: string; confirm_before_update?: boolean | undefined; required?: boolean | undefined; lookup_field_type?: "number" | "boolean" | "text" | "date" | "select" | "select_member" | "select_record_link" | "rollup" | "files" | "formula" | "lookup" | "button" | "autonumber" | undefined; lookup_field_format?: string | undefined; lookup_field_currency?: string | undefined; lookup_field_unit?: string | undefined; lookup_field_options?: { key: string; name: string; color?: string | undefined; mark?: { kind: string; name: string; } | undefined; }[] | undefined; order_by?: { field_key: string; direction: "asc" | "desc"; } | undefined; } | { key: string; name: string; description: string; type: "files"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; } | { key: string; name: string; description: string; type: "formula"; formula: { expression: string; format?: "number" | "currency" | "percentage" | "link" | undefined; currency?: string | undefined; unit?: string | undefined; unit_field?: string | undefined; currency_field?: string | undefined; options?: { key: string; name: string; color: "red" | "orange" | "amber" | "yellow" | "lime" | "green" | "emerald" | "teal" | "cyan" | "sky" | "blue" | "indigo" | "violet" | "purple" | "fuchsia" | "pink" | "rose" | "slate" | "gray" | "zinc" | "neutral" | "stone"; mark?: { kind: string; name: string; } | undefined; }[] | undefined; output_type?: "number" | "boolean" | "text" | "date" | "datetime" | "select" | undefined; volatile?: boolean | undefined; }; confirm_before_update?: boolean | undefined; required?: boolean | undefined; } | { key: string; name: string; description: string; type: "button"; text: string; confirm_before_update?: boolean | undefined; required?: boolean | undefined; workflow_id?: string | null | undefined; } | { key: string; name: string; description: string; type: "autonumber"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; prefix?: string | undefined; padding?: number | undefined; template?: string | undefined; };
|
|
8
16
|
export type TableRecordFilters = TableRecordFiltersConditionNode | TableRecordFiltersTraversalNode | TableRecordFiltersGroupNode;
|
|
17
|
+
export type TableRecordFiltersConditionNode = { node_type: "condition"; field_key?: string | undefined; type?: string | undefined; operator: string; value?: unknown; unit_option?: string | undefined; };
|
|
18
|
+
export type TableRecordFiltersGroupNode = { node_type: "group"; logic: "and" | "or"; children: (TableRecordFilters)[]; };
|
|
19
|
+
export type TableRecordFiltersTraversalNode = { node_type: "traversal"; path: string[]; condition: TableRecordFiltersConditionNode; };
|
package/docs/data_fetching.md
CHANGED
|
@@ -23,7 +23,8 @@ and by `lotics app create --custom` and `lotics app deploy`. Every hook reads th
|
|
|
23
23
|
| `useQuery(alias, params?, { more: n })` | accumulated `rows` + `loadMore` | infinite scroll / "load more" feeds |
|
|
24
24
|
| `useQuery(alias, params?, { rows: false, total: true })` | `total` only, no rows (`total: { by: column }` adds `counts`) | a facet chip, a queue badge, an "N awaiting approval" tile — the size of a set you are not listing |
|
|
25
25
|
| `useQueries(calls, opts?)` | one state per `{ alias, params?, filter?, sort?, aggregate?, total?, rows? }`, in order — a `sort` orders that call's rows before the cap cuts them | reads that are DATA — a list known only at render, one read per item, or one count per stage (`rows: false, total: true`, each state's `.total`); the deploy's alias scan reads a list as dynamic, so every alias in it is still declared. With `aggregate` a call answers the groups of its filtered rows instead of the rows ([queries](./queries.md)) |
|
|
26
|
-
| `queryAll(alias, params?, { filter, sort })` | a promise of every row | outside React, for a job that must hold the whole narrowed set (an
|
|
26
|
+
| `queryAll(alias, params?, { filter, sort })` | a promise of every row | outside React, for a job that must hold the whole narrowed set in the browser (the ids an act runs on): it asks page after page until the server says the set ended, never stopping at the 10,000-row cap. The server's rows as stored — no write of this app is drawn over them |
|
|
27
|
+
| `exportQuery(alias, { params, filter, sort, report, file })` | a promise of the file | every row the narrowed set holds, made into one file by the server — no row reaches the browser. `report` is `{ title, lines, readings }`; `file` is `{ kind: "workbook", language, sheet, columns }` (the default report workbook, each column `{ key, header, type }` an output column of the query — a `number` one on a number column, a `date` one on a date or datetime) or `{ kind: "template", template }`, one the query declares under `templates` by name ([queries](./queries.md)). Past 20,000 rows it rejects, never cuts; each caller makes at most 60 exports a minute. The file's `url` is signed for the caller: open it with `openExternal` |
|
|
27
28
|
|
|
28
29
|
Every hook answers the same `QueryState`: `rows`, `truncated`, `total`, `counts`, `loading`,
|
|
29
30
|
`isValidating`, `error`, `pending`, `refetch`, `page`, `setPage`, `pageCount`, `hasMore`,
|
|
@@ -170,6 +170,7 @@ in it, while a resolved CELL keeps naming them.
|
|
|
170
170
|
`image` is the avatar URL (a presigned URL valid 24 hours, or the member's external OAuth photo) and
|
|
171
171
|
may be `null`. `name` may be null/empty for members without a display name — fall back to `email`. On failure the hook does not throw: it
|
|
172
172
|
resolves `{ members: [], loading: false, error }`.
|
|
173
|
+
`{ enabled: false }` reads nothing until the screen needs the roster.
|
|
173
174
|
|
|
174
175
|
Every hook reading one `group` shares one roster: it is read again when a screen reading it mounts, never on focus.
|
|
175
176
|
|
package/docs/queries.md
CHANGED
|
@@ -45,6 +45,14 @@ Queries live on the app, as an alias → declaration map. `set_app_query` binds
|
|
|
45
45
|
- **`description`** — one line saying what the query returns, capped at 300 characters. No app
|
|
46
46
|
code reads it: it is what the **agents** that reach this app's data (its declared agents, a
|
|
47
47
|
member's chat while the app is open) read to choose between your aliases.
|
|
48
|
+
- **`templates`** — the document templates an `exportQuery` of this alias may fill, by the name
|
|
49
|
+
the export asks for: `{ "<name>": { "template_id": "dtl_…", "rows": "<key>" } }`. The template
|
|
50
|
+
is filled with `{ title, lines, readings, <key>: every row }`, each row keyed by output column —
|
|
51
|
+
a figure, a yes/no, a date or moment as stored (ISO), anything else its words (options,
|
|
52
|
+
members, linked rows and files by name, comma-joined). An excel template makes a workbook, an
|
|
53
|
+
html one a PDF. Binding refuses a template that is not a live excel or html template of the
|
|
54
|
+
app's workspace, one the app's owner may not use, or one reading at its root anything but
|
|
55
|
+
those keys; `rows` is never `title`, `lines` or `readings`.
|
|
48
56
|
- Aliases must be valid JS identifiers (`useQuery("openOrders")` and the generated types depend on it).
|
|
49
57
|
|
|
50
58
|
A deploy never touches the bindings; `get_app_query` reads one back. The **server holds the
|
|
@@ -56,7 +64,7 @@ query's reach (a token in a `table_id` or field-key position fails validation).
|
|
|
56
64
|
|
|
57
65
|
Binding fails — with the compiler's own message, never raw SQL/Postgres text — when any alias:
|
|
58
66
|
|
|
59
|
-
1. isn't a valid identifier, or the declaration isn't `{ ast, params?, description? }` (a
|
|
67
|
+
1. isn't a valid identifier, or the declaration isn't `{ ast, params?, description?, templates? }` (a
|
|
60
68
|
description over 300 characters is refused here too);
|
|
61
69
|
2. references `{{params.x}}` without declaring param `x` (typos can't silently widen);
|
|
62
70
|
3. doesn't parse as a `QueryNode` **as the raw template** (the runtime parses the stored
|
package/docs/runtime.md
CHANGED
|
@@ -62,15 +62,18 @@ Activation is a **two-step gate** — both must hold, so demo data shipping in t
|
|
|
62
62
|
bundle never leaks into normal traffic:
|
|
63
63
|
|
|
64
64
|
1. A fixture is registered via `mount({ fixture })` (`AppFixture` type:
|
|
65
|
-
`dist/mock.d.ts` — `{ queries?, workflows?, recordings? }`).
|
|
65
|
+
`dist/mock.d.ts` — `{ queries?, workflows?, exports?, recordings? }`).
|
|
66
66
|
2. The page URL carries `?__mock=1` (exactly `1`). Without the flag the fixture
|
|
67
67
|
is completely inert.
|
|
68
68
|
|
|
69
69
|
When active, the [query hooks](./data_fetching.md) return `fixture.queries[alias]`
|
|
70
|
-
instead of making any request (`loading` stays `false
|
|
70
|
+
instead of making any request (`loading` stays `false`, except for a function fixture that
|
|
71
|
+
returns a promise: the read stays loading until it settles, to review a pending state); a read with `enabled: false` asks
|
|
71
72
|
the fixture nothing, as it asks the server nothing. **Partial mocking is
|
|
72
73
|
supported**: an alias absent from the fixture still flows through the real
|
|
73
|
-
transport.
|
|
74
|
+
transport. `fixture.exports[alias]` answers `exportQuery(alias, call)` — a
|
|
75
|
+
function of the call returning the file, or a promise of it to review the
|
|
76
|
+
pending state. Calling `mount` again (HMR) replaces the registration last-write-wins.
|
|
74
77
|
|
|
75
78
|
- **Fixture rows stand in for wire rows.** They pass through the same code path
|
|
76
79
|
as fetched rows, so shape them exactly like the query's real output — the same
|
|
@@ -223,9 +226,9 @@ const { rows } = await rpc<{ rows: Record<string, unknown>[] }>("query", {
|
|
|
223
226
|
|
|
224
227
|
`rpc<T = unknown>(op: RpcOp, payload: unknown): Promise<T>` sends one op over
|
|
225
228
|
whichever transport is active. **Prefer the hooks** — they add the SDK's cache,
|
|
226
|
-
loading/error state, analytics, and typing from your declared aliases;
|
|
227
|
-
of a set
|
|
228
|
-
|
|
229
|
+
loading/error state, analytics, and typing from your declared aliases; a file of
|
|
230
|
+
every row of a set is `exportQuery`, made by the server ([data_fetching](./data_fetching.md)).
|
|
231
|
+
`rpc()` exists for the imperative cases neither
|
|
229
232
|
models. `T` is *your* assertion — the SDK does not validate the result shape.
|
|
230
233
|
|
|
231
234
|
The full `RpcOp` union (`dist/rpc.d.ts`), each op's payload, and where its
|
|
@@ -234,6 +237,7 @@ semantics are documented:
|
|
|
234
237
|
| Op | Payload | Resolves to | Owning doc |
|
|
235
238
|
|---|---|---|---|
|
|
236
239
|
| `query` | `{ alias, params?, limit?, offset?, … }` | `{ rows }` | [queries](./queries.md), [data fetching](./data_fetching.md) |
|
|
240
|
+
| `export` | `{ alias, params?, filter?, sort?, report, file }` | `{ file }` | [data fetching](./data_fetching.md) |
|
|
237
241
|
| `field_options` | `{ alias }` | `{ fields }` | [members & options](./members_and_options.md) |
|
|
238
242
|
| `workflow` | `{ alias, inputs }` | `WorkflowResult` | [mutations](./mutations.md) |
|
|
239
243
|
| `upload` | `{ file: File }` (the `File` crosses the bridge by structured clone) | uploaded-file object | [files](./files.md) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/app-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.102.0",
|
|
4
4
|
"description": "The SDK a Lotics custom-code app reads and writes through \u2014 typed hooks over the host bridge, cell readers, mount() and AppRouter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|