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
@@ -1,27 +1,11 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
- function resolvePodClient(client, podId) {
3
- if (!podId || podId === client.podId)
4
- return client;
5
- return client.withPod(podId);
6
- }
7
- function normalizeError(error, fallback) {
8
- if (error instanceof Error)
9
- return error;
10
- return new Error(fallback);
11
- }
12
- function stringifyComparable(value) {
13
- try {
14
- return JSON.stringify(value);
15
- }
16
- catch {
17
- return String(value);
18
- }
19
- }
2
+ import { normalizeError, resolvePodClient, stringifyComparable } from "./utils.js";
20
3
  export function useRecords({ client, podId, tableName, filters, sort, limit = 20, pageToken, offset, sortBy, order, params, enabled = true, autoLoad = true, }) {
21
4
  const [records, setRecords] = useState([]);
22
5
  const [total, setTotal] = useState(0);
23
6
  const [nextPageToken, setNextPageToken] = useState(null);
24
7
  const [isLoading, setIsLoading] = useState(false);
8
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
25
9
  const [error, setError] = useState(null);
26
10
  const trimmedTableName = tableName.trim();
27
11
  const isEnabled = enabled && trimmedTableName.length > 0;
@@ -31,7 +15,7 @@ export function useRecords({ client, podId, tableName, filters, sort, limit = 20
31
15
  const stableFilters = useMemo(() => filters, [filtersKey]);
32
16
  const stableSort = useMemo(() => sort, [sortKey]);
33
17
  const stableParams = useMemo(() => params, [paramsKey]);
34
- const refresh = useCallback(async (overrides = {}) => {
18
+ const refresh = useCallback(async (overrides = {}, signal) => {
35
19
  if (!isEnabled) {
36
20
  setRecords([]);
37
21
  setTotal(0);
@@ -54,6 +38,8 @@ export function useRecords({ client, podId, tableName, filters, sort, limit = 20
54
38
  order: overrides.order ?? order,
55
39
  params: overrides.params ?? stableParams,
56
40
  });
41
+ if (signal?.aborted)
42
+ return [];
57
43
  const nextRecords = (response.items ?? []);
58
44
  setRecords(nextRecords);
59
45
  setTotal(response.total ?? nextRecords.length);
@@ -61,12 +47,15 @@ export function useRecords({ client, podId, tableName, filters, sort, limit = 20
61
47
  return nextRecords;
62
48
  }
63
49
  catch (refreshError) {
50
+ if (signal?.aborted)
51
+ return [];
64
52
  const normalized = normalizeError(refreshError, "Failed to load records.");
65
53
  setError(normalized);
66
54
  return [];
67
55
  }
68
56
  finally {
69
- setIsLoading(false);
57
+ if (!signal?.aborted)
58
+ setIsLoading(false);
70
59
  }
71
60
  }, [
72
61
  client,
@@ -82,6 +71,39 @@ export function useRecords({ client, podId, tableName, filters, sort, limit = 20
82
71
  stableSort,
83
72
  trimmedTableName,
84
73
  ]);
74
+ const loadMore = useCallback(async (overrides = {}) => {
75
+ if (!isEnabled || !nextPageToken || isLoading || isLoadingMore) {
76
+ return [];
77
+ }
78
+ setIsLoadingMore(true);
79
+ setError(null);
80
+ try {
81
+ const scopedClient = resolvePodClient(client, podId);
82
+ const response = await scopedClient.records.list(trimmedTableName, {
83
+ filters: stableFilters,
84
+ sort: stableSort,
85
+ limit: overrides.limit ?? limit,
86
+ pageToken: nextPageToken,
87
+ offset: overrides.offset,
88
+ sortBy: overrides.sortBy ?? sortBy,
89
+ order: overrides.order ?? order,
90
+ params: overrides.params ?? stableParams,
91
+ });
92
+ const moreRecords = (response.items ?? []);
93
+ setRecords((previous) => [...previous, ...moreRecords]);
94
+ setTotal(response.total ?? records.length + moreRecords.length);
95
+ setNextPageToken(response.next_page_token ?? null);
96
+ return moreRecords;
97
+ }
98
+ catch (loadError) {
99
+ const normalized = normalizeError(loadError, "Failed to load more records.");
100
+ setError(normalized);
101
+ return [];
102
+ }
103
+ finally {
104
+ setIsLoadingMore(false);
105
+ }
106
+ }, [client, isEnabled, isLoading, isLoadingMore, limit, nextPageToken, offset, order, podId, records.length, sortBy, stableFilters, stableParams, stableSort, trimmedTableName]);
85
107
  useEffect(() => {
86
108
  if (!isEnabled) {
87
109
  setRecords([]);
@@ -89,18 +111,36 @@ export function useRecords({ client, podId, tableName, filters, sort, limit = 20
89
111
  setNextPageToken(null);
90
112
  setError(null);
91
113
  setIsLoading(false);
114
+ setIsLoadingMore(false);
92
115
  return;
93
116
  }
94
117
  if (!autoLoad)
95
118
  return;
96
- void refresh();
119
+ const controller = new AbortController();
120
+ let cancelled = false;
121
+ (async () => {
122
+ try {
123
+ await refresh({}, controller.signal);
124
+ }
125
+ catch {
126
+ if (!cancelled) {
127
+ setError(normalizeError(new Error("Failed to load records."), "Failed to load records."));
128
+ }
129
+ }
130
+ })();
131
+ return () => {
132
+ cancelled = true;
133
+ controller.abort();
134
+ };
97
135
  }, [autoLoad, isEnabled, refresh]);
98
136
  return useMemo(() => ({
99
137
  records,
100
138
  total,
101
139
  nextPageToken,
102
140
  isLoading,
141
+ isLoadingMore,
103
142
  error,
104
143
  refresh,
105
- }), [error, isLoading, nextPageToken, records, refresh, total]);
144
+ loadMore,
145
+ }), [error, isLoading, isLoadingMore, loadMore, nextPageToken, records, refresh, total]);
106
146
  }
@@ -0,0 +1,66 @@
1
+ import type { LemmaClient } from "../client.js";
2
+ import type { Table } from "../types.js";
3
+ /**
4
+ * React hook for fetching records from a referencing table that point back
5
+ * to a specific record via a foreign key.
6
+ *
7
+ * Unlike `useReverseRelatedRecords` which starts from a parent table and
8
+ * discovers what references it, this hook starts from the child table and
9
+ * says "give me all rows in this table where this FK column equals this ID."
10
+ *
11
+ * @example Fetch all comments for an issue
12
+ * ```tsx
13
+ * const { records, isLoading } = useReferencingRecords({
14
+ * client,
15
+ * table: "comments",
16
+ * foreignKey: "issue_id",
17
+ * recordId: "issue_123",
18
+ * });
19
+ * ```
20
+ *
21
+ * @example Fetch history entries
22
+ * ```tsx
23
+ * const { records, columns, isLoading } = useReferencingRecords({
24
+ * client,
25
+ * table: "issue_history",
26
+ * foreignKey: "issue_id",
27
+ * recordId: selectedIssueId,
28
+ * sortBy: "created_at",
29
+ * order: "desc",
30
+ * });
31
+ * ```
32
+ */
33
+ export interface ReferencingRecordsColumn {
34
+ key: string;
35
+ field: string;
36
+ label: string;
37
+ }
38
+ export interface UseReferencingRecordsOptions {
39
+ client: LemmaClient;
40
+ podId?: string;
41
+ /** The referencing (child) table, e.g. "comments". */
42
+ table: string;
43
+ /** The foreign-key column in the referencing table, e.g. "issue_id". */
44
+ foreignKey: string;
45
+ /** The record ID value to match against the foreign-key column. */
46
+ recordId?: string | null;
47
+ /** Fields to select. Auto-resolved from the table schema if omitted. */
48
+ fields?: string[];
49
+ limit?: number;
50
+ offset?: number;
51
+ sortBy?: string;
52
+ order?: "asc" | "desc" | string;
53
+ enabled?: boolean;
54
+ autoLoad?: boolean;
55
+ }
56
+ export interface UseReferencingRecordsResult<TRow extends Record<string, unknown> = Record<string, unknown>> {
57
+ referencedTable: Table | null;
58
+ columns: ReferencingRecordsColumn[];
59
+ records: TRow[];
60
+ total: number;
61
+ nextPageToken: string | null;
62
+ isLoading: boolean;
63
+ error: Error | null;
64
+ refresh: () => Promise<TRow[]>;
65
+ }
66
+ export declare function useReferencingRecords<TRow extends Record<string, unknown> = Record<string, unknown>>({ client, podId, table, foreignKey, recordId, fields, limit, offset, sortBy, order, enabled, autoLoad, }: UseReferencingRecordsOptions): UseReferencingRecordsResult<TRow>;
@@ -0,0 +1,159 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { normalizeError, resolvePodClient, stringifyComparable } from "./utils.js";
3
+ function sentenceCase(value) {
4
+ return value
5
+ .replace(/[_\.]/g, " ")
6
+ .replace(/\s+/g, " ")
7
+ .trim()
8
+ .replace(/\b\w/g, (match) => match.toUpperCase());
9
+ }
10
+ function pickDefaultFields(table, foreignKey) {
11
+ const names = table.columns
12
+ .map((column) => column.name)
13
+ .filter((name) => name !== "created_at" && name !== "updated_at");
14
+ const prioritized = ["id", "name", "title", "label", "status", foreignKey];
15
+ const next = [];
16
+ prioritized.forEach((name) => {
17
+ if (names.includes(name) && !next.includes(name)) {
18
+ next.push(name);
19
+ }
20
+ });
21
+ names.forEach((name) => {
22
+ if (!next.includes(name)) {
23
+ next.push(name);
24
+ }
25
+ });
26
+ return next.slice(0, 6);
27
+ }
28
+ export function useReferencingRecords({ client, podId, table, foreignKey, recordId = null, fields, limit = 20, offset, sortBy, order, enabled = true, autoLoad = true, }) {
29
+ const [referencedTable, setReferencedTable] = useState(null);
30
+ const [columns, setColumns] = useState([]);
31
+ const [records, setRecords] = useState([]);
32
+ const [total, setTotal] = useState(0);
33
+ const [nextPageToken, setNextPageToken] = useState(null);
34
+ const [isLoading, setIsLoading] = useState(false);
35
+ const [error, setError] = useState(null);
36
+ const trimmedTable = table.trim();
37
+ const trimmedRecordId = typeof recordId === "string" ? recordId.trim() : "";
38
+ const fieldsKey = stringifyComparable(fields);
39
+ const stableFields = useMemo(() => fields, [fieldsKey]);
40
+ const isEnabled = enabled && trimmedTable.length > 0 && trimmedRecordId.length > 0;
41
+ const refresh = useCallback(async (signal) => {
42
+ if (!isEnabled) {
43
+ setReferencedTable(null);
44
+ setColumns([]);
45
+ setRecords([]);
46
+ setTotal(0);
47
+ setNextPageToken(null);
48
+ setError(null);
49
+ setIsLoading(false);
50
+ return [];
51
+ }
52
+ setIsLoading(true);
53
+ setError(null);
54
+ try {
55
+ const scopedClient = resolvePodClient(client, podId);
56
+ const tableResponse = await scopedClient.tables.get(trimmedTable);
57
+ if (signal?.aborted)
58
+ return [];
59
+ setReferencedTable(tableResponse);
60
+ const resolvedFields = (stableFields?.length ? stableFields : pickDefaultFields(tableResponse, foreignKey))
61
+ .filter((field, index, allFields) => field.trim().length > 0 && allFields.indexOf(field) === index);
62
+ const response = await scopedClient.records.list(trimmedTable, {
63
+ filters: [{
64
+ field: foreignKey,
65
+ op: "eq",
66
+ value: trimmedRecordId,
67
+ }],
68
+ limit,
69
+ offset,
70
+ sortBy,
71
+ order,
72
+ });
73
+ if (signal?.aborted)
74
+ return [];
75
+ const nextRecords = (response.items ?? []);
76
+ setColumns(resolvedFields.map((field) => ({
77
+ key: field,
78
+ field,
79
+ label: sentenceCase(field),
80
+ })));
81
+ setRecords(nextRecords);
82
+ setTotal(response.total ?? nextRecords.length);
83
+ setNextPageToken(response.next_page_token ?? null);
84
+ return nextRecords;
85
+ }
86
+ catch (refreshError) {
87
+ if (signal?.aborted)
88
+ return [];
89
+ const normalized = normalizeError(refreshError, "Failed to load referencing records.");
90
+ setError(normalized);
91
+ return [];
92
+ }
93
+ finally {
94
+ if (!signal?.aborted)
95
+ setIsLoading(false);
96
+ }
97
+ }, [
98
+ client,
99
+ foreignKey,
100
+ isEnabled,
101
+ limit,
102
+ offset,
103
+ order,
104
+ podId,
105
+ sortBy,
106
+ stableFields,
107
+ trimmedRecordId,
108
+ trimmedTable,
109
+ ]);
110
+ useEffect(() => {
111
+ if (!isEnabled) {
112
+ setReferencedTable(null);
113
+ setColumns([]);
114
+ setRecords([]);
115
+ setTotal(0);
116
+ setNextPageToken(null);
117
+ setError(null);
118
+ setIsLoading(false);
119
+ return;
120
+ }
121
+ if (!autoLoad)
122
+ return;
123
+ const controller = new AbortController();
124
+ let cancelled = false;
125
+ (async () => {
126
+ try {
127
+ await refresh(controller.signal);
128
+ }
129
+ catch {
130
+ if (!cancelled) {
131
+ setError(normalizeError(new Error("Failed to load referencing records."), "Failed to load referencing records."));
132
+ }
133
+ }
134
+ })();
135
+ return () => {
136
+ cancelled = true;
137
+ controller.abort();
138
+ };
139
+ }, [autoLoad, isEnabled, refresh]);
140
+ return useMemo(() => ({
141
+ referencedTable,
142
+ columns,
143
+ records,
144
+ total,
145
+ nextPageToken,
146
+ isLoading,
147
+ error,
148
+ refresh,
149
+ }), [
150
+ columns,
151
+ error,
152
+ isLoading,
153
+ nextPageToken,
154
+ records,
155
+ refresh,
156
+ referencedTable,
157
+ total,
158
+ ]);
159
+ }
@@ -1,5 +1,22 @@
1
1
  import type { LemmaClient } from "../client.js";
2
2
  import type { Table } from "../types.js";
3
+ /**
4
+ * React hook for fetching base-table rows with their FK-related data
5
+ * joined in a single query. You specify which FK columns to include and
6
+ * the hook auto-resolves the referenced table and join columns.
7
+ *
8
+ * The result has nested objects: `{ id, name, team: { id, name } }`.
9
+ *
10
+ * @example Issues with their team
11
+ * ```tsx
12
+ * const { records, isLoading } = useRelatedRecords({
13
+ * client,
14
+ * tableName: "issues",
15
+ * include: [{ foreignKey: "team_id" }],
16
+ * });
17
+ * // records[0] = { id: "1", title: "Bug", team: { id: "t1", name: "Eng" } }
18
+ * ```
19
+ */
3
20
  export interface RelatedRecordsInclude {
4
21
  foreignKey: string;
5
22
  as?: string;
@@ -1,23 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import { buildJoinedRecordsQuery, parseForeignKeyReference } from "../datastore-query.js";
3
- function resolvePodClient(client, podId) {
4
- if (!podId || podId === client.podId)
5
- return client;
6
- return client.withPod(podId);
7
- }
8
- function normalizeError(error, fallback) {
9
- if (error instanceof Error)
10
- return error;
11
- return new Error(fallback);
12
- }
13
- function stringifyComparable(value) {
14
- try {
15
- return JSON.stringify(value);
16
- }
17
- catch {
18
- return String(value);
19
- }
20
- }
3
+ import { normalizeError, resolvePodClient, stringifyComparable } from "./utils.js";
21
4
  function sentenceCase(value) {
22
5
  return value
23
6
  .replace(/[_\.]/g, " ")
@@ -82,7 +65,7 @@ export function useRelatedRecords({ client, podId, tableName, baseFields, includ
82
65
  const stableInclude = useMemo(() => include, [includeKey]);
83
66
  const stableBaseFields = useMemo(() => baseFields, [baseFieldsKey]);
84
67
  const isEnabled = enabled && trimmedTableName.length > 0 && stableInclude.length > 0;
85
- const refresh = useCallback(async () => {
68
+ const refresh = useCallback(async (signal) => {
86
69
  if (!isEnabled) {
87
70
  setRecords([]);
88
71
  setColumns([]);
@@ -98,6 +81,8 @@ export function useRelatedRecords({ client, podId, tableName, baseFields, includ
98
81
  try {
99
82
  const scopedClient = resolvePodClient(client, podId);
100
83
  const nextBaseTable = await scopedClient.tables.get(trimmedTableName);
84
+ if (signal?.aborted)
85
+ return [];
101
86
  if (nextBaseTable.enable_rls) {
102
87
  throw new Error(`Related record queries are not supported for RLS-enabled table "${trimmedTableName}". Use table-scoped record APIs instead.`);
103
88
  }
@@ -105,6 +90,8 @@ export function useRelatedRecords({ client, podId, tableName, baseFields, includ
105
90
  const resolvedBaseFields = (stableBaseFields?.length ? stableBaseFields : pickDefaultBaseFields(nextBaseTable))
106
91
  .filter((field, index, allFields) => field.trim().length > 0 && allFields.indexOf(field) === index);
107
92
  const resolvedIncludes = await Promise.all(stableInclude.map(async (entry, index) => {
93
+ if (signal?.aborted)
94
+ throw new Error("Aborted");
108
95
  const baseColumn = nextBaseTable.columns.find((column) => column.name === entry.foreignKey) ?? null;
109
96
  const reference = baseColumn?.foreign_key?.references
110
97
  ? parseForeignKeyReference(baseColumn.foreign_key.references)
@@ -163,6 +150,8 @@ export function useRelatedRecords({ client, podId, tableName, baseFields, includ
163
150
  setSql(nextSql);
164
151
  setIncludes(resolvedIncludes.map(({ alias: _alias, ...rest }) => rest));
165
152
  const response = await scopedClient.datastore.query(nextSql);
153
+ if (signal?.aborted)
154
+ return [];
166
155
  const nextColumns = [
167
156
  ...resolvedBaseFields.map((field) => ({
168
157
  key: field,
@@ -196,12 +185,15 @@ export function useRelatedRecords({ client, podId, tableName, baseFields, includ
196
185
  return nextRecords;
197
186
  }
198
187
  catch (refreshError) {
188
+ if (signal?.aborted)
189
+ return [];
199
190
  const normalized = normalizeError(refreshError, "Failed to load related records.");
200
191
  setError(normalized);
201
192
  return [];
202
193
  }
203
194
  finally {
204
- setIsLoading(false);
195
+ if (!signal?.aborted)
196
+ setIsLoading(false);
205
197
  }
206
198
  }, [client, isEnabled, limit, offset, podId, stableBaseFields, stableInclude, trimmedTableName]);
207
199
  useEffect(() => {
@@ -217,7 +209,22 @@ export function useRelatedRecords({ client, podId, tableName, baseFields, includ
217
209
  }
218
210
  if (!autoLoad)
219
211
  return;
220
- void refresh();
212
+ const controller = new AbortController();
213
+ let cancelled = false;
214
+ (async () => {
215
+ try {
216
+ await refresh(controller.signal);
217
+ }
218
+ catch {
219
+ if (!cancelled) {
220
+ setError(normalizeError(new Error("Failed to load related records."), "Failed to load related records."));
221
+ }
222
+ }
223
+ })();
224
+ return () => {
225
+ cancelled = true;
226
+ controller.abort();
227
+ };
221
228
  }, [autoLoad, isEnabled, refresh]);
222
229
  return useMemo(() => ({
223
230
  records,
@@ -1,5 +1,26 @@
1
1
  import type { LemmaClient } from "../client.js";
2
2
  import type { Table } from "../types.js";
3
+ /**
4
+ * React hook for finding records in *other* tables that reference a given
5
+ * record. Starts from the parent table, discovers all tables with FK
6
+ * columns pointing back to it, and fetches the referencing rows.
7
+ *
8
+ * For the simpler case where you already know the referencing table and
9
+ * FK column, prefer `useReferencingRecords` — it has a more intuitive API.
10
+ *
11
+ * @example All comments and history entries for an issue
12
+ * ```tsx
13
+ * const { relations, records, isLoading } = useReverseRelatedRecords({
14
+ * client,
15
+ * tableName: "issues",
16
+ * recordId: "issue_123",
17
+ * relation: { tableName: "comments", foreignKey: "issue_id" },
18
+ * });
19
+ * ```
20
+ *
21
+ * @see useReferencingRecords — flipped-perspective alias for the common
22
+ * "show me all rows in table X where FK = Y" pattern.
23
+ */
3
24
  export interface ReverseRelationSelector {
4
25
  tableName: string;
5
26
  foreignKey: string;
@@ -1,23 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import { parseForeignKeyReference } from "../datastore-query.js";
3
- function resolvePodClient(client, podId) {
4
- if (!podId || podId === client.podId)
5
- return client;
6
- return client.withPod(podId);
7
- }
8
- function normalizeError(error, fallback) {
9
- if (error instanceof Error)
10
- return error;
11
- return new Error(fallback);
12
- }
13
- function stringifyComparable(value) {
14
- try {
15
- return JSON.stringify(value);
16
- }
17
- catch {
18
- return String(value);
19
- }
20
- }
3
+ import { normalizeError, resolvePodClient, stringifyComparable } from "./utils.js";
21
4
  function sentenceCase(value) {
22
5
  return value
23
6
  .replace(/[_\.]/g, " ")
@@ -62,7 +45,7 @@ export function useReverseRelatedRecords({ client, podId, tableName, recordId =
62
45
  const stableRelation = useMemo(() => relation, [relationKey]);
63
46
  const stableFields = useMemo(() => fields, [fieldsKey]);
64
47
  const isEnabled = enabled && trimmedTableName.length > 0 && trimmedRecordId.length > 0;
65
- const refresh = useCallback(async () => {
48
+ const refresh = useCallback(async (signal) => {
66
49
  if (!isEnabled) {
67
50
  setParentTable(null);
68
51
  setRelatedTable(null);
@@ -85,10 +68,14 @@ export function useReverseRelatedRecords({ client, podId, tableName, recordId =
85
68
  scopedClient.tables.list({ limit: tablesLimit }),
86
69
  scopedClient.records.get(trimmedTableName, trimmedRecordId),
87
70
  ]);
71
+ if (signal?.aborted)
72
+ return [];
88
73
  const listedTables = tablesResponse.items ?? [];
89
74
  const nextParentTable = listedTables.find((tableEntry) => tableEntry.name === trimmedTableName)
90
75
  ?? await scopedClient.tables.get(trimmedTableName);
91
76
  const nextParentRecord = parentRecordResponse.data ?? null;
77
+ if (signal?.aborted)
78
+ return [];
92
79
  setParentTable(nextParentTable);
93
80
  setParentRecord(nextParentRecord);
94
81
  const nextRelations = listedTables.flatMap((candidateTable) => candidateTable.columns.flatMap((column) => {
@@ -123,6 +110,8 @@ export function useReverseRelatedRecords({ client, podId, tableName, recordId =
123
110
  ?? await scopedClient.tables.get(nextSelectedRelation.tableName);
124
111
  const referenceValue = nextParentRecord?.[nextSelectedRelation.referencedColumn]
125
112
  ?? (nextSelectedRelation.referencedColumn === "id" ? trimmedRecordId : undefined);
113
+ if (signal?.aborted)
114
+ return [];
126
115
  setRelatedTable(nextRelatedTable);
127
116
  if (typeof referenceValue === "undefined" || referenceValue === null) {
128
117
  setColumns([]);
@@ -144,6 +133,8 @@ export function useReverseRelatedRecords({ client, podId, tableName, recordId =
144
133
  sortBy,
145
134
  order,
146
135
  });
136
+ if (signal?.aborted)
137
+ return [];
147
138
  const nextRecords = (response.items ?? []);
148
139
  setColumns(resolvedFields.map((field) => ({
149
140
  key: field,
@@ -156,12 +147,15 @@ export function useReverseRelatedRecords({ client, podId, tableName, recordId =
156
147
  return nextRecords;
157
148
  }
158
149
  catch (refreshError) {
150
+ if (signal?.aborted)
151
+ return [];
159
152
  const normalized = normalizeError(refreshError, "Failed to load reverse-related records.");
160
153
  setError(normalized);
161
154
  return [];
162
155
  }
163
156
  finally {
164
- setIsLoading(false);
157
+ if (!signal?.aborted)
158
+ setIsLoading(false);
165
159
  }
166
160
  }, [
167
161
  client,
@@ -194,7 +188,22 @@ export function useReverseRelatedRecords({ client, podId, tableName, recordId =
194
188
  }
195
189
  if (!autoLoad)
196
190
  return;
197
- void refresh();
191
+ const controller = new AbortController();
192
+ let cancelled = false;
193
+ (async () => {
194
+ try {
195
+ await refresh(controller.signal);
196
+ }
197
+ catch {
198
+ if (!cancelled) {
199
+ setError(normalizeError(new Error("Failed to load reverse-related records."), "Failed to load reverse-related records."));
200
+ }
201
+ }
202
+ })();
203
+ return () => {
204
+ cancelled = true;
205
+ controller.abort();
206
+ };
198
207
  }, [autoLoad, isEnabled, refresh]);
199
208
  return useMemo(() => ({
200
209
  parentTable,
@@ -1,18 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import { buildSchemaFormFields, buildSchemaFormPayload, buildSchemaFormValues, } from "../schema-form.js";
3
- function normalizeError(error, fallback) {
4
- if (error instanceof Error)
5
- return error;
6
- return new Error(fallback);
7
- }
8
- function stringifyComparable(value) {
9
- try {
10
- return JSON.stringify(value);
11
- }
12
- catch {
13
- return String(value);
14
- }
15
- }
3
+ import { normalizeError, stringifyComparable } from "./utils.js";
16
4
  const EMPTY_VALUES = {};
17
5
  export function useSchemaForm({ schema = null, uiSchema = null, initialValues = EMPTY_VALUES, enabled = true, onSubmit, onError, }) {
18
6
  const [values, setValuesState] = useState({});