@lotics/app-sdk 0.16.0 → 0.18.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/dist/src/hooks.d.ts +15 -4
- package/dist/src/index.d.ts +6 -2
- package/dist/src/index.js +3 -1
- package/dist/src/open_external.d.ts +16 -0
- package/dist/src/open_external.js +19 -0
- package/dist/src/row.d.ts +25 -4
- package/dist/src/row.js +42 -6
- package/dist/src/rpc.d.ts +1 -1
- package/dist/src/rpc.js +23 -0
- package/dist/src/select.d.ts +26 -0
- package/dist/src/select.js +40 -0
- package/package.json +1 -1
package/dist/src/hooks.d.ts
CHANGED
|
@@ -25,9 +25,9 @@ interface QueryState<R> {
|
|
|
25
25
|
* alias → input-type map. Result:
|
|
26
26
|
* - Undeclared alias → compile-time error at the `useWorkflow("...")` site
|
|
27
27
|
* - Declared with full `{workflow_id, inputs}` form → callable typed as
|
|
28
|
-
* `(inputs: <DeclaredShape>) => Promise<
|
|
28
|
+
* `(inputs: <DeclaredShape>) => Promise<WorkflowResult>`
|
|
29
29
|
* - Declared with shorthand (bare workflow_id) → callable typed as
|
|
30
|
-
* `(inputs?: Record<string, unknown>) => Promise<
|
|
30
|
+
* `(inputs?: Record<string, unknown>) => Promise<WorkflowResult>` (untyped inputs)
|
|
31
31
|
*
|
|
32
32
|
* ```tsx
|
|
33
33
|
* const issue = useWorkflow("issueInvoiceStorageDrop");
|
|
@@ -35,8 +35,19 @@ interface QueryState<R> {
|
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
37
|
export declare function useWorkflow<K extends keyof AppWorkflows & string>(alias: K): UseWorkflowFn<K>;
|
|
38
|
-
export declare function useWorkflow(alias: string): (inputs?: Record<string, unknown>) => Promise<
|
|
39
|
-
type UseWorkflowFn<K extends keyof AppWorkflows & string> = AppWorkflows[K] extends Record<string, unknown> ? AppWorkflows[K] extends Record<string, never> ? (inputs?: Record<string, never>) => Promise<
|
|
38
|
+
export declare function useWorkflow(alias: string): (inputs?: Record<string, unknown>) => Promise<WorkflowResult>;
|
|
39
|
+
type UseWorkflowFn<K extends keyof AppWorkflows & string> = AppWorkflows[K] extends Record<string, unknown> ? AppWorkflows[K] extends Record<string, never> ? (inputs?: Record<string, never>) => Promise<WorkflowResult> : (inputs: AppWorkflows[K]) => Promise<WorkflowResult> : (inputs?: Record<string, unknown>) => Promise<WorkflowResult>;
|
|
40
|
+
/**
|
|
41
|
+
* Result of an app-workflow run — the execute endpoint's response. `files` holds
|
|
42
|
+
* any document a workflow step generated (e.g. via a `generate_*_from_template`
|
|
43
|
+
* tool), resolved for download: read `files[0].url` and pass it to `openExternal`.
|
|
44
|
+
* A workflow that generates no file resolves with `files` absent.
|
|
45
|
+
*/
|
|
46
|
+
export interface WorkflowResult {
|
|
47
|
+
status: "success" | "error";
|
|
48
|
+
message?: string;
|
|
49
|
+
files?: UploadedFile[];
|
|
50
|
+
}
|
|
40
51
|
type UseQueryParams<K extends keyof AppQueries & string> = AppQueries[K] extends Record<string, never> ? [params?: Record<string, never>] : [params: AppQueries[K]];
|
|
41
52
|
/**
|
|
42
53
|
* Read rows from a query the app's author declared in `lotics.queries`.
|
package/dist/src/index.d.ts
CHANGED
|
@@ -17,13 +17,17 @@
|
|
|
17
17
|
export { mount } from "./mount.js";
|
|
18
18
|
export type { MountOptions } from "./mount.js";
|
|
19
19
|
export { useWorkflow, useQuery, useFileUpload } from "./hooks.js";
|
|
20
|
-
export type { UploadedFile } from "./hooks.js";
|
|
20
|
+
export type { UploadedFile, WorkflowResult } from "./hooks.js";
|
|
21
21
|
export { rpc } from "./rpc.js";
|
|
22
22
|
export type { RpcOp } from "./rpc.js";
|
|
23
|
+
export { openExternal } from "./open_external.js";
|
|
23
24
|
export { readMembers } from "./members.js";
|
|
24
25
|
export type { ResolvedMember } from "./members.js";
|
|
26
|
+
export { readSelect } from "./select.js";
|
|
27
|
+
export type { ResolvedOption } from "./select.js";
|
|
25
28
|
export type { AppFixture } from "./mock.js";
|
|
26
29
|
export type { AppWorkflows, AppQueries } from "./types.js";
|
|
27
|
-
export { row } from "./row.js";
|
|
30
|
+
export { row, readLinks } from "./row.js";
|
|
31
|
+
export type { ResolvedLink } from "./row.js";
|
|
28
32
|
export { useOptimistic } from "./use_optimistic.js";
|
|
29
33
|
export type { OptimisticApi } from "./use_optimistic.js";
|
package/dist/src/index.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
export { mount } from "./mount.js";
|
|
18
18
|
export { useWorkflow, useQuery, useFileUpload } from "./hooks.js";
|
|
19
19
|
export { rpc } from "./rpc.js";
|
|
20
|
+
export { openExternal } from "./open_external.js";
|
|
20
21
|
export { readMembers } from "./members.js";
|
|
21
|
-
export {
|
|
22
|
+
export { readSelect } from "./select.js";
|
|
23
|
+
export { row, readLinks } from "./row.js";
|
|
22
24
|
export { useOptimistic } from "./use_optimistic.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open an external URL in a new tab.
|
|
3
|
+
*
|
|
4
|
+
* The embedded app iframe is sandboxed without `allow-popups`, so a direct
|
|
5
|
+
* `window.open` from app code is silently dropped. This routes the open to
|
|
6
|
+
* whoever can actually perform it: the un-sandboxed host frame (embedded) or
|
|
7
|
+
* the app's own top-level page (public). The URL is scheme-validated
|
|
8
|
+
* (`http`/`https` only) at the point it opens — a non-string or disallowed
|
|
9
|
+
* scheme rejects.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* import { openExternal } from "@lotics/app-sdk";
|
|
13
|
+
* await openExternal(invoiceUrl);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function openExternal(url: string): Promise<void>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { rpc } from "./rpc.js";
|
|
2
|
+
/**
|
|
3
|
+
* Open an external URL in a new tab.
|
|
4
|
+
*
|
|
5
|
+
* The embedded app iframe is sandboxed without `allow-popups`, so a direct
|
|
6
|
+
* `window.open` from app code is silently dropped. This routes the open to
|
|
7
|
+
* whoever can actually perform it: the un-sandboxed host frame (embedded) or
|
|
8
|
+
* the app's own top-level page (public). The URL is scheme-validated
|
|
9
|
+
* (`http`/`https` only) at the point it opens — a non-string or disallowed
|
|
10
|
+
* scheme rejects.
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* import { openExternal } from "@lotics/app-sdk";
|
|
14
|
+
* await openExternal(invoiceUrl);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function openExternal(url) {
|
|
18
|
+
return rpc("openExternal", { url });
|
|
19
|
+
}
|
package/dist/src/row.d.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Accessors that coerce raw `useQuery` row values into typed values. Each cell
|
|
3
|
-
* of a query row is `unknown`: the query layer serializes a select column as
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
3
|
+
* of a query row is `unknown`: the query layer serializes a select column as a
|
|
4
|
+
* `{ key, label }` array (one entry per selected option), a date/datetime as a
|
|
5
|
+
* `YYYY-MM-DD[THH:mm…]` string, a number/boolean as itself.
|
|
6
6
|
*
|
|
7
7
|
* These live in app-sdk — next to `useQuery`, whose serialization contract they
|
|
8
8
|
* decode — so a change to that contract updates them in one place, and so apps
|
|
9
9
|
* stop re-implementing the same coercions. They are pure (`unknown` in, value
|
|
10
10
|
* out) and never throw.
|
|
11
11
|
*/
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Select → first option key. Reads the enriched `{ key, label }` shape (and the
|
|
14
|
+
* legacy bare id / single-element array / `{ id }` shapes for back-compat).
|
|
15
|
+
* Use `readSelect` for the full `{ key, label }[]` on multi-select cells.
|
|
16
|
+
*/
|
|
13
17
|
declare function opt(v: unknown): string | null;
|
|
14
18
|
/** Text/markdown/autonumber → string (numbers stringified; everything else ""). */
|
|
15
19
|
declare function text(v: unknown): string;
|
|
@@ -24,11 +28,28 @@ declare function bool(v: unknown): boolean;
|
|
|
24
28
|
* fields are not handled here — they have no consumer yet.)
|
|
25
29
|
*/
|
|
26
30
|
declare function date(v: unknown): Date | null;
|
|
31
|
+
/** A linked record cell entry — the target record's id + its display text. */
|
|
32
|
+
export interface ResolvedLink {
|
|
33
|
+
/** The linked record's id (e.g. "rec_…"). Use to correlate/filter. */
|
|
34
|
+
id: string;
|
|
35
|
+
/** The linked record's display text (its primary-field value). Use to render. */
|
|
36
|
+
display: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* select_record_link → the FIRST linked record as `{ id, display }` (or null).
|
|
40
|
+
* The common single-link case: read `.display` to render the linked record's
|
|
41
|
+
* name, `.id` to correlate/filter (e.g. group rows by a parent record). Use
|
|
42
|
+
* `readLinks` for the full list on multi-link fields.
|
|
43
|
+
*/
|
|
44
|
+
declare function link(v: unknown): ResolvedLink | null;
|
|
45
|
+
/** select_record_link → ALL linked records as `{ id, display }[]` (empty if none). */
|
|
46
|
+
export declare function readLinks(v: unknown): ResolvedLink[];
|
|
27
47
|
export declare const row: {
|
|
28
48
|
opt: typeof opt;
|
|
29
49
|
text: typeof text;
|
|
30
50
|
num: typeof num;
|
|
31
51
|
bool: typeof bool;
|
|
32
52
|
date: typeof date;
|
|
53
|
+
link: typeof link;
|
|
33
54
|
};
|
|
34
55
|
export {};
|
package/dist/src/row.js
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Accessors that coerce raw `useQuery` row values into typed values. Each cell
|
|
3
|
-
* of a query row is `unknown`: the query layer serializes a select column as
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
3
|
+
* of a query row is `unknown`: the query layer serializes a select column as a
|
|
4
|
+
* `{ key, label }` array (one entry per selected option), a date/datetime as a
|
|
5
|
+
* `YYYY-MM-DD[THH:mm…]` string, a number/boolean as itself.
|
|
6
6
|
*
|
|
7
7
|
* These live in app-sdk — next to `useQuery`, whose serialization contract they
|
|
8
8
|
* decode — so a change to that contract updates them in one place, and so apps
|
|
9
9
|
* stop re-implementing the same coercions. They are pure (`unknown` in, value
|
|
10
10
|
* out) and never throw.
|
|
11
11
|
*/
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Select → first option key. Reads the enriched `{ key, label }` shape (and the
|
|
14
|
+
* legacy bare id / single-element array / `{ id }` shapes for back-compat).
|
|
15
|
+
* Use `readSelect` for the full `{ key, label }[]` on multi-select cells.
|
|
16
|
+
*/
|
|
13
17
|
function opt(v) {
|
|
14
18
|
if (typeof v === "string")
|
|
15
19
|
return v || null;
|
|
16
20
|
if (Array.isArray(v))
|
|
17
21
|
return v.length ? opt(v[0]) : null;
|
|
18
22
|
if (v && typeof v === "object") {
|
|
23
|
+
const key = v.key;
|
|
24
|
+
if (typeof key === "string")
|
|
25
|
+
return key || null;
|
|
19
26
|
const id = v.id;
|
|
20
|
-
return typeof id === "string" ? id : null;
|
|
27
|
+
return typeof id === "string" ? id || null : null;
|
|
21
28
|
}
|
|
22
29
|
return null;
|
|
23
30
|
}
|
|
@@ -53,4 +60,33 @@ function date(v) {
|
|
|
53
60
|
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(text(v));
|
|
54
61
|
return m ? new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : null;
|
|
55
62
|
}
|
|
56
|
-
|
|
63
|
+
/** One `{ id, display }` object → ResolvedLink, or null if absent/malformed. */
|
|
64
|
+
function asLink(v) {
|
|
65
|
+
if (!v || typeof v !== "object")
|
|
66
|
+
return null;
|
|
67
|
+
const id = v.id;
|
|
68
|
+
if (typeof id !== "string" || !id)
|
|
69
|
+
return null;
|
|
70
|
+
const display = v.display;
|
|
71
|
+
return { id, display: typeof display === "string" ? display : "" };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* select_record_link → the FIRST linked record as `{ id, display }` (or null).
|
|
75
|
+
* The common single-link case: read `.display` to render the linked record's
|
|
76
|
+
* name, `.id` to correlate/filter (e.g. group rows by a parent record). Use
|
|
77
|
+
* `readLinks` for the full list on multi-link fields.
|
|
78
|
+
*/
|
|
79
|
+
function link(v) {
|
|
80
|
+
if (Array.isArray(v))
|
|
81
|
+
return v.length ? asLink(v[0]) : null;
|
|
82
|
+
return asLink(v);
|
|
83
|
+
}
|
|
84
|
+
/** select_record_link → ALL linked records as `{ id, display }[]` (empty if none). */
|
|
85
|
+
export function readLinks(v) {
|
|
86
|
+
if (!Array.isArray(v)) {
|
|
87
|
+
const one = asLink(v);
|
|
88
|
+
return one ? [one] : [];
|
|
89
|
+
}
|
|
90
|
+
return v.map(asLink).filter((x) => x !== null);
|
|
91
|
+
}
|
|
92
|
+
export const row = { opt, text, num, bool, date, link };
|
package/dist/src/rpc.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* app → host: { id, op, payload }
|
|
19
19
|
* host → app: { id, type: "result", data } | { id, type: "error", message }
|
|
20
20
|
*/
|
|
21
|
-
export type RpcOp = "query" | "workflow" | "upload" | "context";
|
|
21
|
+
export type RpcOp = "query" | "workflow" | "upload" | "context" | "openExternal";
|
|
22
22
|
/**
|
|
23
23
|
* The app's identity, resolved once at startup to tag PostHog events.
|
|
24
24
|
* Assembled by whichever transport is active:
|
package/dist/src/rpc.js
CHANGED
|
@@ -219,8 +219,31 @@ function rpcStandalone(op, payload) {
|
|
|
219
219
|
return standaloneUpload(payload.file);
|
|
220
220
|
case "context":
|
|
221
221
|
return standaloneContext();
|
|
222
|
+
case "openExternal":
|
|
223
|
+
return standaloneOpenExternal(payload);
|
|
222
224
|
}
|
|
223
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Open an external URL in a new tab, scheme-validated. In standalone mode the
|
|
228
|
+
* app is a normal top-level page (`<slug>.lotics.app`), so `window.open` is not
|
|
229
|
+
* sandbox-blocked — open directly. (Bridged apps route this op to the host,
|
|
230
|
+
* which opens it in the un-sandboxed parent frame; see `app_iframe_host`.)
|
|
231
|
+
* The scheme is re-validated wherever the open actually happens — never trust a
|
|
232
|
+
* URL handed across the bridge.
|
|
233
|
+
*/
|
|
234
|
+
function openValidatedUrl(url) {
|
|
235
|
+
if (typeof url !== "string")
|
|
236
|
+
throw new Error("openExternal requires a url string");
|
|
237
|
+
const parsed = new URL(url);
|
|
238
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
239
|
+
throw new Error(`openExternal: unsupported URL scheme "${parsed.protocol}"`);
|
|
240
|
+
}
|
|
241
|
+
window.open(parsed.href, "_blank", "noopener,noreferrer");
|
|
242
|
+
}
|
|
243
|
+
// `async` so a synchronous validation throw surfaces as a rejected Promise.
|
|
244
|
+
async function standaloneOpenExternal(p) {
|
|
245
|
+
openValidatedUrl(p.url);
|
|
246
|
+
}
|
|
224
247
|
async function standaloneContext() {
|
|
225
248
|
const info = await resolveAppInfo();
|
|
226
249
|
return {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for `select` cells in `useQuery` rows.
|
|
3
|
+
*
|
|
4
|
+
* The server (`backend/lib/select_option_resolver.ts`) rewrites every
|
|
5
|
+
* `select` column from its storage shape (`string[]` of bare `opt_*` keys)
|
|
6
|
+
* into `ResolvedOption[]` before the row reaches the app. Apps used to
|
|
7
|
+
* hardcode an `opt_* → label` map because the SDK didn't expose the resolved
|
|
8
|
+
* shape; this helper makes the right shape the obvious one.
|
|
9
|
+
*
|
|
10
|
+
* If the wire format changes, the resolver and this reader move together.
|
|
11
|
+
*/
|
|
12
|
+
export interface ResolvedOption {
|
|
13
|
+
key: string;
|
|
14
|
+
/** Option display name. Falls back to the key when the option was deleted
|
|
15
|
+
* after the cell was written — surfaces the stale state explicitly. */
|
|
16
|
+
label: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse a `useQuery` cell value into `ResolvedOption[]`. Returns `[]` for
|
|
20
|
+
* null/undefined/empty cells and for any unexpected shape — callers iterate
|
|
21
|
+
* uniformly without null-checks. A bare-string entry (a column whose source
|
|
22
|
+
* options couldn't be resolved server-side) becomes `{ key, label: key }` so
|
|
23
|
+
* single-value reads still work. Entries that fail the shape check are
|
|
24
|
+
* dropped silently rather than corrupting the array with partial data.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readSelect(value: unknown): ResolvedOption[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for `select` cells in `useQuery` rows.
|
|
3
|
+
*
|
|
4
|
+
* The server (`backend/lib/select_option_resolver.ts`) rewrites every
|
|
5
|
+
* `select` column from its storage shape (`string[]` of bare `opt_*` keys)
|
|
6
|
+
* into `ResolvedOption[]` before the row reaches the app. Apps used to
|
|
7
|
+
* hardcode an `opt_* → label` map because the SDK didn't expose the resolved
|
|
8
|
+
* shape; this helper makes the right shape the obvious one.
|
|
9
|
+
*
|
|
10
|
+
* If the wire format changes, the resolver and this reader move together.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Parse a `useQuery` cell value into `ResolvedOption[]`. Returns `[]` for
|
|
14
|
+
* null/undefined/empty cells and for any unexpected shape — callers iterate
|
|
15
|
+
* uniformly without null-checks. A bare-string entry (a column whose source
|
|
16
|
+
* options couldn't be resolved server-side) becomes `{ key, label: key }` so
|
|
17
|
+
* single-value reads still work. Entries that fail the shape check are
|
|
18
|
+
* dropped silently rather than corrupting the array with partial data.
|
|
19
|
+
*/
|
|
20
|
+
export function readSelect(value) {
|
|
21
|
+
if (!Array.isArray(value))
|
|
22
|
+
return [];
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const entry of value) {
|
|
25
|
+
if (typeof entry === "string") {
|
|
26
|
+
if (entry !== "")
|
|
27
|
+
out.push({ key: entry, label: entry });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (!entry || typeof entry !== "object")
|
|
31
|
+
continue;
|
|
32
|
+
const obj = entry;
|
|
33
|
+
const key = obj.key;
|
|
34
|
+
if (typeof key !== "string" || key === "")
|
|
35
|
+
continue;
|
|
36
|
+
const label = typeof obj.label === "string" ? obj.label : key;
|
|
37
|
+
out.push({ key, label });
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|