@lotics/app-sdk 0.29.0 → 0.31.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.
@@ -69,3 +69,29 @@ export interface UseCommentsArgs {
69
69
  * ```
70
70
  */
71
71
  export declare function useComments(args: UseCommentsArgs): CommentsState;
72
+ export interface CommentCountsState {
73
+ /** `{ record_id: count }`. Empty while loading or unavailable. */
74
+ counts: Record<string, number>;
75
+ /** True on the initial load only — false during background revalidation. */
76
+ loading: boolean;
77
+ error: string | null;
78
+ /** Same gate as `useComments` — member signed in AND the app declared `comments`. */
79
+ available: boolean;
80
+ /** Re-pull the counts (e.g. after posting a comment). */
81
+ refetch: () => void;
82
+ }
83
+ export interface UseCommentCountsArgs {
84
+ /** The table whose per-record comment counts to read. */
85
+ table_id: string;
86
+ }
87
+ /**
88
+ * Per-record comment counts for a whole table, as `{ record_id: count }` — for
89
+ * row badges (a count beside each row) without loading any comment content. One
90
+ * server-side `GROUP BY`. App-authority like {@link useComments}.
91
+ *
92
+ * ```tsx
93
+ * const { counts } = useCommentCounts({ table_id });
94
+ * // counts[row.__source_record_id] ?? 0
95
+ * ```
96
+ */
97
+ export declare function useCommentCounts(args: UseCommentCountsArgs): CommentCountsState;
@@ -1,20 +1,22 @@
1
1
  /**
2
2
  * `useComments` — read and write the comments on a record from a custom-code app.
3
3
  *
4
- * Comments are a *members-only* surface and behave differently from `useQuery`
5
- * / `useWorkflow`. Those run under the app OWNER's authority (the app is the
6
- * capability), which is what lets a public app expose owner-curated data to an
7
- * anonymous visitor. Comments are member-to-member discussion tied to a real
8
- * author identity, so they run under the VIEWING MEMBER's own authority instead:
4
+ * Comments are a *members-only* surface but, like `useQuery` / `useWorkflow`,
5
+ * run under the app's authority — NOT the member's table IAM. An app user with
6
+ * the app open (and the app declaring `comments`) may read/write comments on any
7
+ * record the app reaches, regardless of their own table access:
9
8
  *
10
- * - read / write authorized against the viewer's own table access + row-scope
11
- * - the comment's author is the viewer (correct attribution)
9
+ * - read / write gated by `app:use` + the `comments` capability (no table grant)
10
+ * - the comment's author is still the real viewing member (correct attribution)
12
11
  * - edit / delete are author-only, checked against the viewer
13
12
  *
14
- * The bridged host (`app_iframe_host`) holds the member session and proxies each
15
- * op to the member's `/v1/records/.../comments` endpoints. A standalone (public)
16
- * app has no signed-in member: `available` is false, the list stays empty, and a
17
- * mutation rejects. Check `available` before rendering a composer.
13
+ * `useCommentCounts` returns per-record counts for a table (row badges) a
14
+ * single server-side `GROUP BY`, no comment content shipped.
15
+ *
16
+ * The bridged host (`app_iframe_host`) proxies each op to the app-scoped
17
+ * `/v1/apps/{id}/…/comments` endpoints. A standalone (public) app has no
18
+ * signed-in member: `available` is false, lists stay empty, and a mutation
19
+ * rejects. Check `available` before rendering a composer.
18
20
  *
19
21
  * Freshness mirrors `useQuery`: SWR-cached, revalidate on focus / reconnect, and
20
22
  * an explicit refetch after every mutation. There is no realtime push to apps,
@@ -150,3 +152,29 @@ export function useComments(args) {
150
152
  refetch,
151
153
  };
152
154
  }
155
+ /**
156
+ * Per-record comment counts for a whole table, as `{ record_id: count }` — for
157
+ * row badges (a count beside each row) without loading any comment content. One
158
+ * server-side `GROUP BY`. App-authority like {@link useComments}.
159
+ *
160
+ * ```tsx
161
+ * const { counts } = useCommentCounts({ table_id });
162
+ * // counts[row.__source_record_id] ?? 0
163
+ * ```
164
+ */
165
+ export function useCommentCounts(args) {
166
+ const { table_id } = args;
167
+ const { memberId, commentsEnabled, resolved } = useAppContext();
168
+ const available = memberId != null && commentsEnabled;
169
+ const swr = useSWR(available ? ["app-comment-counts", table_id] : null, () => rpc("comments.counts", { table_id }), { shouldRetryOnError: false });
170
+ const refetch = useCallback(() => {
171
+ void swr.mutate();
172
+ }, [swr]);
173
+ return {
174
+ counts: swr.data ?? {},
175
+ loading: available ? swr.isLoading : !resolved,
176
+ error: swr.error ? swr.error.message : null,
177
+ available,
178
+ refetch,
179
+ };
180
+ }
@@ -1,4 +1,4 @@
1
- import type { AppWorkflows, AppQueries } from "./types.js";
1
+ import type { AppWorkflows, AppWorkflowResults, AppQueries } from "./types.js";
2
2
  import type { ResolvedMember } from "./members.js";
3
3
  /** Fields shared by every query hook's return value. */
4
4
  interface QueryStateBase {
@@ -151,17 +151,22 @@ export interface PaginatedQueryOptions extends BaseQueryOptions {
151
151
  */
152
152
  export declare function useWorkflow<K extends keyof AppWorkflows & string>(alias: K): UseWorkflowFn<K>;
153
153
  export declare function useWorkflow(alias: string): (inputs?: Record<string, unknown>) => Promise<WorkflowResult>;
154
- 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>;
154
+ type ResultDataOf<K extends string> = K extends keyof AppWorkflowResults ? AppWorkflowResults[K] : unknown;
155
+ 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<ResultDataOf<K>>> : (inputs: AppWorkflows[K]) => Promise<WorkflowResult<ResultDataOf<K>>> : (inputs?: Record<string, unknown>) => Promise<WorkflowResult<ResultDataOf<K>>>;
155
156
  /**
156
157
  * Result of an app-workflow run — the execute endpoint's response. `files` holds
157
158
  * any document a workflow step generated (e.g. via a `generate_*_from_template`
158
159
  * tool), resolved for download: read `files[0].url` and pass it to `openExternal`.
159
160
  * A workflow that generates no file resolves with `files` absent.
161
+ *
162
+ * `data` is the structured value the workflow returned via `return({ data })`,
163
+ * typed per the alias's declared `outputs` schema (`unknown` when none was declared).
160
164
  */
161
- export interface WorkflowResult {
165
+ export interface WorkflowResult<TData = unknown> {
162
166
  status: "success" | "error";
163
167
  message?: string;
164
168
  files?: UploadedFile[];
169
+ data?: TData;
165
170
  }
166
171
  type QueryArgs<K extends keyof AppQueries & string, O> = AppQueries[K] extends Record<string, never> ? [params?: Record<string, never>, opts?: O] : [params: AppQueries[K], opts?: O];
167
172
  /**
@@ -18,8 +18,8 @@ export { mount } from "./mount.js";
18
18
  export type { MountOptions } from "./mount.js";
19
19
  export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useFileUpload, useMembers, } from "./hooks.js";
20
20
  export type { UploadedFile, BaseQueryOptions, QueryOptions, InfiniteQueryOptions, PaginatedQueryOptions, QuerySortKey, QueryFilter, QueryFilterCondition, QueryFilterGroup, WorkflowResult, MembersOptions, } from "./hooks.js";
21
- export { useComments } from "./comments.js";
22
- export type { AppComment, AppCommentFile, CommentsState, UseCommentsArgs } from "./comments.js";
21
+ export { useComments, useCommentCounts } from "./comments.js";
22
+ export type { AppComment, AppCommentFile, CommentsState, UseCommentsArgs, CommentCountsState, UseCommentCountsArgs, } from "./comments.js";
23
23
  export { useViewer } from "./viewer.js";
24
24
  export { requestGeofencedLocation, isWithinZone } from "./geolocation.js";
25
25
  export type { GeofenceZone, GeoCoords, GeofenceOutcome, GeofenceOptions } from "./geolocation.js";
package/dist/src/index.js CHANGED
@@ -16,7 +16,7 @@
16
16
  */
17
17
  export { mount } from "./mount.js";
18
18
  export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useFileUpload, useMembers, } from "./hooks.js";
19
- export { useComments } from "./comments.js";
19
+ export { useComments, useCommentCounts } from "./comments.js";
20
20
  export { useViewer } from "./viewer.js";
21
21
  export { requestGeofencedLocation, isWithinZone } from "./geolocation.js";
22
22
  export { rpc } from "./rpc.js";
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" | "members" | "context" | "openExternal" | "comments.list" | "comments.create" | "comments.update" | "comments.delete";
21
+ export type RpcOp = "query" | "workflow" | "upload" | "members" | "context" | "openExternal" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
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
@@ -227,6 +227,7 @@ function rpcStandalone(op, payload) {
227
227
  case "comments.create":
228
228
  case "comments.update":
229
229
  case "comments.delete":
230
+ case "comments.counts":
230
231
  return rejectCommentsStandalone();
231
232
  }
232
233
  }
@@ -23,6 +23,27 @@
23
23
  */
24
24
  export interface AppWorkflows {
25
25
  }
26
+ /**
27
+ * App-specific workflow-RESULT augmentation point. Same pattern as AppWorkflows,
28
+ * but maps each alias to the type of the `data` its workflow returns via
29
+ * `return({ data })` — derived from the alias's declared `outputs` schema.
30
+ *
31
+ * The base SDK ships it empty, so `useWorkflow(alias)` resolves `result.data` as
32
+ * `unknown`. Per-app codegen augments it for aliases that declare `outputs`:
33
+ *
34
+ * ```ts
35
+ * // .lotics/app_workflows.d.ts (generated)
36
+ * declare module "@lotics/app-sdk" {
37
+ * interface AppWorkflowResults {
38
+ * "computeQuote": { total: number; lines: ReadonlyArray<{ name: string; amount: number }> };
39
+ * }
40
+ * }
41
+ * ```
42
+ *
43
+ * An alias absent from this map (no declared `outputs`) gets `result.data: unknown`.
44
+ */
45
+ export interface AppWorkflowResults {
46
+ }
26
47
  /**
27
48
  * App-specific named-query augmentation point. Same pattern as AppWorkflows.
28
49
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "Runtime SDK for Lotics custom-code apps — typed hooks, postMessage bridge, mount entry point",
5
5
  "type": "module",
6
6
  "exports": {