@lotics/app-sdk 0.30.0 → 0.32.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
+ }
@@ -157,9 +157,10 @@ type UseWorkflowFn<K extends keyof AppWorkflows & string> = AppWorkflows[K] exte
157
157
  * Result of an app-workflow run — the execute endpoint's response. `files` holds
158
158
  * any document a workflow step generated (e.g. via a `generate_*_from_template`
159
159
  * tool), resolved for download: read `files[0].url` and pass it to `openExternal`.
160
- * A workflow that generates no file resolves with `files` absent. `data` is the
161
- * structured value the workflow returned via `return({ data })`, typed per the
162
- * alias's declared `outputs` schema (`unknown` when none was declared).
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).
163
164
  */
164
165
  export interface WorkflowResult<TData = unknown> {
165
166
  status: "success" | "error";
@@ -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";
@@ -32,7 +32,7 @@ export { readSelect } from "./select.js";
32
32
  export type { ResolvedOption } from "./select.js";
33
33
  export type { AppFixture } from "./mock.js";
34
34
  export type { AppWorkflows, AppQueries } from "./types.js";
35
- export { row, readLinks, readFiles } from "./row.js";
35
+ export { row, readLinks, readFiles, readLocked } from "./row.js";
36
36
  export type { ResolvedLink, AppFile } from "./row.js";
37
37
  export { useOptimistic } from "./use_optimistic.js";
38
38
  export type { OptimisticApi } from "./use_optimistic.js";
package/dist/src/index.js CHANGED
@@ -16,13 +16,13 @@
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";
23
23
  export { openExternal } from "./open_external.js";
24
24
  export { readMembers } from "./members.js";
25
25
  export { readSelect } from "./select.js";
26
- export { row, readLinks, readFiles } from "./row.js";
26
+ export { row, readLinks, readFiles, readLocked } from "./row.js";
27
27
  export { useOptimistic } from "./use_optimistic.js";
28
28
  export { useRecents } from "./use_recents.js";
package/dist/src/row.d.ts CHANGED
@@ -65,6 +65,15 @@ export interface AppFile {
65
65
  * an unservable file. Map to `@lotics/ui` `DisplayFile` for FileThumbnail/Gallery.
66
66
  */
67
67
  export declare function readFiles(v: unknown): AppFile[];
68
+ /**
69
+ * Record lock state for a `useQuery` row. The query layer emits a row-level
70
+ * `__source_locked` addressing column (alongside `__source_record_id`). A locked
71
+ * record rejects direct writes, so an app reads this to show a locked state and
72
+ * route edits through a `request_locked_field_change` workflow instead of an
73
+ * `update_records` save. Pass the whole row (not a cell). False for any
74
+ * non-object / absent flag.
75
+ */
76
+ export declare function readLocked(rowValue: unknown): boolean;
68
77
  export declare const row: {
69
78
  opt: typeof opt;
70
79
  text: typeof text;
package/dist/src/row.js CHANGED
@@ -118,4 +118,17 @@ export function readFiles(v) {
118
118
  }
119
119
  return out;
120
120
  }
121
+ /**
122
+ * Record lock state for a `useQuery` row. The query layer emits a row-level
123
+ * `__source_locked` addressing column (alongside `__source_record_id`). A locked
124
+ * record rejects direct writes, so an app reads this to show a locked state and
125
+ * route edits through a `request_locked_field_change` workflow instead of an
126
+ * `update_records` save. Pass the whole row (not a cell). False for any
127
+ * non-object / absent flag.
128
+ */
129
+ export function readLocked(rowValue) {
130
+ if (!rowValue || typeof rowValue !== "object")
131
+ return false;
132
+ return bool(rowValue["__source_locked"]);
133
+ }
121
134
  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" | "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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.30.0",
3
+ "version": "0.32.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": {
@@ -46,4 +46,4 @@
46
46
  "url": "https://github.com/lotics/lotics.git",
47
47
  "directory": "packages/app-sdk"
48
48
  }
49
- }
49
+ }