lemma-sdk 0.2.30 → 0.2.32

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.
Files changed (74) hide show
  1. package/README.md +213 -51
  2. package/dist/react/index.d.ts +23 -1
  3. package/dist/react/index.js +11 -0
  4. package/dist/react/useAgentInputSchema.d.ts +19 -0
  5. package/dist/react/useAgentInputSchema.js +73 -0
  6. package/dist/react/useAgentRun.js +18 -20
  7. package/dist/react/useAgentRuns.d.ts +33 -0
  8. package/dist/react/useAgentRuns.js +149 -0
  9. package/dist/react/useAssistantRun.js +10 -9
  10. package/dist/react/useAssistantSession.js +21 -25
  11. package/dist/react/useBulkRecords.js +9 -16
  12. package/dist/react/useConversation.js +24 -8
  13. package/dist/react/useConversations.d.ts +4 -0
  14. package/dist/react/useConversations.js +49 -3
  15. package/dist/react/useCreateRecord.d.ts +33 -3
  16. package/dist/react/useCreateRecord.js +20 -17
  17. package/dist/react/useCurrentUser.d.ts +14 -0
  18. package/dist/react/useCurrentUser.js +68 -0
  19. package/dist/react/useDeleteRecord.js +9 -16
  20. package/dist/react/useFlowRunHistory.js +1 -5
  21. package/dist/react/useFlowSession.js +41 -33
  22. package/dist/react/useForeignKeyOptions.d.ts +18 -0
  23. package/dist/react/useForeignKeyOptions.js +26 -15
  24. package/dist/react/useFunctionRun.d.ts +36 -0
  25. package/dist/react/useFunctionRun.js +30 -0
  26. package/dist/react/useFunctionRuns.d.ts +33 -0
  27. package/dist/react/useFunctionRuns.js +149 -0
  28. package/dist/react/useFunctionSession.js +37 -29
  29. package/dist/react/useJoinedRecords.d.ts +57 -2
  30. package/dist/react/useJoinedRecords.js +77 -27
  31. package/dist/react/useMembers.d.ts +4 -0
  32. package/dist/react/useMembers.js +55 -16
  33. package/dist/react/useOrganizationMembers.d.ts +26 -0
  34. package/dist/react/useOrganizationMembers.js +113 -0
  35. package/dist/react/usePodAccess.d.ts +22 -0
  36. package/dist/react/usePodAccess.js +128 -0
  37. package/dist/react/useRecord.d.ts +16 -0
  38. package/dist/react/useRecord.js +24 -13
  39. package/dist/react/useRecordForm.d.ts +42 -3
  40. package/dist/react/useRecordForm.js +44 -24
  41. package/dist/react/useRecords.d.ts +2 -0
  42. package/dist/react/useRecords.js +62 -22
  43. package/dist/react/useReferencingRecords.d.ts +66 -0
  44. package/dist/react/useReferencingRecords.js +159 -0
  45. package/dist/react/useRelatedRecords.d.ts +17 -0
  46. package/dist/react/useRelatedRecords.js +28 -21
  47. package/dist/react/useReverseRelatedRecords.d.ts +21 -0
  48. package/dist/react/useReverseRelatedRecords.js +30 -21
  49. package/dist/react/useSchemaForm.js +1 -13
  50. package/dist/react/useTable.js +24 -13
  51. package/dist/react/useTables.d.ts +4 -0
  52. package/dist/react/useTables.js +57 -15
  53. package/dist/react/useTaskSession.js +11 -22
  54. package/dist/react/useUpdateRecord.d.ts +34 -3
  55. package/dist/react/useUpdateRecord.js +21 -17
  56. package/dist/react/useWorkflowResume.d.ts +18 -0
  57. package/dist/react/useWorkflowResume.js +45 -0
  58. package/dist/react/useWorkflowRun.d.ts +21 -0
  59. package/dist/react/useWorkflowRun.js +49 -0
  60. package/dist/react/useWorkflowRuns.d.ts +33 -0
  61. package/dist/react/useWorkflowRuns.js +149 -0
  62. package/dist/react/useWorkflowStart.js +20 -27
  63. package/dist/react/utils.d.ts +5 -0
  64. package/dist/react/utils.js +25 -0
  65. package/dist/types.d.ts +1 -0
  66. package/package.json +2 -4
  67. package/dist/react/components/AssistantChrome.d.ts +0 -86
  68. package/dist/react/components/AssistantChrome.js +0 -48
  69. package/dist/react/components/AssistantEmbedded.d.ts +0 -10
  70. package/dist/react/components/AssistantEmbedded.js +0 -15
  71. package/dist/react/components/AssistantExperience.d.ts +0 -96
  72. package/dist/react/components/AssistantExperience.js +0 -1294
  73. package/dist/react/components/assistant-types.d.ts +0 -80
  74. package/dist/react/components/assistant-types.js +0 -1
