@lotics/app-sdk 0.110.0 → 0.111.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/index.js +97 -9
- package/dist/router.d.ts +3 -1
- package/dist/router.js +39 -12
- package/dist/rpc.d.ts +5 -1
- package/docs/ai.md +1 -1
- package/docs/data_fetching.md +1 -1
- package/docs/mutations.md +2 -2
- package/docs/navigation_and_state.md +19 -0
- package/docs/queries.md +5 -5
- package/docs/recipes.md +2 -3
- package/docs/workflows.md +2 -2
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -21,7 +21,7 @@ This file is the index. The **exact type** of anything is its shipped declaratio
|
|
|
21
21
|
| [docs/recipes.md](./docs/recipes.md) | Task-shaped how-tos — returning a generated file, returning structured data, parameterized lookups, composable optional filters, cell decoding, testing an AI action without spending credits. |
|
|
22
22
|
| [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload` and its `fidelity`, `renameFile` (a new file over the same bytes), `useAttachments`/`useAttachmentPiles`, `readFiles` and presigned URLs (a bearer credential — never logged or persisted), workflow-generated files, naming a zip's entries, the delivery bounds. |
|
|
23
23
|
| [docs/members_and_options.md](./docs/members_and_options.md) | People, select options and comments — `useMembers`, `useFieldOptions`, `useViewer`, `useWorkspaceTimezone`/`useWorkspaceCurrency`, `useAppContext`, the workspace's zone at the root, `useComments`. |
|
|
24
|
-
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model), `useUrlState` + `urlParam` codecs, `useRecents`, `useFolderPick`. |
|
|
24
|
+
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model, the screens and `drawers` it reports to the host), `useUrlState` + `urlParam` codecs, `useRecents`, `useFolderPick`. |
|
|
25
25
|
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming parts, the agent's ask-back), `askAi`, `useAiContext`, and what the member's own chat agent can do with the app while it is open. |
|
|
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, why a per-input bound is a tenancy floor rather than an authorization check. |
|
|
27
27
|
| [docs/runtime.md](./docs/runtime.md) | `mount()` and the errors it reports to the host (`reportAppError`), the two transports, the API address a standalone bundle reads out of its own page (`<meta name="lotics-api-base">`), `rpc()`, the mock harness (`fixture` + `?__mock=1` — queries, workflows and agents, a mocked agent's run played line by line), `openExternal`/`openApp`/`downloadFile`, geofencing, the peer dependencies. |
|
package/dist/index.js
CHANGED
|
@@ -18911,7 +18911,7 @@ var kitGlyphSchema = zod_default.string().superRefine((name, ctx) => {
|
|
|
18911
18911
|
var optionMarkSchema = zod_default.discriminatedUnion("kind", [
|
|
18912
18912
|
zod_default.object({
|
|
18913
18913
|
kind: zod_default.literal("brand"),
|
|
18914
|
-
name: zod_default.enum(BRAND_NAMES).describe(
|
|
18914
|
+
name: zod_default.enum(BRAND_NAMES).describe("The channel the option IS, drawn as its logo")
|
|
18915
18915
|
}).strict(),
|
|
18916
18916
|
zod_default.object({
|
|
18917
18917
|
kind: zod_default.literal("icon"),
|
|
@@ -20794,7 +20794,40 @@ function viReadGroup(n, leading) {
|
|
|
20794
20794
|
}
|
|
20795
20795
|
return parts.join(" ");
|
|
20796
20796
|
}
|
|
20797
|
-
|
|
20797
|
+
var EN_ONES = [
|
|
20798
|
+
"zero",
|
|
20799
|
+
"one",
|
|
20800
|
+
"two",
|
|
20801
|
+
"three",
|
|
20802
|
+
"four",
|
|
20803
|
+
"five",
|
|
20804
|
+
"six",
|
|
20805
|
+
"seven",
|
|
20806
|
+
"eight",
|
|
20807
|
+
"nine",
|
|
20808
|
+
"ten",
|
|
20809
|
+
"eleven",
|
|
20810
|
+
"twelve",
|
|
20811
|
+
"thirteen",
|
|
20812
|
+
"fourteen",
|
|
20813
|
+
"fifteen",
|
|
20814
|
+
"sixteen",
|
|
20815
|
+
"seventeen",
|
|
20816
|
+
"eighteen",
|
|
20817
|
+
"nineteen"
|
|
20818
|
+
];
|
|
20819
|
+
var EN_TENS = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"];
|
|
20820
|
+
var EN_SCALES = ["", "thousand", "million", "billion", "trillion", "quadrillion", "quintillion"];
|
|
20821
|
+
function enReadGroup(n) {
|
|
20822
|
+
const hundreds = Math.floor(n / 100);
|
|
20823
|
+
const rest = n % 100;
|
|
20824
|
+
const parts = [];
|
|
20825
|
+
if (hundreds > 0) parts.push(EN_ONES[hundreds], "hundred");
|
|
20826
|
+
if (rest >= 20) parts.push(rest % 10 === 0 ? EN_TENS[rest / 10] : `${EN_TENS[Math.floor(rest / 10)]}-${EN_ONES[rest % 10]}`);
|
|
20827
|
+
else if (rest > 0) parts.push(EN_ONES[rest]);
|
|
20828
|
+
return parts.join(" ");
|
|
20829
|
+
}
|
|
20830
|
+
function numberToWords(value, language = "vi") {
|
|
20798
20831
|
if (value == null) return "";
|
|
20799
20832
|
let n;
|
|
20800
20833
|
if (typeof value === "number") {
|
|
@@ -20807,9 +20840,13 @@ function numberToWords(value) {
|
|
|
20807
20840
|
if (!isFinite(n)) {
|
|
20808
20841
|
throw new Error("numberToWords: value must be a finite number");
|
|
20809
20842
|
}
|
|
20843
|
+
if (language !== "vi" && language !== "en") {
|
|
20844
|
+
throw new Error(`numberToWords: language must be "vi" or "en", got ${JSON.stringify(language)}`);
|
|
20845
|
+
}
|
|
20810
20846
|
n = Math.round(n);
|
|
20811
20847
|
const negative = n < 0;
|
|
20812
20848
|
n = Math.abs(n);
|
|
20849
|
+
if (language === "en") return enNumberToWords(n, negative);
|
|
20813
20850
|
if (n === 0) return "kh\xF4ng";
|
|
20814
20851
|
const groups = [];
|
|
20815
20852
|
for (let rem = n; rem > 0; rem = Math.floor(rem / 1e3)) {
|
|
@@ -20825,6 +20862,19 @@ function numberToWords(value) {
|
|
|
20825
20862
|
const result = parts.join(" ");
|
|
20826
20863
|
return negative ? "\xE2m " + result : result;
|
|
20827
20864
|
}
|
|
20865
|
+
function enNumberToWords(n, negative) {
|
|
20866
|
+
if (n === 0) return "zero";
|
|
20867
|
+
const parts = [];
|
|
20868
|
+
let group = 0;
|
|
20869
|
+
for (let rem = n; rem > 0; rem = Math.floor(rem / 1e3), group++) {
|
|
20870
|
+
const value = rem % 1e3;
|
|
20871
|
+
if (value === 0) continue;
|
|
20872
|
+
if (group >= EN_SCALES.length) throw new Error("numberToWords: value is too large to read in English");
|
|
20873
|
+
parts.unshift(EN_SCALES[group] ? `${enReadGroup(value)} ${EN_SCALES[group]}` : enReadGroup(value));
|
|
20874
|
+
}
|
|
20875
|
+
const result = parts.join(" ");
|
|
20876
|
+
return negative ? "minus " + result : result;
|
|
20877
|
+
}
|
|
20828
20878
|
function formatNumber(value, decimals) {
|
|
20829
20879
|
if (value == null) return "";
|
|
20830
20880
|
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
@@ -23112,7 +23162,23 @@ var TZDate = class _TZDate extends TZDateMini {
|
|
|
23112
23162
|
|
|
23113
23163
|
// ../shared/src/expression_date.ts
|
|
23114
23164
|
function hasTimezone(dateStr) {
|
|
23115
|
-
return /[Zz]|[+-]\d{2}
|
|
23165
|
+
return /[Zz]|[+-]\d{2}:?\d{2}$/.test(dateStr);
|
|
23166
|
+
}
|
|
23167
|
+
var LONG_FORMAT_TOKENS = /P+p+|P+|p+|''|'(''|[^'])+('|$)|./g;
|
|
23168
|
+
var FORMAT_TOKENS = /[yYQqMLwIdDecihHKkms]o|(\w)\1*|''|'(''|[^'])+('|$)|./g;
|
|
23169
|
+
var FORMAT_TOKEN_LETTERS = new Set("GyYRuQqMLwIdDEecihabBHKkmsSXxOztT");
|
|
23170
|
+
var PROTECTED_TOKENS = { D: "d", DD: "dd", YY: "yy", YYYY: "yyyy" };
|
|
23171
|
+
function datePatternProblem(pattern) {
|
|
23172
|
+
const tokens = (pattern.match(LONG_FORMAT_TOKENS) ?? []).map((token2) => /^[Pp]/.test(token2) ? "" : token2).join("").match(FORMAT_TOKENS) ?? [];
|
|
23173
|
+
for (const token2 of tokens) {
|
|
23174
|
+
if (token2.startsWith("'")) continue;
|
|
23175
|
+
const instead = PROTECTED_TOKENS[token2];
|
|
23176
|
+
if (instead !== void 0) return `"${token2}" is not a day of the month or a calendar year \u2014 write "${instead}"`;
|
|
23177
|
+
if (!FORMAT_TOKEN_LETTERS.has(token2.charAt(0)) && /[a-zA-Z]/.test(token2.charAt(0))) {
|
|
23178
|
+
return `"${token2.charAt(0)}" is no date token \u2014 quote literal text, as in "'Ng\xE0y' dd 'th\xE1ng' MM 'n\u0103m' yyyy"`;
|
|
23179
|
+
}
|
|
23180
|
+
}
|
|
23181
|
+
return void 0;
|
|
23116
23182
|
}
|
|
23117
23183
|
function parseDateStringInTimezone(dateStr, timezone) {
|
|
23118
23184
|
const match2 = dateStr.match(
|
|
@@ -23214,13 +23280,18 @@ function getDateExpressionFunctions(defaultTimezone) {
|
|
|
23214
23280
|
throw new Error("formatDate: formatStr must be a string");
|
|
23215
23281
|
}
|
|
23216
23282
|
validateOptionalTimezone(timezone, "formatDate");
|
|
23283
|
+
const problem = datePatternProblem(formatStr);
|
|
23284
|
+
if (problem !== void 0) {
|
|
23285
|
+
throw new Error(`formatDate: ${problem}`);
|
|
23286
|
+
}
|
|
23217
23287
|
const tz = getTimezone(timezone);
|
|
23218
23288
|
if (typeof date6 === "string" && !hasTimezone(date6)) {
|
|
23219
23289
|
const testDate = new Date(date6);
|
|
23220
23290
|
if (isNaN(testDate.getTime())) {
|
|
23221
23291
|
throw new Error("formatDate: invalid date");
|
|
23222
23292
|
}
|
|
23223
|
-
|
|
23293
|
+
const iso = parseISO(date6);
|
|
23294
|
+
return format(isNaN(iso.getTime()) ? testDate : iso, formatStr);
|
|
23224
23295
|
}
|
|
23225
23296
|
const dateObj = date6 instanceof Date ? date6 : new Date(date6);
|
|
23226
23297
|
if (isNaN(dateObj.getTime())) {
|
|
@@ -24500,16 +24571,16 @@ var appSchema = zod_default.object({
|
|
|
24500
24571
|
"Whether a shared password gates the public binding. True \u2192 anonymous visitors must authenticate at `/v1/apps/{app_id}/public/authenticate` before any publicAppAccess endpoint resolves. The hash itself is never sent over the wire; only this flag is exposed (and only on authenticated owner-side reads \u2014 the public by-subdomain response surfaces the same fact as `requires_password`)."
|
|
24501
24572
|
),
|
|
24502
24573
|
workflows: zod_default.record(zod_default.string(), appBoundWorkflowSchema).nullable().optional().describe(
|
|
24503
|
-
"Alias \u2192 workflow declaration map. Each alias resolves to a workflow_id and an optional typed inputs schema, set by `set_app_workflow` / `
|
|
24574
|
+
"Alias \u2192 workflow declaration map. Each alias resolves to a workflow_id and an optional typed inputs schema, set by `set_app_workflow` / `remove_app_binding` or a model apply; a deploy carries it forward. The iframe SDK's useWorkflow(alias) resolves through this map; when an inputs schema is declared, the server validates payloads against it before invocation. The workflow always executes under the app's IAM principal."
|
|
24504
24575
|
),
|
|
24505
24576
|
queries: zod_default.record(zod_default.string(), appQueryDeclarationSchema).nullable().optional().describe(
|
|
24506
|
-
"Alias \u2192 query declaration map. Each alias resolves to a fixed query AST template with a typed param schema, set by `
|
|
24577
|
+
"Alias \u2192 query declaration map. Each alias resolves to a fixed query AST template with a typed param schema, set by `set_app_queries` or a model apply; a deploy carries it forward. The iframe SDK's useQuery(alias, params) resolves through this map; custom-code apps never send a raw AST. The query runs under the app's IAM principal."
|
|
24507
24578
|
),
|
|
24508
24579
|
capabilities: appCapabilitiesSchema.nullable().optional().describe(
|
|
24509
24580
|
"Opt-in app capabilities, set by `update_app` or a model apply; a deploy carries them forward. Capabilities are off unless declared \u2014 least ambient authority. `comments` gates the members-only `useComments` primitive: only an app that declares it can read/write record comments (each under the VIEWING member's own authority)."
|
|
24510
24581
|
),
|
|
24511
24582
|
agents: zod_default.record(zod_default.string(), appAgentDeclarationSchema).nullable().optional().describe(
|
|
24512
|
-
"Alias \u2192 agent declaration map. Each alias binds a streaming tool-loop agent the app runs via `useAgentRun(alias)` (an SSE stream), with declared tools, model, and typed inputs/outputs, set by `set_app_agent` / `
|
|
24583
|
+
"Alias \u2192 agent declaration map. Each alias binds a streaming tool-loop agent the app runs via `useAgentRun(alias)` (an SSE stream), with declared tools, model, and typed inputs/outputs, set by `set_app_agent` / `remove_app_binding` or a model apply; a deploy carries it forward. The agent runs under the app's IAM principal; runs persist a flat history per session."
|
|
24513
24584
|
),
|
|
24514
24585
|
theme: appThemeSchema.nullable().optional().describe("Theme settings for the app"),
|
|
24515
24586
|
can_author: zod_default.boolean().optional().describe(
|
|
@@ -24709,8 +24780,15 @@ var cForStepSchema = zod_default.lazy(
|
|
|
24709
24780
|
);
|
|
24710
24781
|
var workflowStepsSchema = zod_default.array(workflowStepSchema).min(1);
|
|
24711
24782
|
|
|
24783
|
+
// ../shared/src/retired_tools.ts
|
|
24784
|
+
var RETIRED_TOOLS = /* @__PURE__ */ new Map([
|
|
24785
|
+
["generate_pdf_from_template", "generate_document"],
|
|
24786
|
+
["generate_excel_from_template", "generate_document"],
|
|
24787
|
+
["generate_word_from_template", "generate_document"]
|
|
24788
|
+
]);
|
|
24789
|
+
|
|
24712
24790
|
// ../shared/src/tool_input_semantics_registry.ts
|
|
24713
|
-
var
|
|
24791
|
+
var CURRENT_TOOL_INPUT_SEMANTICS = {
|
|
24714
24792
|
// ─── Bulk record mutations ─────────────────────────────────────────────
|
|
24715
24793
|
update_records: {
|
|
24716
24794
|
table_id: { kind: "table_ref", rows: "change" },
|
|
@@ -24935,7 +25013,7 @@ var TOOL_INPUT_SEMANTICS = {
|
|
|
24935
25013
|
force: { kind: "scalar" }
|
|
24936
25014
|
},
|
|
24937
25015
|
// ─── Document-generation tools ────────────────────────────────────────
|
|
24938
|
-
|
|
25016
|
+
generate_document: {
|
|
24939
25017
|
data: { kind: "template_variables" },
|
|
24940
25018
|
document_template_id: { kind: "scalar" },
|
|
24941
25019
|
filename: { kind: "text" }
|
|
@@ -25242,6 +25320,15 @@ var TOOL_INPUT_SEMANTICS = {
|
|
|
25242
25320
|
take: { kind: "scalar" }
|
|
25243
25321
|
}
|
|
25244
25322
|
};
|
|
25323
|
+
var TOOL_INPUT_SEMANTICS = {
|
|
25324
|
+
...CURRENT_TOOL_INPUT_SEMANTICS,
|
|
25325
|
+
...Object.fromEntries(
|
|
25326
|
+
[...RETIRED_TOOLS].flatMap(([retired, replacement]) => {
|
|
25327
|
+
const semantics = CURRENT_TOOL_INPUT_SEMANTICS[replacement];
|
|
25328
|
+
return semantics ? [[retired, semantics]] : [];
|
|
25329
|
+
})
|
|
25330
|
+
)
|
|
25331
|
+
};
|
|
25245
25332
|
|
|
25246
25333
|
// ../shared/src/workflow_step_helpers.ts
|
|
25247
25334
|
function stepChildStepArrays(step) {
|
|
@@ -27134,6 +27221,7 @@ var TABLE_ID_PATTERN = new RegExp(
|
|
|
27134
27221
|
`^tbl_[${ID_ALPHABET}]+$|^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`,
|
|
27135
27222
|
"i"
|
|
27136
27223
|
);
|
|
27224
|
+
var PREFIXED_ID_PATTERN = new RegExp(`^[a-z]{3}_[${ID_ALPHABET}]{${ID_LENGTH}}$`);
|
|
27137
27225
|
|
|
27138
27226
|
// ../shared/src/table_field_keys.ts
|
|
27139
27227
|
var nanoid4 = customAlphabet(ID_ALPHABET, 6);
|
package/dist/router.d.ts
CHANGED
|
@@ -4,7 +4,9 @@ export interface NotFoundWords {
|
|
|
4
4
|
message: (path: string) => string;
|
|
5
5
|
firstScreen: string;
|
|
6
6
|
}
|
|
7
|
-
export declare function AppRouter({ routes, notFound }: {
|
|
7
|
+
export declare function AppRouter({ routes, notFound, drawers }: {
|
|
8
8
|
routes: RouteObject[];
|
|
9
9
|
notFound?: NotFoundWords;
|
|
10
|
+
/** The query keys a drawer opens at, the one drawn lowest first: each open counts as a screen over the path's. */
|
|
11
|
+
drawers?: readonly string[];
|
|
10
12
|
}): import("react").JSX.Element;
|
package/dist/router.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isEmbedded,
|
|
3
|
+
postHostNotification,
|
|
3
4
|
setUrlParams
|
|
4
5
|
} from "./chunk-CRJQYSDI.js";
|
|
5
6
|
|
|
@@ -8,6 +9,7 @@ import { useEffect } from "react";
|
|
|
8
9
|
import {
|
|
9
10
|
BrowserRouter,
|
|
10
11
|
Link,
|
|
12
|
+
matchRoutes,
|
|
11
13
|
useLocation,
|
|
12
14
|
useRoutes
|
|
13
15
|
} from "react-router";
|
|
@@ -20,12 +22,40 @@ function screenHref(loc) {
|
|
|
20
22
|
const qs = search.toString();
|
|
21
23
|
return loc.pathname + (qs ? `?${qs}` : "") + loc.hash;
|
|
22
24
|
}
|
|
23
|
-
function
|
|
25
|
+
function routePattern(matches, pathname) {
|
|
26
|
+
const concrete = pathname.split("/").filter((segment) => segment !== "");
|
|
27
|
+
let shape = [];
|
|
28
|
+
for (const { route, params } of matches) {
|
|
29
|
+
const path = route.path ?? "";
|
|
30
|
+
if (path.startsWith("/")) shape = [];
|
|
31
|
+
for (const segment of path.split("/")) {
|
|
32
|
+
if (segment === "") continue;
|
|
33
|
+
if (!segment.endsWith("?")) {
|
|
34
|
+
shape.push(segment);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const base = segment.slice(0, -1);
|
|
38
|
+
const present = base.startsWith(":") ? params[base.slice(1)] !== void 0 : concrete[shape.length]?.toLowerCase() === base.toLowerCase();
|
|
39
|
+
if (present) shape.push(base);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return `/${shape.join("/")}`;
|
|
43
|
+
}
|
|
44
|
+
function HostScreenMirror({ routes, drawers }) {
|
|
24
45
|
const location = useLocation();
|
|
25
46
|
const href = screenHref(location);
|
|
26
47
|
useEffect(() => {
|
|
27
48
|
void setUrlParams({ [LOC_KEY]: href });
|
|
28
49
|
}, [href]);
|
|
50
|
+
const { pathname, search } = location;
|
|
51
|
+
const params = new URLSearchParams(search);
|
|
52
|
+
const opened = drawers.flatMap((key) => params.getAll(key).map((value) => ({ key, value })));
|
|
53
|
+
const matches = matchRoutes(routes, pathname);
|
|
54
|
+
const route = matches === null ? null : routePattern(matches, pathname) + opened.map(({ key }) => `+${key}`).join("");
|
|
55
|
+
const openedAt = JSON.stringify(opened.map(({ value }) => value));
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (route !== null) postHostNotification({ type: "screen_view", route });
|
|
58
|
+
}, [pathname, openedAt, route]);
|
|
29
59
|
return null;
|
|
30
60
|
}
|
|
31
61
|
function RoutedRoutes({ routes }) {
|
|
@@ -76,20 +106,17 @@ function firstScreenPath(routes) {
|
|
|
76
106
|
}
|
|
77
107
|
function AppRouter({
|
|
78
108
|
routes,
|
|
79
|
-
notFound = NOT_FOUND_ENGLISH
|
|
109
|
+
notFound = NOT_FOUND_ENGLISH,
|
|
110
|
+
drawers = []
|
|
80
111
|
}) {
|
|
81
112
|
const embedded = isEmbedded();
|
|
113
|
+
const claimed = withNotFound(
|
|
114
|
+
routes,
|
|
115
|
+
/* @__PURE__ */ jsx(NotFoundScreen, { home: firstScreenPath(routes), words: notFound })
|
|
116
|
+
);
|
|
82
117
|
return /* @__PURE__ */ jsxs(BrowserRouter, { children: [
|
|
83
|
-
embedded ? /* @__PURE__ */ jsx(HostScreenMirror, {}) : null,
|
|
84
|
-
/* @__PURE__ */ jsx(
|
|
85
|
-
RoutedRoutes,
|
|
86
|
-
{
|
|
87
|
-
routes: withNotFound(
|
|
88
|
-
routes,
|
|
89
|
-
/* @__PURE__ */ jsx(NotFoundScreen, { home: firstScreenPath(routes), words: notFound })
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
)
|
|
118
|
+
embedded ? /* @__PURE__ */ jsx(HostScreenMirror, { routes: claimed, drawers }) : null,
|
|
119
|
+
/* @__PURE__ */ jsx(RoutedRoutes, { routes: claimed })
|
|
93
120
|
] });
|
|
94
121
|
}
|
|
95
122
|
export {
|
package/dist/rpc.d.ts
CHANGED
|
@@ -49,7 +49,11 @@ export type HostNotification = {
|
|
|
49
49
|
type: "aiContext";
|
|
50
50
|
slot: string;
|
|
51
51
|
context: AiContextValue | null;
|
|
52
|
-
} | AppErrorReport
|
|
52
|
+
} | AppErrorReport | {
|
|
53
|
+
type: "screen_view";
|
|
54
|
+
/** The route pattern that claims the screen (`/item/:id`), a `+<key>` per drawer open over it; never its address. */
|
|
55
|
+
route: string;
|
|
56
|
+
};
|
|
53
57
|
/** The app's identity: the host's context when bridged, `/by-subdomain` (no member) standalone. */
|
|
54
58
|
export interface AppContext {
|
|
55
59
|
member_id: string | null;
|
package/docs/ai.md
CHANGED
|
@@ -13,7 +13,7 @@ Don't run a structured extraction through `askAi` (the result is stranded in a c
|
|
|
13
13
|
|
|
14
14
|
## Declared agents — what `useAgentRun` runs
|
|
15
15
|
|
|
16
|
-
An agent is **declared on the app server-side, by alias**, with the `set_app_agent` tool (removed with `
|
|
16
|
+
An agent is **declared on the app server-side, by alias**, with the `set_app_agent` tool (removed with `remove_app_binding`) — the one way an agent is bound or changed, each write a new version of the app; a deploy never touches it, and `get_app_agent` reads one back. Invoking an alias that isn't bound fails with a "no agent alias" error naming `set_app_agent`.
|
|
17
17
|
|
|
18
18
|
A declaration carries:
|
|
19
19
|
|
package/docs/data_fetching.md
CHANGED
|
@@ -7,7 +7,7 @@ every write goes through a workflow — [./mutations.md](./mutations.md). Exact
|
|
|
7
7
|
`dist/queries.d.ts`, `dist/row.d.ts`, `dist/select.d.ts`, `dist/members.d.ts`.
|
|
8
8
|
|
|
9
9
|
The read model in one paragraph: an app never sends a raw query. It invokes a **named query by
|
|
10
|
-
alias** (bound with `
|
|
10
|
+
alias** (bound with `set_app_queries`) and fills the template's declared `{{params.x}}`
|
|
11
11
|
value holes; the server holds the canonical AST and runs it under the **app owner's** authority
|
|
12
12
|
([./security.md](./security.md)). The generated `.lotics/app_queries.d.ts` augments `AppQueries`,
|
|
13
13
|
so params are typed per the binding; an alias it does not carry is accepted as a plain string,
|
package/docs/mutations.md
CHANGED
|
@@ -138,8 +138,8 @@ neither, so the refusal arrives and marks no control.
|
|
|
138
138
|
|
|
139
139
|
### Generated files come back in `files[]`
|
|
140
140
|
|
|
141
|
-
Any step in the run whose tool output carries a `file_id` (most commonly
|
|
142
|
-
`
|
|
141
|
+
Any step in the run whose tool output carries a `file_id` (most commonly
|
|
142
|
+
`generate_document`) is collected automatically into
|
|
143
143
|
`result.files[]` — each an `UploadedFile`
|
|
144
144
|
`{ id, filename, mime_type, url?, thumbnail_url? }` with a presigned `url` (24-hour TTL) the
|
|
145
145
|
app can open directly:
|
|
@@ -121,6 +121,25 @@ Observable details:
|
|
|
121
121
|
to the framework. Never declare them in a `useUrlState` shape or write them
|
|
122
122
|
yourself; undeclared keys are already preserved automatically (see below).
|
|
123
123
|
|
|
124
|
+
### Which screens are used (embedded)
|
|
125
|
+
|
|
126
|
+
Each time the path changes, `AppRouter` tells the host which route claims the
|
|
127
|
+
new screen, as that route is declared: `/item/:id`, never `/item/rec_8f2…`, and
|
|
128
|
+
never the query or hash. The host counts one view per change, so a
|
|
129
|
+
`useUrlState` filter is not a new screen and a step to the next item is. A
|
|
130
|
+
static segment is reported as written, so never build a route's `path` from
|
|
131
|
+
your data: a record's id or name goes in a `:param`.
|
|
132
|
+
|
|
133
|
+
A drawer the app opens at a query key rather than a path is a screen too when
|
|
134
|
+
`drawers` names its key, listed from the one drawn lowest:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
<AppRouter routes={routes} drawers={["item"]} />
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`/?item=rec_8f2…` is then counted as `/+item`, one `+<key>` per drawer open, and
|
|
141
|
+
opening, stepping or closing one is a new view; the key's value is never sent.
|
|
142
|
+
|
|
124
143
|
## `useUrlState` — typed view-state in the address bar
|
|
125
144
|
|
|
126
145
|
Save a declared slice of view-state into the address bar so a filtered view
|
package/docs/queries.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Queries — the query engine authoring reference
|
|
2
2
|
|
|
3
3
|
Every read an app performs is a **named query**: a fixed AST template bound to the app by alias
|
|
4
|
-
(`
|
|
4
|
+
(`set_app_queries`), validated and compiled when it is written, and invoked by alias
|
|
5
5
|
through the read hooks ([data_fetching.md](./data_fetching.md)). This is the authoring reference
|
|
6
6
|
for the query engine itself.
|
|
7
7
|
|
|
@@ -11,11 +11,11 @@ for the query engine itself.
|
|
|
11
11
|
|
|
12
12
|
### Declaration
|
|
13
13
|
|
|
14
|
-
Queries live on the app, as an alias → declaration map. `
|
|
15
|
-
|
|
14
|
+
Queries live on the app, as an alias → declaration map. `set_app_queries` binds one alias or
|
|
15
|
+
several, each call a new version of the app:
|
|
16
16
|
|
|
17
17
|
```jsonc
|
|
18
|
-
//
|
|
18
|
+
// set_app_queries { "app_id": "app_…", "queries": { "openOrders": … } }
|
|
19
19
|
{
|
|
20
20
|
"ast": {
|
|
21
21
|
"kind": "project",
|
|
@@ -28,7 +28,7 @@ Queries live on the app, as an alias → declaration map. `set_app_query` binds
|
|
|
28
28
|
"columns": ["order_code", "customer", "total", "created"]
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
//
|
|
31
|
+
// set_app_queries { "app_id": "app_…", "queries": { "orderByCode": … } }
|
|
32
32
|
{
|
|
33
33
|
"ast": { /* … a filter with "{{params.code}}" … */ },
|
|
34
34
|
"params": { "code": { "type": "text" } },
|
package/docs/recipes.md
CHANGED
|
@@ -11,8 +11,7 @@ A button that produces a file — a quotation, a debit note, a label — and han
|
|
|
11
11
|
**no** record.
|
|
12
12
|
|
|
13
13
|
**The mechanism is the return channel.** Any workflow tool
|
|
14
|
-
whose step output carries a `file_id` (`
|
|
15
|
-
`generate_word_from_template`) is auto-collected by the execute endpoint and comes back in
|
|
14
|
+
whose step output carries a `file_id` (`generate_document`) is auto-collected by the execute endpoint and comes back in
|
|
16
15
|
**`result.files[]`**, each with a servable `url`.
|
|
17
16
|
|
|
18
17
|
So the workflow only *generates*; attaching the file to a record is a separate, optional step, and
|
|
@@ -22,7 +21,7 @@ So the workflow only *generates*; attaching the file to a record is a separate,
|
|
|
22
21
|
// workflow body — alias `genDebit`, input `record_id`
|
|
23
22
|
const rid = trigger.app_workflow.inputs.record_id;
|
|
24
23
|
const rec = await get_record({ table_id: "tbl_x", record_id: rid });
|
|
25
|
-
await
|
|
24
|
+
await generate_document({
|
|
26
25
|
data: { /* … */ }, filename: `Debit_${code}`, document_template_id: "dtl_x",
|
|
27
26
|
});
|
|
28
27
|
return({ status: "success", message: `Đã tạo ${code}` }); // the file is extracted, not returned
|
package/docs/workflows.md
CHANGED
|
@@ -506,7 +506,7 @@ full menu by category, so a miss is one informed retry.
|
|
|
506
506
|
| **Type / null** | `isNull`, `isNotNull`, `isEmpty`, `isString`, `isNumber`, `isBoolean`, `isArray`, `isObject`, `coalesce`, `toNumber`, `toString`, `typeOf`, `parseJson`, `toJson` |
|
|
507
507
|
| **Array** | `size`, `first`, `requireFirst`, `last`, `nth`, `at`, `slice`, `includes`, `filter`, `find`, `some`, `every`, `pluck`, `sortBy`, `groupBy`, `countBy`, `unique`, `uniqueBy`, `compact`, `flatten`, `reverse`, `concat`, `difference`, `differenceBy`, `intersection`, `intersectionBy`, `list`, `range`, `reduce` |
|
|
508
508
|
| **Number** | `sum`, `sumBy`, `mean`, `meanBy`, `min`, `max`, `minBy`, `maxBy`, `round`, `ceil`, `floor`, `abs`, `mod`, `pow`, `sqrt`, `clamp`, `percentage` |
|
|
509
|
-
| **String** | `upper`, `lower`, `capitalize`, `trim`, `contains`, `startsWith`, `endsWith`, `replace`, `replaceAll`, `substring`, `length`, `split`, `join`, `padStart`, `padEnd`, `formatNumber(value, decimals)` (fixed-decimal, ungrouped), `formatDecimal(value, decimals, locale)` (grouped for a reader — `formatDecimal(151000, 0, "vi-VN")` → `151.000`), `numberToWords` |
|
|
509
|
+
| **String** | `upper`, `lower`, `capitalize`, `trim`, `contains`, `startsWith`, `endsWith`, `replace`, `replaceAll`, `substring`, `length`, `split`, `join`, `padStart`, `padEnd`, `formatNumber(value, decimals)` (fixed-decimal, ungrouped), `formatDecimal(value, decimals, locale)` (grouped for a reader — `formatDecimal(151000, 0, "vi-VN")` → `151.000`), `numberToWords(x, lang?)` (`"vi"` or `"en"`) |
|
|
510
510
|
| **Object** | `keys`, `values`, `entries`, `get`, `pick`, `omit`, `merge`, `nonNullKeys` |
|
|
511
511
|
| **Date** | `now`, `formatDate`, `parseDate`, `addDays`, `subDays`, `addHours`, `subHours`, `addMinutes`, `subMinutes`, `startOfDay`, `endOfDay`, `differenceInCalendarDays`, `differenceInHours`, `differenceInMinutes`, `isBefore`, `isAfter`, `isSameDay`, `isToday`, `isWithinRange` |
|
|
512
512
|
| **Other** | `formatCurrency(amount, locale, currency)`, `randomNumber(len)`, `randomAlphaNumeric(len)`, `sample(items)`, `current_member_in_any_group(["grp_…"])` |
|
|
@@ -755,7 +755,7 @@ values and put the writing after it.
|
|
|
755
755
|
matches the same filter.
|
|
756
756
|
### Keep expensive steps off the path the caller waits on
|
|
757
757
|
|
|
758
|
-
`
|
|
758
|
+
`generate_document` and `agent` are the two steps that dominate a body's wall clock. Ask
|
|
759
759
|
whether the person pressing the button needs that artifact **at that instant**. A document that is
|
|
760
760
|
printed later belongs in the workflow that prints it — moving it there also removes the reads that
|
|
761
761
|
existed only to feed it, which is usually where the round trips were hiding.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/app-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.111.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": {
|