lemma-sdk 0.2.28 → 0.2.31
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 +241 -201
- package/bin/lemma-sdk.js +108 -0
- package/dist/browser/lemma-client.js +125 -4
- package/dist/client.d.ts +2 -0
- package/dist/client.js +3 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +2 -2
- package/dist/datastore-query.d.ts +54 -0
- package/dist/datastore-query.js +157 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +3 -0
- package/dist/namespaces/datastore.d.ts +9 -0
- package/dist/namespaces/datastore.js +13 -0
- package/dist/namespaces/records.d.ts +1 -1
- package/dist/openapi_client/index.d.ts +4 -0
- package/dist/openapi_client/index.js +1 -0
- package/dist/openapi_client/models/ConvertedArtifactResponse.d.ts +6 -0
- package/dist/openapi_client/models/ConvertedFileResponse.d.ts +10 -0
- package/dist/openapi_client/models/ConvertedFileResponse.js +1 -0
- package/dist/openapi_client/models/CreateFolderRequest.d.ts +3 -1
- package/dist/openapi_client/models/FlowRunEntity.d.ts +1 -0
- package/dist/openapi_client/models/ScheduledFlowStart.d.ts +3 -6
- package/dist/openapi_client/models/ScheduledFlowStartType.d.ts +4 -0
- package/dist/openapi_client/models/ScheduledFlowStartType.js +9 -0
- package/dist/openapi_client/models/WorkflowInstallRequest.d.ts +5 -0
- package/dist/openapi_client/models/WorkflowTimeInstallConfig.d.ts +19 -0
- package/dist/openapi_client/models/WorkflowTimeInstallConfig.js +1 -0
- package/dist/openapi_client/services/FilesService.d.ts +27 -1
- package/dist/openapi_client/services/FilesService.js +69 -1
- package/dist/openapi_client/services/WorkflowsService.d.ts +1 -1
- package/dist/openapi_client/services/WorkflowsService.js +1 -1
- package/dist/react/assistant-output.d.ts +6 -0
- package/dist/react/assistant-output.js +90 -0
- package/dist/react/index.d.ts +62 -8
- package/dist/react/index.js +31 -4
- package/dist/react/useAgentInputSchema.d.ts +19 -0
- package/dist/react/useAgentInputSchema.js +73 -0
- package/dist/react/useAgentRun.d.ts +17 -0
- package/dist/react/useAgentRun.js +56 -0
- package/dist/react/useAgentRuns.d.ts +33 -0
- package/dist/react/useAgentRuns.js +149 -0
- package/dist/react/useAssistantRun.d.ts +9 -0
- package/dist/react/useAssistantRun.js +28 -17
- package/dist/react/useAssistantSession.d.ts +5 -0
- package/dist/react/useAssistantSession.js +135 -86
- package/dist/react/useBulkRecords.d.ts +20 -0
- package/dist/react/useBulkRecords.js +65 -0
- package/dist/react/useConversation.d.ts +18 -0
- package/dist/react/useConversation.js +75 -0
- package/dist/react/useConversationMessages.d.ts +59 -0
- package/dist/react/useConversationMessages.js +167 -0
- package/dist/react/useConversations.d.ts +52 -0
- package/dist/react/useConversations.js +228 -0
- package/dist/react/useCreateRecord.d.ts +18 -0
- package/dist/react/useCreateRecord.js +51 -0
- package/dist/react/useCurrentUser.d.ts +14 -0
- package/dist/react/useCurrentUser.js +68 -0
- package/dist/react/useDeleteRecord.d.ts +21 -0
- package/dist/react/useDeleteRecord.js +52 -0
- package/dist/react/useFlowRunHistory.js +1 -5
- package/dist/react/useFlowSession.js +41 -33
- package/dist/react/useForeignKeyOptions.d.ts +31 -0
- package/dist/react/useForeignKeyOptions.js +161 -0
- package/dist/react/useFunctionRun.d.ts +19 -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 +18 -0
- package/dist/react/useJoinedRecords.js +80 -0
- package/dist/react/useMembers.d.ts +26 -0
- package/dist/react/useMembers.js +98 -0
- 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 +18 -0
- package/dist/react/useRecord.js +75 -0
- package/dist/react/useRecordForm.d.ts +42 -0
- package/dist/react/useRecordForm.js +221 -0
- package/dist/react/useRecordSchema.d.ts +20 -0
- package/dist/react/useRecordSchema.js +24 -0
- package/dist/react/useRecords.d.ts +20 -0
- package/dist/react/useRecords.js +146 -0
- package/dist/react/useRelatedRecords.d.ts +43 -0
- package/dist/react/useRelatedRecords.js +239 -0
- package/dist/react/useReverseRelatedRecords.d.ts +47 -0
- package/dist/react/useReverseRelatedRecords.js +235 -0
- package/dist/react/useSchemaForm.d.ts +24 -0
- package/dist/react/useSchemaForm.js +104 -0
- package/dist/react/useTable.d.ts +16 -0
- package/dist/react/useTable.js +70 -0
- package/dist/react/useTables.d.ts +26 -0
- package/dist/react/useTables.js +113 -0
- package/dist/react/useTaskSession.js +11 -22
- package/dist/react/useUpdateRecord.d.ts +21 -0
- package/dist/react/useUpdateRecord.js +55 -0
- 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.d.ts +33 -0
- package/dist/react/useWorkflowStart.js +148 -0
- package/dist/react/utils.d.ts +5 -0
- package/dist/react/utils.js +25 -0
- package/dist/record-form.d.ts +30 -0
- package/dist/record-form.js +199 -0
- package/dist/schema-form.d.ts +41 -0
- package/dist/schema-form.js +200 -0
- package/dist/types.d.ts +6 -1
- package/package.json +11 -8
- 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/styles.css +0 -2407
- /package/dist/{react/components/assistant-types.js → openapi_client/models/ConvertedArtifactResponse.js} +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { normalizeError, resolvePodClient, stringifyComparable } from "./utils.js";
|
|
3
|
+
export function useRecords({ client, podId, tableName, filters, sort, limit = 20, pageToken, offset, sortBy, order, params, enabled = true, autoLoad = true, }) {
|
|
4
|
+
const [records, setRecords] = useState([]);
|
|
5
|
+
const [total, setTotal] = useState(0);
|
|
6
|
+
const [nextPageToken, setNextPageToken] = useState(null);
|
|
7
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
8
|
+
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
9
|
+
const [error, setError] = useState(null);
|
|
10
|
+
const trimmedTableName = tableName.trim();
|
|
11
|
+
const isEnabled = enabled && trimmedTableName.length > 0;
|
|
12
|
+
const filtersKey = stringifyComparable(filters);
|
|
13
|
+
const sortKey = stringifyComparable(sort);
|
|
14
|
+
const paramsKey = stringifyComparable(params);
|
|
15
|
+
const stableFilters = useMemo(() => filters, [filtersKey]);
|
|
16
|
+
const stableSort = useMemo(() => sort, [sortKey]);
|
|
17
|
+
const stableParams = useMemo(() => params, [paramsKey]);
|
|
18
|
+
const refresh = useCallback(async (overrides = {}, signal) => {
|
|
19
|
+
if (!isEnabled) {
|
|
20
|
+
setRecords([]);
|
|
21
|
+
setTotal(0);
|
|
22
|
+
setNextPageToken(null);
|
|
23
|
+
setError(null);
|
|
24
|
+
setIsLoading(false);
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
setIsLoading(true);
|
|
28
|
+
setError(null);
|
|
29
|
+
try {
|
|
30
|
+
const scopedClient = resolvePodClient(client, podId);
|
|
31
|
+
const response = await scopedClient.records.list(trimmedTableName, {
|
|
32
|
+
filters: overrides.filters ?? stableFilters,
|
|
33
|
+
sort: overrides.sort ?? stableSort,
|
|
34
|
+
limit: overrides.limit ?? limit,
|
|
35
|
+
pageToken: overrides.pageToken ?? pageToken,
|
|
36
|
+
offset: overrides.offset ?? offset,
|
|
37
|
+
sortBy: overrides.sortBy ?? sortBy,
|
|
38
|
+
order: overrides.order ?? order,
|
|
39
|
+
params: overrides.params ?? stableParams,
|
|
40
|
+
});
|
|
41
|
+
if (signal?.aborted)
|
|
42
|
+
return [];
|
|
43
|
+
const nextRecords = (response.items ?? []);
|
|
44
|
+
setRecords(nextRecords);
|
|
45
|
+
setTotal(response.total ?? nextRecords.length);
|
|
46
|
+
setNextPageToken(response.next_page_token ?? null);
|
|
47
|
+
return nextRecords;
|
|
48
|
+
}
|
|
49
|
+
catch (refreshError) {
|
|
50
|
+
if (signal?.aborted)
|
|
51
|
+
return [];
|
|
52
|
+
const normalized = normalizeError(refreshError, "Failed to load records.");
|
|
53
|
+
setError(normalized);
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
if (!signal?.aborted)
|
|
58
|
+
setIsLoading(false);
|
|
59
|
+
}
|
|
60
|
+
}, [
|
|
61
|
+
client,
|
|
62
|
+
isEnabled,
|
|
63
|
+
limit,
|
|
64
|
+
offset,
|
|
65
|
+
order,
|
|
66
|
+
pageToken,
|
|
67
|
+
podId,
|
|
68
|
+
sortBy,
|
|
69
|
+
stableFilters,
|
|
70
|
+
stableParams,
|
|
71
|
+
stableSort,
|
|
72
|
+
trimmedTableName,
|
|
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]);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!isEnabled) {
|
|
109
|
+
setRecords([]);
|
|
110
|
+
setTotal(0);
|
|
111
|
+
setNextPageToken(null);
|
|
112
|
+
setError(null);
|
|
113
|
+
setIsLoading(false);
|
|
114
|
+
setIsLoadingMore(false);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!autoLoad)
|
|
118
|
+
return;
|
|
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
|
+
};
|
|
135
|
+
}, [autoLoad, isEnabled, refresh]);
|
|
136
|
+
return useMemo(() => ({
|
|
137
|
+
records,
|
|
138
|
+
total,
|
|
139
|
+
nextPageToken,
|
|
140
|
+
isLoading,
|
|
141
|
+
isLoadingMore,
|
|
142
|
+
error,
|
|
143
|
+
refresh,
|
|
144
|
+
loadMore,
|
|
145
|
+
}), [error, isLoading, isLoadingMore, loadMore, nextPageToken, records, refresh, total]);
|
|
146
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { LemmaClient } from "../client.js";
|
|
2
|
+
import type { Table } from "../types.js";
|
|
3
|
+
export interface RelatedRecordsInclude {
|
|
4
|
+
foreignKey: string;
|
|
5
|
+
as?: string;
|
|
6
|
+
fields?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface RelatedRecordsResolvedInclude {
|
|
9
|
+
foreignKey: string;
|
|
10
|
+
relationKey: string;
|
|
11
|
+
relatedTable: string;
|
|
12
|
+
relatedColumn: string;
|
|
13
|
+
fields: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface RelatedRecordsColumn {
|
|
16
|
+
key: string;
|
|
17
|
+
field: string;
|
|
18
|
+
label: string;
|
|
19
|
+
source: "base" | "related";
|
|
20
|
+
relationKey?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface UseRelatedRecordsOptions {
|
|
23
|
+
client: LemmaClient;
|
|
24
|
+
podId?: string;
|
|
25
|
+
tableName: string;
|
|
26
|
+
baseFields?: string[];
|
|
27
|
+
include: RelatedRecordsInclude[];
|
|
28
|
+
limit?: number;
|
|
29
|
+
offset?: number;
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
autoLoad?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface UseRelatedRecordsResult<TRow extends Record<string, unknown> = Record<string, unknown>> {
|
|
34
|
+
records: TRow[];
|
|
35
|
+
columns: RelatedRecordsColumn[];
|
|
36
|
+
sql: string;
|
|
37
|
+
includes: RelatedRecordsResolvedInclude[];
|
|
38
|
+
baseTable: Table | null;
|
|
39
|
+
isLoading: boolean;
|
|
40
|
+
error: Error | null;
|
|
41
|
+
refresh: () => Promise<TRow[]>;
|
|
42
|
+
}
|
|
43
|
+
export declare function useRelatedRecords<TRow extends Record<string, unknown> = Record<string, unknown>>({ client, podId, tableName, baseFields, include, limit, offset, enabled, autoLoad, }: UseRelatedRecordsOptions): UseRelatedRecordsResult<TRow>;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { buildJoinedRecordsQuery, parseForeignKeyReference } from "../datastore-query.js";
|
|
3
|
+
import { normalizeError, resolvePodClient, stringifyComparable } from "./utils.js";
|
|
4
|
+
function sentenceCase(value) {
|
|
5
|
+
return value
|
|
6
|
+
.replace(/[_\.]/g, " ")
|
|
7
|
+
.replace(/\s+/g, " ")
|
|
8
|
+
.trim()
|
|
9
|
+
.replace(/\b\w/g, (match) => match.toUpperCase());
|
|
10
|
+
}
|
|
11
|
+
function inferRelationKey(foreignKey, relatedTable) {
|
|
12
|
+
const stripped = foreignKey
|
|
13
|
+
.replace(/_id$/i, "")
|
|
14
|
+
.replace(/_uuid$/i, "")
|
|
15
|
+
.replace(/_fk$/i, "")
|
|
16
|
+
.trim();
|
|
17
|
+
return stripped.length > 0 ? stripped : relatedTable;
|
|
18
|
+
}
|
|
19
|
+
function pickDefaultBaseFields(table) {
|
|
20
|
+
const names = table.columns
|
|
21
|
+
.map((column) => column.name)
|
|
22
|
+
.filter((name) => name !== "created_at" && name !== "updated_at");
|
|
23
|
+
const prioritized = ["id", "name", "title", "label", "status", "type"];
|
|
24
|
+
const next = [];
|
|
25
|
+
prioritized.forEach((name) => {
|
|
26
|
+
if (names.includes(name) && !next.includes(name)) {
|
|
27
|
+
next.push(name);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
names.forEach((name) => {
|
|
31
|
+
if (!next.includes(name)) {
|
|
32
|
+
next.push(name);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
return next.slice(0, 5);
|
|
36
|
+
}
|
|
37
|
+
function pickDefaultRelatedFields(table, relatedColumn) {
|
|
38
|
+
const names = table.columns.map((column) => column.name);
|
|
39
|
+
const prioritized = ["id", "name", "title", "label", "email", "slug", relatedColumn];
|
|
40
|
+
const next = [];
|
|
41
|
+
prioritized.forEach((name) => {
|
|
42
|
+
if (names.includes(name) && !next.includes(name)) {
|
|
43
|
+
next.push(name);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
if (next.length === 0 && names.length > 0) {
|
|
47
|
+
next.push(names[0]);
|
|
48
|
+
}
|
|
49
|
+
return next.slice(0, 3);
|
|
50
|
+
}
|
|
51
|
+
function readAliasedValue(record, prefix, field) {
|
|
52
|
+
return record[`${prefix}${field}`];
|
|
53
|
+
}
|
|
54
|
+
export function useRelatedRecords({ client, podId, tableName, baseFields, include, limit = 20, offset, enabled = true, autoLoad = true, }) {
|
|
55
|
+
const [records, setRecords] = useState([]);
|
|
56
|
+
const [columns, setColumns] = useState([]);
|
|
57
|
+
const [sql, setSql] = useState("");
|
|
58
|
+
const [includes, setIncludes] = useState([]);
|
|
59
|
+
const [baseTable, setBaseTable] = useState(null);
|
|
60
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
61
|
+
const [error, setError] = useState(null);
|
|
62
|
+
const trimmedTableName = tableName.trim();
|
|
63
|
+
const includeKey = stringifyComparable(include);
|
|
64
|
+
const baseFieldsKey = stringifyComparable(baseFields);
|
|
65
|
+
const stableInclude = useMemo(() => include, [includeKey]);
|
|
66
|
+
const stableBaseFields = useMemo(() => baseFields, [baseFieldsKey]);
|
|
67
|
+
const isEnabled = enabled && trimmedTableName.length > 0 && stableInclude.length > 0;
|
|
68
|
+
const refresh = useCallback(async (signal) => {
|
|
69
|
+
if (!isEnabled) {
|
|
70
|
+
setRecords([]);
|
|
71
|
+
setColumns([]);
|
|
72
|
+
setSql("");
|
|
73
|
+
setIncludes([]);
|
|
74
|
+
setBaseTable(null);
|
|
75
|
+
setError(null);
|
|
76
|
+
setIsLoading(false);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
setIsLoading(true);
|
|
80
|
+
setError(null);
|
|
81
|
+
try {
|
|
82
|
+
const scopedClient = resolvePodClient(client, podId);
|
|
83
|
+
const nextBaseTable = await scopedClient.tables.get(trimmedTableName);
|
|
84
|
+
if (signal?.aborted)
|
|
85
|
+
return [];
|
|
86
|
+
if (nextBaseTable.enable_rls) {
|
|
87
|
+
throw new Error(`Related record queries are not supported for RLS-enabled table "${trimmedTableName}". Use table-scoped record APIs instead.`);
|
|
88
|
+
}
|
|
89
|
+
setBaseTable(nextBaseTable);
|
|
90
|
+
const resolvedBaseFields = (stableBaseFields?.length ? stableBaseFields : pickDefaultBaseFields(nextBaseTable))
|
|
91
|
+
.filter((field, index, allFields) => field.trim().length > 0 && allFields.indexOf(field) === index);
|
|
92
|
+
const resolvedIncludes = await Promise.all(stableInclude.map(async (entry, index) => {
|
|
93
|
+
if (signal?.aborted)
|
|
94
|
+
throw new Error("Aborted");
|
|
95
|
+
const baseColumn = nextBaseTable.columns.find((column) => column.name === entry.foreignKey) ?? null;
|
|
96
|
+
const reference = baseColumn?.foreign_key?.references
|
|
97
|
+
? parseForeignKeyReference(baseColumn.foreign_key.references)
|
|
98
|
+
: null;
|
|
99
|
+
if (!baseColumn) {
|
|
100
|
+
throw new Error(`Column "${entry.foreignKey}" was not found on table "${trimmedTableName}".`);
|
|
101
|
+
}
|
|
102
|
+
if (!reference) {
|
|
103
|
+
throw new Error(`Column "${entry.foreignKey}" on "${trimmedTableName}" is not a foreign key.`);
|
|
104
|
+
}
|
|
105
|
+
const relatedTable = await scopedClient.tables.get(reference.table);
|
|
106
|
+
if (relatedTable.enable_rls) {
|
|
107
|
+
throw new Error(`Related record queries cannot join into RLS-enabled table "${reference.table}". Use table-scoped record APIs instead.`);
|
|
108
|
+
}
|
|
109
|
+
const relationKey = entry.as?.trim() || inferRelationKey(entry.foreignKey, reference.table);
|
|
110
|
+
const relatedFields = (entry.fields?.length ? entry.fields : pickDefaultRelatedFields(relatedTable, reference.column))
|
|
111
|
+
.filter((field, fieldIndex, allFields) => field.trim().length > 0 && allFields.indexOf(field) === fieldIndex);
|
|
112
|
+
if (relatedFields.length === 0) {
|
|
113
|
+
throw new Error(`No display fields were resolved for relation "${entry.foreignKey}".`);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
foreignKey: entry.foreignKey,
|
|
117
|
+
relationKey,
|
|
118
|
+
relatedTable: reference.table,
|
|
119
|
+
relatedColumn: reference.column,
|
|
120
|
+
fields: relatedFields,
|
|
121
|
+
alias: `rel_${index}_${relationKey}`,
|
|
122
|
+
};
|
|
123
|
+
}));
|
|
124
|
+
const nextSql = buildJoinedRecordsQuery({
|
|
125
|
+
from: { table: trimmedTableName, alias: "base" },
|
|
126
|
+
select: [
|
|
127
|
+
...resolvedBaseFields.map((field) => ({
|
|
128
|
+
table: "base",
|
|
129
|
+
column: field,
|
|
130
|
+
as: `base__${field}`,
|
|
131
|
+
})),
|
|
132
|
+
...resolvedIncludes.flatMap((entry) => entry.fields.map((field) => ({
|
|
133
|
+
table: entry.alias,
|
|
134
|
+
column: field,
|
|
135
|
+
as: `${entry.relationKey}__${field}`,
|
|
136
|
+
}))),
|
|
137
|
+
],
|
|
138
|
+
joins: resolvedIncludes.map((entry) => ({
|
|
139
|
+
table: entry.relatedTable,
|
|
140
|
+
alias: entry.alias,
|
|
141
|
+
type: "left",
|
|
142
|
+
on: {
|
|
143
|
+
left: { table: "base", column: entry.foreignKey },
|
|
144
|
+
right: { table: entry.alias, column: entry.relatedColumn },
|
|
145
|
+
},
|
|
146
|
+
})),
|
|
147
|
+
limit,
|
|
148
|
+
offset,
|
|
149
|
+
});
|
|
150
|
+
setSql(nextSql);
|
|
151
|
+
setIncludes(resolvedIncludes.map(({ alias: _alias, ...rest }) => rest));
|
|
152
|
+
const response = await scopedClient.datastore.query(nextSql);
|
|
153
|
+
if (signal?.aborted)
|
|
154
|
+
return [];
|
|
155
|
+
const nextColumns = [
|
|
156
|
+
...resolvedBaseFields.map((field) => ({
|
|
157
|
+
key: field,
|
|
158
|
+
field,
|
|
159
|
+
label: sentenceCase(field),
|
|
160
|
+
source: "base",
|
|
161
|
+
})),
|
|
162
|
+
...resolvedIncludes.flatMap((entry) => entry.fields.map((field) => ({
|
|
163
|
+
key: `${entry.relationKey}.${field}`,
|
|
164
|
+
field,
|
|
165
|
+
label: `${sentenceCase(entry.relationKey)} ${sentenceCase(field)}`,
|
|
166
|
+
source: "related",
|
|
167
|
+
relationKey: entry.relationKey,
|
|
168
|
+
}))),
|
|
169
|
+
];
|
|
170
|
+
const nextRecords = (response.items ?? []).map((record) => {
|
|
171
|
+
const nextRecord = {};
|
|
172
|
+
resolvedBaseFields.forEach((field) => {
|
|
173
|
+
nextRecord[field] = readAliasedValue(record, "base__", field);
|
|
174
|
+
});
|
|
175
|
+
resolvedIncludes.forEach((entry) => {
|
|
176
|
+
nextRecord[entry.relationKey] = entry.fields.reduce((accumulator, field) => {
|
|
177
|
+
accumulator[field] = readAliasedValue(record, `${entry.relationKey}__`, field);
|
|
178
|
+
return accumulator;
|
|
179
|
+
}, {});
|
|
180
|
+
});
|
|
181
|
+
return nextRecord;
|
|
182
|
+
});
|
|
183
|
+
setColumns(nextColumns);
|
|
184
|
+
setRecords(nextRecords);
|
|
185
|
+
return nextRecords;
|
|
186
|
+
}
|
|
187
|
+
catch (refreshError) {
|
|
188
|
+
if (signal?.aborted)
|
|
189
|
+
return [];
|
|
190
|
+
const normalized = normalizeError(refreshError, "Failed to load related records.");
|
|
191
|
+
setError(normalized);
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
if (!signal?.aborted)
|
|
196
|
+
setIsLoading(false);
|
|
197
|
+
}
|
|
198
|
+
}, [client, isEnabled, limit, offset, podId, stableBaseFields, stableInclude, trimmedTableName]);
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (!isEnabled) {
|
|
201
|
+
setRecords([]);
|
|
202
|
+
setColumns([]);
|
|
203
|
+
setSql("");
|
|
204
|
+
setIncludes([]);
|
|
205
|
+
setBaseTable(null);
|
|
206
|
+
setError(null);
|
|
207
|
+
setIsLoading(false);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (!autoLoad)
|
|
211
|
+
return;
|
|
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
|
+
};
|
|
228
|
+
}, [autoLoad, isEnabled, refresh]);
|
|
229
|
+
return useMemo(() => ({
|
|
230
|
+
records,
|
|
231
|
+
columns,
|
|
232
|
+
sql,
|
|
233
|
+
includes,
|
|
234
|
+
baseTable,
|
|
235
|
+
isLoading,
|
|
236
|
+
error,
|
|
237
|
+
refresh,
|
|
238
|
+
}), [baseTable, columns, error, includes, isLoading, records, refresh, sql]);
|
|
239
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { LemmaClient } from "../client.js";
|
|
2
|
+
import type { Table } from "../types.js";
|
|
3
|
+
export interface ReverseRelationSelector {
|
|
4
|
+
tableName: string;
|
|
5
|
+
foreignKey: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ReverseRelatedRelation {
|
|
8
|
+
tableName: string;
|
|
9
|
+
foreignKey: string;
|
|
10
|
+
referencedColumn: string;
|
|
11
|
+
label: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ReverseRelatedRecordsColumn {
|
|
14
|
+
key: string;
|
|
15
|
+
field: string;
|
|
16
|
+
label: string;
|
|
17
|
+
}
|
|
18
|
+
export interface UseReverseRelatedRecordsOptions {
|
|
19
|
+
client: LemmaClient;
|
|
20
|
+
podId?: string;
|
|
21
|
+
tableName: string;
|
|
22
|
+
recordId?: string | null;
|
|
23
|
+
relation?: ReverseRelationSelector | null;
|
|
24
|
+
fields?: string[];
|
|
25
|
+
limit?: number;
|
|
26
|
+
offset?: number;
|
|
27
|
+
sortBy?: string;
|
|
28
|
+
order?: "asc" | "desc" | string;
|
|
29
|
+
tablesLimit?: number;
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
autoLoad?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface UseReverseRelatedRecordsResult<TRow extends Record<string, unknown> = Record<string, unknown>> {
|
|
34
|
+
parentTable: Table | null;
|
|
35
|
+
relatedTable: Table | null;
|
|
36
|
+
parentRecord: Record<string, unknown> | null;
|
|
37
|
+
relations: ReverseRelatedRelation[];
|
|
38
|
+
selectedRelation: ReverseRelatedRelation | null;
|
|
39
|
+
columns: ReverseRelatedRecordsColumn[];
|
|
40
|
+
records: TRow[];
|
|
41
|
+
total: number;
|
|
42
|
+
nextPageToken: string | null;
|
|
43
|
+
isLoading: boolean;
|
|
44
|
+
error: Error | null;
|
|
45
|
+
refresh: () => Promise<TRow[]>;
|
|
46
|
+
}
|
|
47
|
+
export declare function useReverseRelatedRecords<TRow extends Record<string, unknown> = Record<string, unknown>>({ client, podId, tableName, recordId, relation, fields, limit, offset, sortBy, order, tablesLimit, enabled, autoLoad, }: UseReverseRelatedRecordsOptions): UseReverseRelatedRecordsResult<TRow>;
|