@@ -0,0 +1,149 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { normalizeError, resolvePodClient } from "./utils.js";
3
+ export function useFunctionRuns({ client, podId, functionName, enabled = true, autoLoad = true, limit = 100, pageToken, initialRunId = null, }) {
4
+ const [runs, setRuns] = useState([]);
5
+ const [total, setTotal] = useState(0);
6
+ const [nextPageToken, setNextPageToken] = useState(null);
7
+ const [selectedRunId, setSelectedRunId] = useState(initialRunId);
8
+ const [isLoading, setIsLoading] = useState(false);
9
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
10
+ const [error, setError] = useState(null);
11
+ const trimmedFunctionName = functionName.trim();
12
+ const isEnabled = enabled && trimmedFunctionName.length > 0;
13
+ const refresh = useCallback(async (overrides = {}, signal) => {
14
+ if (!isEnabled) {
15
+ setRuns([]);
16
+ setTotal(0);
17
+ setNextPageToken(null);
18
+ setError(null);
19
+ setIsLoading(false);
20
+ return [];
21
+ }
22
+ setIsLoading(true);
23
+ setError(null);
24
+ try {
25
+ const scopedClient = resolvePodClient(client, podId);
26
+ const response = await scopedClient.functions.runs.list(trimmedFunctionName, {
27
+ limit: overrides.limit ?? limit,
28
+ pageToken: overrides.pageToken ?? pageToken,
29
+ });
30
+ if (signal?.aborted)
31
+ return [];
32
+ const nextRuns = response.items ?? [];
33
+ setRuns(nextRuns);
34
+ setTotal(response.total ?? nextRuns.length);
35
+ setNextPageToken(response.next_page_token ?? null);
36
+ setSelectedRunId((current) => (current && nextRuns.some((run) => run.id === current) ? current : initialRunId));
37
+ return nextRuns;
38
+ }
39
+ catch (refreshError) {
40
+ if (signal?.aborted)
41
+ return [];
42
+ const normalized = normalizeError(refreshError, "Failed to load function runs.");
43
+ setError(normalized);
44
+ return [];
45
+ }
46
+ finally {
47
+ if (!signal?.aborted)
48
+ setIsLoading(false);
49
+ }
50
+ }, [client, initialRunId, isEnabled, limit, pageToken, podId, trimmedFunctionName]);
51
+ const loadMore = useCallback(async (overrides = {}) => {
52
+ if (!isEnabled || !nextPageToken || isLoading || isLoadingMore) {
53
+ return [];
54
+ }
55
+ setIsLoadingMore(true);
56
+ setError(null);
57
+ try {
58
+ const scopedClient = resolvePodClient(client, podId);
59
+ const response = await scopedClient.functions.runs.list(trimmedFunctionName, {
60
+ limit: overrides.limit ?? limit,
61
+ pageToken: nextPageToken,
62
+ });
63
+ const moreRuns = response.items ?? [];
64
+ setRuns((previous) => [...previous, ...moreRuns]);
65
+ setTotal(response.total ?? runs.length + moreRuns.length);
66
+ setNextPageToken(response.next_page_token ?? null);
67
+ return moreRuns;
68
+ }
69
+ catch (loadError) {
70
+ const normalized = normalizeError(loadError, "Failed to load more function runs.");
71
+ setError(normalized);
72
+ return [];
73
+ }
74
+ finally {
75
+ setIsLoadingMore(false);
76
+ }
77
+ }, [client, isEnabled, isLoading, isLoadingMore, limit, nextPageToken, podId, trimmedFunctionName]);
78
+ useEffect(() => {
79
+ setSelectedRunId(initialRunId);
80
+ }, [initialRunId]);
81
+ useEffect(() => {
82
+ if (!isEnabled) {
83
+ setRuns([]);
84
+ setTotal(0);
85
+ setNextPageToken(null);
86
+ setError(null);
87
+ setIsLoading(false);
88
+ setIsLoadingMore(false);
89
+ return;
90
+ }
91
+ if (!autoLoad)
92
+ return;
93
+ const controller = new AbortController();
94
+ let cancelled = false;
95
+ (async () => {
96
+ try {
97
+ await refresh({}, controller.signal);
98
+ }
99
+ catch {
100
+ if (!cancelled) {
101
+ setError(normalizeError(new Error("Failed to load function runs."), "Failed to load function runs."));
102
+ }
103
+ }
104
+ })();
105
+ return () => {
106
+ cancelled = true;
107
+ controller.abort();
108
+ };
109
+ }, [autoLoad, isEnabled, refresh]);
110
+ const selectRun = useCallback((runId) => {
111
+ setSelectedRunId(runId);
112
+ }, []);
113
+ const clearSelection = useCallback(() => {
114
+ setSelectedRunId(null);
115
+ }, []);
116
+ return useMemo(() => {
117
+ const effectiveSelectedRunId = selectedRunId ?? runs[0]?.id ?? null;
118
+ const selectedRun = effectiveSelectedRunId
119
+ ? runs.find((run) => run.id === effectiveSelectedRunId) ?? null
120
+ : null;
121
+ return {
122
+ runs,
123
+ total,
124
+ nextPageToken,
125
+ selectedRunId,
126
+ effectiveSelectedRunId,
127
+ selectedRun,
128
+ isLoading,
129
+ isLoadingMore,
130
+ error,
131
+ selectRun,
132
+ clearSelection,
133
+ refresh,
134
+ loadMore,
135
+ };
136
+ }, [
137
+ clearSelection,
138
+ error,
139
+ isLoading,
140
+ isLoadingMore,
141
+ loadMore,
142
+ nextPageToken,
143
+ refresh,
144
+ runs,
145
+ selectRun,
146
+ selectedRunId,
147
+ total,
148
+ ]);
149
+ }
@@ -1,12 +1,6 @@
1
- import { useCallback, useEffect, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { isTerminalFunctionStatus, normalizeRunStatus, sleep } from "../run-utils.js";
3
- function resolvePodId(client, podId) {
4
- const resolved = podId ?? client.podId;
5
- if (!resolved) {
6
- throw new Error("podId is required. Pass podId or set it on LemmaClient.");
7
- }
8
- return resolved;
9
- }
3
+ import { normalizeError, resolvePodClient, resolvePodId } from "./utils.js";
10
4
  function resolveFunctionName(base, override) {
11
5
  const resolved = override ?? base;
12
6
  if (!resolved) {
@@ -14,17 +8,16 @@ function resolveFunctionName(base, override) {
14
8
  }
15
9
  return resolved;
16
10
  }
17
- function normalizeError(error, fallback) {
18
- if (error instanceof Error)
19
- return error;
20
- return new Error(fallback);
21
- }
22
11
  export function useFunctionSession({ client, podId, functionName, runId: externalRunId = null, autoPoll = true, pollIntervalMs = 2000, onRun, onError, }) {
23
12
  const [runId, setRunIdState] = useState(externalRunId);
24
13
  const [run, setRun] = useState(null);
25
14
  const [status, setStatus] = useState(undefined);
26
15
  const [isPolling, setIsPolling] = useState(false);
27
16
  const [error, setError] = useState(null);
17
+ const onRunRef = useRef(onRun);
18
+ const onErrorRef = useRef(onError);
19
+ useEffect(() => { onRunRef.current = onRun; }, [onRun]);
20
+ useEffect(() => { onErrorRef.current = onError; }, [onError]);
28
21
  const setRunId = useCallback((nextRunId) => {
29
22
  setRunIdState(nextRunId);
30
23
  if (!nextRunId) {
@@ -44,27 +37,27 @@ export function useFunctionSession({ client, podId, functionName, runId: externa
44
37
  if (!id)
45
38
  return null;
46
39
  try {
47
- client.setPodId(resolvePodId(client, podId));
40
+ const scopedClient = resolvePodClient(client, resolvePodId(client, podId));
48
41
  const name = resolveFunctionName(functionName);
49
- const nextRun = await client.functions.runs.get(name, id);
42
+ const nextRun = await scopedClient.functions.runs.get(name, id);
50
43
  setRun(nextRun);
51
44
  const nextStatus = normalizeRunStatus(nextRun.status);
52
45
  setStatus(nextStatus);
53
- onRun?.(nextRun);
46
+ onRunRef.current?.(nextRun);
54
47
  return nextRun;
55
48
  }
56
49
  catch (refreshError) {
57
50
  const normalized = normalizeError(refreshError, "Failed to fetch function run.");
58
51
  setError(normalized);
59
- onError?.(refreshError);
52
+ onErrorRef.current?.(refreshError);
60
53
  return null;
61
54
  }
62
- }, [client, functionName, onError, onRun, podId, runId]);
55
+ }, [client, functionName, podId, runId]);
63
56
  const listHistory = useCallback(async (options = {}) => {
64
57
  try {
65
- client.setPodId(resolvePodId(client, podId));
58
+ const scopedClient = resolvePodClient(client, resolvePodId(client, podId));
66
59
  const name = resolveFunctionName(functionName, options.functionName);
67
- const response = await client.functions.runs.list(name, {
60
+ const response = await scopedClient.functions.runs.list(name, {
68
61
  limit: options.limit,
69
62
  pageToken: options.pageToken,
70
63
  });
@@ -73,32 +66,47 @@ export function useFunctionSession({ client, podId, functionName, runId: externa
73
66
  catch (listError) {
74
67
  const normalized = normalizeError(listError, "Failed to list function runs.");
75
68
  setError(normalized);
76
- onError?.(listError);
69
+ onErrorRef.current?.(listError);
77
70
  return [];
78
71
  }
79
- }, [client, functionName, onError, podId]);
72
+ }, [client, functionName, podId]);
80
73
  const start = useCallback(async (options = {}) => {
81
74
  setError(null);
82
- client.setPodId(resolvePodId(client, podId));
75
+ const scopedClient = resolvePodClient(client, resolvePodId(client, podId));
83
76
  const name = resolveFunctionName(functionName, options.functionName);
84
- const created = await client.functions.runs.create(name, {
77
+ const created = await scopedClient.functions.runs.create(name, {
85
78
  input: options.input,
86
79
  });
87
80
  setRun(created);
88
81
  setRunIdState(created.id);
89
82
  const nextStatus = normalizeRunStatus(created.status);
90
83
  setStatus(nextStatus);
91
- onRun?.(created);
84
+ onRunRef.current?.(created);
92
85
  if (options.connect !== false) {
93
86
  await refresh(created.id);
94
87
  }
95
88
  return created;
96
- }, [client, functionName, onRun, podId, refresh]);
89
+ }, [client, functionName, podId, refresh]);
97
90
  useEffect(() => {
98
91
  if (!runId) {
99
92
  return;
100
93
  }
101
- void refresh(runId);
94
+ const controller = new AbortController();
95
+ let cancelled = false;
96
+ (async () => {
97
+ try {
98
+ await refresh(runId);
99
+ }
100
+ catch {
101
+ if (!cancelled) {
102
+ // refresh handles errors internally
103
+ }
104
+ }
105
+ })();
106
+ return () => {
107
+ cancelled = true;
108
+ controller.abort();
109
+ };
102
110
  }, [refresh, runId]);
103
111
  useEffect(() => {
104
112
  if (!autoPoll || !runId) {
@@ -132,14 +140,14 @@ export function useFunctionSession({ client, podId, functionName, runId: externa
132
140
  void loop().catch((pollError) => {
133
141
  const normalized = normalizeError(pollError, "Failed while polling function run.");
134
142
  setError(normalized);
135
- onError?.(pollError);
143
+ onErrorRef.current?.(pollError);
136
144
  setIsPolling(false);
137
145
  });
138
146
  return () => {
139
147
  active = false;
140
148
  abortController.abort();
141
149
  };
142
- }, [autoPoll, onError, pollIntervalMs, refresh, runId]);
150
+ }, [autoPoll, pollIntervalMs, refresh, runId]);
143
151
  return {
144
152
  runId,
145
153
  run,
@@ -1,9 +1,64 @@
1
1
  import type { LemmaClient } from "../client.js";
2
2
  import { type JoinedRecordsQueryDefinition } from "../datastore-query.js";
3
+ /**
4
+ * A simplified join descriptor. The hook auto-resolves the join condition
5
+ * from the foreign-key metadata on the base table's column named by `on`.
6
+ */
7
+ export interface JoinedRecordsShorthandJoin {
8
+ /** The table to join into (e.g. "teams"). */
9
+ table: string;
10
+ /**
11
+ * The FK column on the base table that references the join table
12
+ * (e.g. "team_id"). The hook reads the FK metadata on this column
13
+ * to determine the join condition automatically.
14
+ */
15
+ on: string;
16
+ /** Optional alias for the joined table. Defaults to the table name. */
17
+ alias?: string;
18
+ /** Join type. Defaults to "left". */
19
+ type?: "inner" | "left" | "left outer" | "right" | "right outer" | "full" | "full outer";
20
+ }
21
+ /**
22
+ * React hook for cross-table join queries. Supports two API styles:
23
+ *
24
+ * 1. **Full query** via the `query` option — for complex joins with custom
25
+ * select lists, filters, and expressions.
26
+ *
27
+ * 2. **Shorthand** via `baseTable` + `joins` — the hook auto-resolves the
28
+ * join conditions from the schema's foreign-key metadata, so you don't
29
+ * need to spell out the `on` condition manually.
30
+ *
31
+ * @example Full query (existing API)
32
+ * ```tsx
33
+ * const { records, isLoading } = useJoinedRecords({
34
+ * client,
35
+ * query: {
36
+ * from: "issues",
37
+ * joins: [{ table: "teams", on: { left: "issues.team_id", right: "teams.id" } }],
38
+ * },
39
+ * });
40
+ * ```
41
+ *
42
+ * @example Shorthand with FK auto-resolution
43
+ * ```tsx
44
+ * const { records, isLoading } = useJoinedRecords({
45
+ * client,
46
+ * baseTable: "issues",
47
+ * joins: [{ table: "teams", on: "team_id" }],
48
+ * });
49
+ * // The hook reads the FK metadata on issues.team_id to find that
50
+ * // it references teams.id, and builds the join automatically.
51
+ * ```
52
+ */
3
53
  export interface UseJoinedRecordsOptions {
4
54
  client: LemmaClient;
5
55
  podId?: string;
6
- query: JoinedRecordsQueryDefinition;
56
+ /** Full join query definition. Mutually exclusive with `baseTable` + `joins`. */
57
+ query?: JoinedRecordsQueryDefinition;
58
+ /** Base table for shorthand mode. Mutually exclusive with `query`. */
59
+ baseTable?: string;
60
+ /** Shorthand join descriptors. Auto-resolves join conditions from FK metadata. */
61
+ joins?: JoinedRecordsShorthandJoin[];
7
62
  enabled?: boolean;
8
63
  autoLoad?: boolean;
9
64
  }
@@ -15,4 +70,4 @@ export interface UseJoinedRecordsResult<TRecord extends Record<string, unknown>
15
70
  error: Error | null;
16
71
  refresh: () => Promise<TRecord[]>;
17
72
  }
18
- export declare function useJoinedRecords<TRecord extends Record<string, unknown> = Record<string, unknown>>({ client, podId, query, enabled, autoLoad, }: UseJoinedRecordsOptions): UseJoinedRecordsResult<TRecord>;
73
+ export declare function useJoinedRecords<TRecord extends Record<string, unknown> = Record<string, unknown>>({ client, podId, query, baseTable, joins: shorthandJoins, enabled, autoLoad, }: UseJoinedRecordsOptions): UseJoinedRecordsResult<TRecord>;
@@ -1,37 +1,51 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
- import { buildJoinedRecordsQuery } from "../datastore-query.js";
3
- function resolvePodId(client, podId) {
4
- const resolved = podId ?? client.podId;
5
- if (!resolved) {
6
- throw new Error("podId is required. Pass podId or set it on LemmaClient.");
7
- }
8
- return resolved;
9
- }
10
- function normalizeError(error, fallback) {
11
- if (error instanceof Error)
12
- return error;
13
- return new Error(fallback);
14
- }
15
- function stringifyComparable(value) {
16
- try {
17
- return JSON.stringify(value);
18
- }
19
- catch {
20
- return String(value);
21
- }
2
+ import { buildJoinedRecordsQuery, parseForeignKeyReference, } from "../datastore-query.js";
3
+ import { normalizeError, resolvePodId, stringifyComparable } from "./utils.js";
4
+ async function buildShorthandQuery(client, podId, baseTableName, shorthandJoins) {
5
+ const resolvedPodId = resolvePodId(client, podId);
6
+ const scopedClient = resolvedPodId === client.podId ? client : client.withPod(resolvedPodId);
7
+ const baseTable = await scopedClient.tables.get(baseTableName.trim());
8
+ const joins = await Promise.all(shorthandJoins.map(async (entry) => {
9
+ const baseColumn = baseTable.columns.find((col) => col.name === entry.on) ?? null;
10
+ const reference = baseColumn?.foreign_key?.references
11
+ ? parseForeignKeyReference(baseColumn.foreign_key.references)
12
+ : null;
13
+ if (!baseColumn) {
14
+ throw new Error(`Column "${entry.on}" was not found on table "${baseTableName}".`);
15
+ }
16
+ if (!reference) {
17
+ throw new Error(`Column "${entry.on}" on "${baseTableName}" is not a foreign key. Use the full query API for non-FK joins.`);
18
+ }
19
+ return {
20
+ type: entry.type ?? "left",
21
+ table: entry.table,
22
+ alias: entry.alias,
23
+ on: {
24
+ left: { table: baseTableName, column: entry.on },
25
+ right: { table: entry.alias ?? entry.table, column: reference.column },
26
+ },
27
+ };
28
+ }));
29
+ return {
30
+ from: { table: baseTableName, alias: baseTableName },
31
+ joins,
32
+ };
22
33
  }
23
- export function useJoinedRecords({ client, podId, query, enabled = true, autoLoad = true, }) {
34
+ export function useJoinedRecords({ client, podId, query, baseTable, joins: shorthandJoins, enabled = true, autoLoad = true, }) {
24
35
  const [records, setRecords] = useState([]);
25
36
  const [total, setTotal] = useState(0);
37
+ const [sql, setSql] = useState("");
26
38
  const [isLoading, setIsLoading] = useState(false);
27
39
  const [error, setError] = useState(null);
40
+ const hasShorthand = !query && !!baseTable && !!shorthandJoins?.length;
28
41
  const queryKey = stringifyComparable(query);
42
+ const shorthandKey = stringifyComparable({ baseTable, shorthandJoins });
29
43
  const stableQuery = useMemo(() => query, [queryKey]);
30
- const sql = useMemo(() => buildJoinedRecordsQuery(stableQuery), [stableQuery]);
31
- const refresh = useCallback(async () => {
44
+ const refresh = useCallback(async (signal) => {
32
45
  if (!enabled) {
33
46
  setRecords([]);
34
47
  setTotal(0);
48
+ setSql("");
35
49
  setError(null);
36
50
  setIsLoading(false);
37
51
  return [];
@@ -41,32 +55,68 @@ export function useJoinedRecords({ client, podId, query, enabled = true, autoLoa
41
55
  try {
42
56
  const resolvedPodId = resolvePodId(client, podId);
43
57
  const scopedClient = resolvedPodId === client.podId ? client : client.withPod(resolvedPodId);
44
- const response = await scopedClient.datastore.query(sql);
58
+ let resolvedQuery = stableQuery;
59
+ if (hasShorthand && baseTable && shorthandJoins) {
60
+ resolvedQuery = await buildShorthandQuery(client, podId, baseTable, shorthandJoins);
61
+ }
62
+ if (!resolvedQuery) {
63
+ setRecords([]);
64
+ setTotal(0);
65
+ setSql("");
66
+ setIsLoading(false);
67
+ return [];
68
+ }
69
+ if (signal?.aborted)
70
+ return [];
71
+ const nextSql = buildJoinedRecordsQuery(resolvedQuery);
72
+ setSql(nextSql);
73
+ const response = await scopedClient.datastore.query(nextSql);
74
+ if (signal?.aborted)
75
+ return [];
45
76
  const nextRecords = (response.items ?? []);
46
77
  setRecords(nextRecords);
47
78
  setTotal(response.total ?? nextRecords.length);
48
79
  return nextRecords;
49
80
  }
50
81
  catch (refreshError) {
82
+ if (signal?.aborted)
83
+ return [];
51
84
  const normalized = normalizeError(refreshError, "Failed to load joined records.");
52
85
  setError(normalized);
53
86
  return [];
54
87
  }
55
88
  finally {
56
- setIsLoading(false);
89
+ if (!signal?.aborted)
90
+ setIsLoading(false);
57
91
  }
58
- }, [client, enabled, podId, sql]);
92
+ }, [client, enabled, hasShorthand, baseTable, podId, shorthandJoins, stableQuery]);
59
93
  useEffect(() => {
60
94
  if (!enabled) {
61
95
  setRecords([]);
62
96
  setTotal(0);
97
+ setSql("");
63
98
  setError(null);
64
99
  setIsLoading(false);
65
100
  return;
66
101
  }
67
102
  if (!autoLoad)
68
103
  return;
69
- void refresh();
104
+ const controller = new AbortController();
105
+ let cancelled = false;
106
+ (async () => {
107
+ try {
108
+ await refresh(controller.signal);
109
+ }
110
+ catch {
111
+ if (!cancelled) {
112
+ setError(normalizeError(new Error("Failed to load joined records."), "Failed to load joined records."));
113
+ }
114
+ }
115
+ })();
116
+ return () => {
117
+ cancelled = true;
118
+ controller.abort();
119
+ };
70
120
  }, [autoLoad, enabled, refresh]);
71
121
  return useMemo(() => ({
72
122
  records,
@@ -13,10 +13,14 @@ export interface UseMembersResult {
13
13
  total: number;
14
14
  nextPageToken: string | null;
15
15
  isLoading: boolean;
16
+ isLoadingMore: boolean;
16
17
  error: Error | null;
17
18
  refresh: (overrides?: {
18
19
  limit?: number;
19
20
  pageToken?: string;
20
21
  }) => Promise<PodMember[]>;
22
+ loadMore: (overrides?: {
23
+ limit?: number;
24
+ }) => Promise<PodMember[]>;
21
25
  }
22
26
  export declare function useMembers({ client, podId, enabled, autoLoad, limit, pageToken, }: UseMembersOptions): UseMembersResult;
@@ -1,23 +1,13 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
- function resolvePodId(client, podId) {
3
- const resolved = podId ?? client.podId;
4
- if (!resolved) {
5
- throw new Error("podId is required. Pass podId or set it on LemmaClient.");
6
- }
7
- return resolved;
8
- }
9
- function normalizeError(error, fallback) {
10
- if (error instanceof Error)
11
- return error;
12
- return new Error(fallback);
13
- }
2
+ import { normalizeError, resolvePodId } from "./utils.js";
14
3
  export function useMembers({ client, podId, enabled = true, autoLoad = true, limit = 100, pageToken, }) {
15
4
  const [members, setMembers] = useState([]);
16
5
  const [total, setTotal] = useState(0);
17
6
  const [nextPageToken, setNextPageToken] = useState(null);
18
7
  const [isLoading, setIsLoading] = useState(false);
8
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
19
9
  const [error, setError] = useState(null);
20
- const refresh = useCallback(async (overrides = {}) => {
10
+ const refresh = useCallback(async (overrides = {}, signal) => {
21
11
  if (!enabled)
22
12
  return [];
23
13
  setIsLoading(true);
@@ -28,6 +18,8 @@ export function useMembers({ client, podId, enabled = true, autoLoad = true, lim
28
18
  limit: overrides.limit ?? limit,
29
19
  pageToken: overrides.pageToken ?? pageToken,
30
20
  });
21
+ if (signal?.aborted)
22
+ return [];
31
23
  const nextMembers = response.items ?? [];
32
24
  setMembers(nextMembers);
33
25
  setTotal(response.total ?? nextMembers.length);
@@ -35,25 +27,72 @@ export function useMembers({ client, podId, enabled = true, autoLoad = true, lim
35
27
  return nextMembers;
36
28
  }
37
29
  catch (refreshError) {
30
+ if (signal?.aborted)
31
+ return [];
38
32
  const normalized = normalizeError(refreshError, "Failed to load pod members.");
39
33
  setError(normalized);
40
34
  return [];
41
35
  }
42
36
  finally {
43
- setIsLoading(false);
37
+ if (!signal?.aborted)
38
+ setIsLoading(false);
44
39
  }
45
40
  }, [client, enabled, limit, pageToken, podId]);
41
+ const loadMore = useCallback(async (overrides = {}) => {
42
+ if (!enabled || !nextPageToken || isLoading || isLoadingMore) {
43
+ return [];
44
+ }
45
+ setIsLoadingMore(true);
46
+ setError(null);
47
+ try {
48
+ const resolvedPodId = resolvePodId(client, podId);
49
+ const response = await client.podMembers.list(resolvedPodId, {
50
+ limit: overrides.limit ?? limit,
51
+ pageToken: nextPageToken,
52
+ });
53
+ const moreMembers = response.items ?? [];
54
+ setMembers((previous) => [...previous, ...moreMembers]);
55
+ setTotal(response.total ?? members.length + moreMembers.length);
56
+ setNextPageToken(response.next_page_token ?? null);
57
+ return moreMembers;
58
+ }
59
+ catch (loadError) {
60
+ const normalized = normalizeError(loadError, "Failed to load more pod members.");
61
+ setError(normalized);
62
+ return [];
63
+ }
64
+ finally {
65
+ setIsLoadingMore(false);
66
+ }
67
+ }, [client, enabled, isLoading, isLoadingMore, limit, members.length, nextPageToken, podId]);
46
68
  useEffect(() => {
47
69
  if (!enabled || !autoLoad)
48
70
  return;
49
- void refresh();
71
+ const controller = new AbortController();
72
+ let cancelled = false;
73
+ (async () => {
74
+ try {
75
+ await refresh({}, controller.signal);
76
+ }
77
+ catch {
78
+ if (!cancelled) {
79
+ setError(normalizeError(new Error("Failed to load pod members."), "Failed to load pod members."));
80
+ }
81
+ }
82
+ })();
83
+ return () => {
84
+ cancelled = true;
85
+ controller.abort();
86
+ };
50
87
  }, [autoLoad, enabled, refresh]);
51
88
  return useMemo(() => ({
52
89
  members,
53
90
  total,
54
91
  nextPageToken,
55
92
  isLoading,
93
+ isLoadingMore,
56
94
  error,
57
95
  refresh,
58
- }), [error, isLoading, members, nextPageToken, refresh, total]);
96
+ loadMore,
97
+ }), [error, isLoading, isLoadingMore, loadMore, members, nextPageToken, refresh, total]);
59
98
  }
@@ -0,0 +1,26 @@
1
+ import type { LemmaClient } from "../client.js";
2
+ import type { OrganizationMember } from "../types.js";
3
+ export interface UseOrganizationMembersOptions {
4
+ client: LemmaClient;
5
+ organizationId: string;
6
+ enabled?: boolean;
7
+ autoLoad?: boolean;
8
+ limit?: number;
9
+ pageToken?: string;
10
+ }
11
+ export interface UseOrganizationMembersResult {
12
+ members: OrganizationMember[];
13
+ total: number;
14
+ nextPageToken: string | null;
15
+ isLoading: boolean;
16
+ isLoadingMore: boolean;
17
+ error: Error | null;
18
+ refresh: (overrides?: {
19
+ limit?: number;
20
+ pageToken?: string;
21
+ }) => Promise<OrganizationMember[]>;
22
+ loadMore: (overrides?: {
23
+ limit?: number;
24
+ }) => Promise<OrganizationMember[]>;
25
+ }
26
+ export declare function useOrganizationMembers({ client, organizationId, enabled, autoLoad, limit, pageToken, }: UseOrganizationMembersOptions): UseOrganizationMembersResult;