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.
- package/README.md +213 -51
- package/dist/react/index.d.ts +23 -1
- package/dist/react/index.js +11 -0
- package/dist/react/useAgentInputSchema.d.ts +19 -0
- package/dist/react/useAgentInputSchema.js +73 -0
- package/dist/react/useAgentRun.js +18 -20
- package/dist/react/useAgentRuns.d.ts +33 -0
- package/dist/react/useAgentRuns.js +149 -0
- package/dist/react/useAssistantRun.js +10 -9
- package/dist/react/useAssistantSession.js +21 -25
- package/dist/react/useBulkRecords.js +9 -16
- package/dist/react/useConversation.js +24 -8
- package/dist/react/useConversations.d.ts +4 -0
- package/dist/react/useConversations.js +49 -3
- package/dist/react/useCreateRecord.d.ts +33 -3
- package/dist/react/useCreateRecord.js +20 -17
- package/dist/react/useCurrentUser.d.ts +14 -0
- package/dist/react/useCurrentUser.js +68 -0
- package/dist/react/useDeleteRecord.js +9 -16
- package/dist/react/useFlowRunHistory.js +1 -5
- package/dist/react/useFlowSession.js +41 -33
- package/dist/react/useForeignKeyOptions.d.ts +18 -0
- package/dist/react/useForeignKeyOptions.js +26 -15
- package/dist/react/useFunctionRun.d.ts +36 -0
- package/dist/react/useFunctionRun.js +30 -0
- package/dist/react/useFunctionRuns.d.ts +33 -0
- package/dist/react/useFunctionRuns.js +149 -0
- package/dist/react/useFunctionSession.js +37 -29
- package/dist/react/useJoinedRecords.d.ts +57 -2
- package/dist/react/useJoinedRecords.js +77 -27
- package/dist/react/useMembers.d.ts +4 -0
- package/dist/react/useMembers.js +55 -16
- package/dist/react/useOrganizationMembers.d.ts +26 -0
- package/dist/react/useOrganizationMembers.js +113 -0
- package/dist/react/usePodAccess.d.ts +22 -0
- package/dist/react/usePodAccess.js +128 -0
- package/dist/react/useRecord.d.ts +16 -0
- package/dist/react/useRecord.js +24 -13
- package/dist/react/useRecordForm.d.ts +42 -3
- package/dist/react/useRecordForm.js +44 -24
- package/dist/react/useRecords.d.ts +2 -0
- package/dist/react/useRecords.js +62 -22
- package/dist/react/useReferencingRecords.d.ts +66 -0
- package/dist/react/useReferencingRecords.js +159 -0
- package/dist/react/useRelatedRecords.d.ts +17 -0
- package/dist/react/useRelatedRecords.js +28 -21
- package/dist/react/useReverseRelatedRecords.d.ts +21 -0
- package/dist/react/useReverseRelatedRecords.js +30 -21
- package/dist/react/useSchemaForm.js +1 -13
- package/dist/react/useTable.js +24 -13
- package/dist/react/useTables.d.ts +4 -0
- package/dist/react/useTables.js +57 -15
- package/dist/react/useTaskSession.js +11 -22
- package/dist/react/useUpdateRecord.d.ts +34 -3
- package/dist/react/useUpdateRecord.js +21 -17
- package/dist/react/useWorkflowResume.d.ts +18 -0
- package/dist/react/useWorkflowResume.js +45 -0
- package/dist/react/useWorkflowRun.d.ts +21 -0
- package/dist/react/useWorkflowRun.js +49 -0
- package/dist/react/useWorkflowRuns.d.ts +33 -0
- package/dist/react/useWorkflowRuns.js +149 -0
- package/dist/react/useWorkflowStart.js +20 -27
- package/dist/react/utils.d.ts +5 -0
- package/dist/react/utils.js +25 -0
- package/dist/types.d.ts +1 -0
- package/package.json +2 -4
- package/dist/react/components/AssistantChrome.d.ts +0 -86
- package/dist/react/components/AssistantChrome.js +0 -48
- package/dist/react/components/AssistantEmbedded.d.ts +0 -10
- package/dist/react/components/AssistantEmbedded.js +0 -15
- package/dist/react/components/AssistantExperience.d.ts +0 -96
- package/dist/react/components/AssistantExperience.js +0 -1294
- package/dist/react/components/assistant-types.d.ts +0 -80
- package/dist/react/components/assistant-types.js +0 -1
package/dist/react/useRecords.js
CHANGED
|
@@ -1,27 +1,11 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({});
|