lemma-sdk 0.2.27 → 0.2.30
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 +113 -233
- 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/ConvertedArtifactResponse.js +1 -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/components/AssistantExperience.js +6 -4
- package/dist/react/index.d.ts +42 -8
- package/dist/react/index.js +21 -4
- package/dist/react/useAgentRun.d.ts +17 -0
- package/dist/react/useAgentRun.js +58 -0
- package/dist/react/useAssistantRun.d.ts +9 -0
- package/dist/react/useAssistantRun.js +19 -9
- package/dist/react/useAssistantSession.d.ts +5 -0
- package/dist/react/useAssistantSession.js +123 -70
- package/dist/react/useBulkRecords.d.ts +20 -0
- package/dist/react/useBulkRecords.js +72 -0
- package/dist/react/useConversation.d.ts +18 -0
- package/dist/react/useConversation.js +59 -0
- package/dist/react/useConversationMessages.d.ts +59 -0
- package/dist/react/useConversationMessages.js +167 -0
- package/dist/react/useConversations.d.ts +48 -0
- package/dist/react/useConversations.js +182 -0
- package/dist/react/useCreateRecord.d.ts +18 -0
- package/dist/react/useCreateRecord.js +58 -0
- package/dist/react/useDeleteRecord.d.ts +21 -0
- package/dist/react/useDeleteRecord.js +59 -0
- package/dist/react/useForeignKeyOptions.d.ts +31 -0
- package/dist/react/useForeignKeyOptions.js +150 -0
- package/dist/react/useJoinedRecords.d.ts +18 -0
- package/dist/react/useJoinedRecords.js +79 -0
- package/dist/react/useMembers.d.ts +22 -0
- package/dist/react/useMembers.js +59 -0
- package/dist/react/useRecord.d.ts +18 -0
- package/dist/react/useRecord.js +64 -0
- package/dist/react/useRecordForm.d.ts +42 -0
- package/dist/react/useRecordForm.js +238 -0
- package/dist/react/useRecordSchema.d.ts +20 -0
- package/dist/react/useRecordSchema.js +24 -0
- package/dist/react/useRecords.d.ts +18 -0
- package/dist/react/useRecords.js +106 -0
- package/dist/react/useRelatedRecords.d.ts +43 -0
- package/dist/react/useRelatedRecords.js +232 -0
- package/dist/react/useReverseRelatedRecords.d.ts +47 -0
- package/dist/react/useReverseRelatedRecords.js +226 -0
- package/dist/react/useSchemaForm.d.ts +24 -0
- package/dist/react/useSchemaForm.js +116 -0
- package/dist/react/useTable.d.ts +16 -0
- package/dist/react/useTable.js +59 -0
- package/dist/react/useTables.d.ts +22 -0
- package/dist/react/useTables.js +71 -0
- package/dist/react/useUpdateRecord.d.ts +21 -0
- package/dist/react/useUpdateRecord.js +62 -0
- package/dist/react/useWorkflowStart.d.ts +33 -0
- package/dist/react/useWorkflowStart.js +155 -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 +5 -1
- package/package.json +10 -5
- package/dist/react/styles.css +0 -2401
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
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
|
+
}
|
|
21
|
+
function sentenceCase(value) {
|
|
22
|
+
return value
|
|
23
|
+
.replace(/[_\.]/g, " ")
|
|
24
|
+
.replace(/\s+/g, " ")
|
|
25
|
+
.trim()
|
|
26
|
+
.replace(/\b\w/g, (match) => match.toUpperCase());
|
|
27
|
+
}
|
|
28
|
+
function inferRelationKey(foreignKey, relatedTable) {
|
|
29
|
+
const stripped = foreignKey
|
|
30
|
+
.replace(/_id$/i, "")
|
|
31
|
+
.replace(/_uuid$/i, "")
|
|
32
|
+
.replace(/_fk$/i, "")
|
|
33
|
+
.trim();
|
|
34
|
+
return stripped.length > 0 ? stripped : relatedTable;
|
|
35
|
+
}
|
|
36
|
+
function pickDefaultBaseFields(table) {
|
|
37
|
+
const names = table.columns
|
|
38
|
+
.map((column) => column.name)
|
|
39
|
+
.filter((name) => name !== "created_at" && name !== "updated_at");
|
|
40
|
+
const prioritized = ["id", "name", "title", "label", "status", "type"];
|
|
41
|
+
const next = [];
|
|
42
|
+
prioritized.forEach((name) => {
|
|
43
|
+
if (names.includes(name) && !next.includes(name)) {
|
|
44
|
+
next.push(name);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
names.forEach((name) => {
|
|
48
|
+
if (!next.includes(name)) {
|
|
49
|
+
next.push(name);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return next.slice(0, 5);
|
|
53
|
+
}
|
|
54
|
+
function pickDefaultRelatedFields(table, relatedColumn) {
|
|
55
|
+
const names = table.columns.map((column) => column.name);
|
|
56
|
+
const prioritized = ["id", "name", "title", "label", "email", "slug", relatedColumn];
|
|
57
|
+
const next = [];
|
|
58
|
+
prioritized.forEach((name) => {
|
|
59
|
+
if (names.includes(name) && !next.includes(name)) {
|
|
60
|
+
next.push(name);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
if (next.length === 0 && names.length > 0) {
|
|
64
|
+
next.push(names[0]);
|
|
65
|
+
}
|
|
66
|
+
return next.slice(0, 3);
|
|
67
|
+
}
|
|
68
|
+
function readAliasedValue(record, prefix, field) {
|
|
69
|
+
return record[`${prefix}${field}`];
|
|
70
|
+
}
|
|
71
|
+
export function useRelatedRecords({ client, podId, tableName, baseFields, include, limit = 20, offset, enabled = true, autoLoad = true, }) {
|
|
72
|
+
const [records, setRecords] = useState([]);
|
|
73
|
+
const [columns, setColumns] = useState([]);
|
|
74
|
+
const [sql, setSql] = useState("");
|
|
75
|
+
const [includes, setIncludes] = useState([]);
|
|
76
|
+
const [baseTable, setBaseTable] = useState(null);
|
|
77
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
78
|
+
const [error, setError] = useState(null);
|
|
79
|
+
const trimmedTableName = tableName.trim();
|
|
80
|
+
const includeKey = stringifyComparable(include);
|
|
81
|
+
const baseFieldsKey = stringifyComparable(baseFields);
|
|
82
|
+
const stableInclude = useMemo(() => include, [includeKey]);
|
|
83
|
+
const stableBaseFields = useMemo(() => baseFields, [baseFieldsKey]);
|
|
84
|
+
const isEnabled = enabled && trimmedTableName.length > 0 && stableInclude.length > 0;
|
|
85
|
+
const refresh = useCallback(async () => {
|
|
86
|
+
if (!isEnabled) {
|
|
87
|
+
setRecords([]);
|
|
88
|
+
setColumns([]);
|
|
89
|
+
setSql("");
|
|
90
|
+
setIncludes([]);
|
|
91
|
+
setBaseTable(null);
|
|
92
|
+
setError(null);
|
|
93
|
+
setIsLoading(false);
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
setIsLoading(true);
|
|
97
|
+
setError(null);
|
|
98
|
+
try {
|
|
99
|
+
const scopedClient = resolvePodClient(client, podId);
|
|
100
|
+
const nextBaseTable = await scopedClient.tables.get(trimmedTableName);
|
|
101
|
+
if (nextBaseTable.enable_rls) {
|
|
102
|
+
throw new Error(`Related record queries are not supported for RLS-enabled table "${trimmedTableName}". Use table-scoped record APIs instead.`);
|
|
103
|
+
}
|
|
104
|
+
setBaseTable(nextBaseTable);
|
|
105
|
+
const resolvedBaseFields = (stableBaseFields?.length ? stableBaseFields : pickDefaultBaseFields(nextBaseTable))
|
|
106
|
+
.filter((field, index, allFields) => field.trim().length > 0 && allFields.indexOf(field) === index);
|
|
107
|
+
const resolvedIncludes = await Promise.all(stableInclude.map(async (entry, index) => {
|
|
108
|
+
const baseColumn = nextBaseTable.columns.find((column) => column.name === entry.foreignKey) ?? null;
|
|
109
|
+
const reference = baseColumn?.foreign_key?.references
|
|
110
|
+
? parseForeignKeyReference(baseColumn.foreign_key.references)
|
|
111
|
+
: null;
|
|
112
|
+
if (!baseColumn) {
|
|
113
|
+
throw new Error(`Column "${entry.foreignKey}" was not found on table "${trimmedTableName}".`);
|
|
114
|
+
}
|
|
115
|
+
if (!reference) {
|
|
116
|
+
throw new Error(`Column "${entry.foreignKey}" on "${trimmedTableName}" is not a foreign key.`);
|
|
117
|
+
}
|
|
118
|
+
const relatedTable = await scopedClient.tables.get(reference.table);
|
|
119
|
+
if (relatedTable.enable_rls) {
|
|
120
|
+
throw new Error(`Related record queries cannot join into RLS-enabled table "${reference.table}". Use table-scoped record APIs instead.`);
|
|
121
|
+
}
|
|
122
|
+
const relationKey = entry.as?.trim() || inferRelationKey(entry.foreignKey, reference.table);
|
|
123
|
+
const relatedFields = (entry.fields?.length ? entry.fields : pickDefaultRelatedFields(relatedTable, reference.column))
|
|
124
|
+
.filter((field, fieldIndex, allFields) => field.trim().length > 0 && allFields.indexOf(field) === fieldIndex);
|
|
125
|
+
if (relatedFields.length === 0) {
|
|
126
|
+
throw new Error(`No display fields were resolved for relation "${entry.foreignKey}".`);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
foreignKey: entry.foreignKey,
|
|
130
|
+
relationKey,
|
|
131
|
+
relatedTable: reference.table,
|
|
132
|
+
relatedColumn: reference.column,
|
|
133
|
+
fields: relatedFields,
|
|
134
|
+
alias: `rel_${index}_${relationKey}`,
|
|
135
|
+
};
|
|
136
|
+
}));
|
|
137
|
+
const nextSql = buildJoinedRecordsQuery({
|
|
138
|
+
from: { table: trimmedTableName, alias: "base" },
|
|
139
|
+
select: [
|
|
140
|
+
...resolvedBaseFields.map((field) => ({
|
|
141
|
+
table: "base",
|
|
142
|
+
column: field,
|
|
143
|
+
as: `base__${field}`,
|
|
144
|
+
})),
|
|
145
|
+
...resolvedIncludes.flatMap((entry) => entry.fields.map((field) => ({
|
|
146
|
+
table: entry.alias,
|
|
147
|
+
column: field,
|
|
148
|
+
as: `${entry.relationKey}__${field}`,
|
|
149
|
+
}))),
|
|
150
|
+
],
|
|
151
|
+
joins: resolvedIncludes.map((entry) => ({
|
|
152
|
+
table: entry.relatedTable,
|
|
153
|
+
alias: entry.alias,
|
|
154
|
+
type: "left",
|
|
155
|
+
on: {
|
|
156
|
+
left: { table: "base", column: entry.foreignKey },
|
|
157
|
+
right: { table: entry.alias, column: entry.relatedColumn },
|
|
158
|
+
},
|
|
159
|
+
})),
|
|
160
|
+
limit,
|
|
161
|
+
offset,
|
|
162
|
+
});
|
|
163
|
+
setSql(nextSql);
|
|
164
|
+
setIncludes(resolvedIncludes.map(({ alias: _alias, ...rest }) => rest));
|
|
165
|
+
const response = await scopedClient.datastore.query(nextSql);
|
|
166
|
+
const nextColumns = [
|
|
167
|
+
...resolvedBaseFields.map((field) => ({
|
|
168
|
+
key: field,
|
|
169
|
+
field,
|
|
170
|
+
label: sentenceCase(field),
|
|
171
|
+
source: "base",
|
|
172
|
+
})),
|
|
173
|
+
...resolvedIncludes.flatMap((entry) => entry.fields.map((field) => ({
|
|
174
|
+
key: `${entry.relationKey}.${field}`,
|
|
175
|
+
field,
|
|
176
|
+
label: `${sentenceCase(entry.relationKey)} ${sentenceCase(field)}`,
|
|
177
|
+
source: "related",
|
|
178
|
+
relationKey: entry.relationKey,
|
|
179
|
+
}))),
|
|
180
|
+
];
|
|
181
|
+
const nextRecords = (response.items ?? []).map((record) => {
|
|
182
|
+
const nextRecord = {};
|
|
183
|
+
resolvedBaseFields.forEach((field) => {
|
|
184
|
+
nextRecord[field] = readAliasedValue(record, "base__", field);
|
|
185
|
+
});
|
|
186
|
+
resolvedIncludes.forEach((entry) => {
|
|
187
|
+
nextRecord[entry.relationKey] = entry.fields.reduce((accumulator, field) => {
|
|
188
|
+
accumulator[field] = readAliasedValue(record, `${entry.relationKey}__`, field);
|
|
189
|
+
return accumulator;
|
|
190
|
+
}, {});
|
|
191
|
+
});
|
|
192
|
+
return nextRecord;
|
|
193
|
+
});
|
|
194
|
+
setColumns(nextColumns);
|
|
195
|
+
setRecords(nextRecords);
|
|
196
|
+
return nextRecords;
|
|
197
|
+
}
|
|
198
|
+
catch (refreshError) {
|
|
199
|
+
const normalized = normalizeError(refreshError, "Failed to load related records.");
|
|
200
|
+
setError(normalized);
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
setIsLoading(false);
|
|
205
|
+
}
|
|
206
|
+
}, [client, isEnabled, limit, offset, podId, stableBaseFields, stableInclude, trimmedTableName]);
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (!isEnabled) {
|
|
209
|
+
setRecords([]);
|
|
210
|
+
setColumns([]);
|
|
211
|
+
setSql("");
|
|
212
|
+
setIncludes([]);
|
|
213
|
+
setBaseTable(null);
|
|
214
|
+
setError(null);
|
|
215
|
+
setIsLoading(false);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!autoLoad)
|
|
219
|
+
return;
|
|
220
|
+
void refresh();
|
|
221
|
+
}, [autoLoad, isEnabled, refresh]);
|
|
222
|
+
return useMemo(() => ({
|
|
223
|
+
records,
|
|
224
|
+
columns,
|
|
225
|
+
sql,
|
|
226
|
+
includes,
|
|
227
|
+
baseTable,
|
|
228
|
+
isLoading,
|
|
229
|
+
error,
|
|
230
|
+
refresh,
|
|
231
|
+
}), [baseTable, columns, error, includes, isLoading, records, refresh, sql]);
|
|
232
|
+
}
|
|
@@ -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>;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
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
|
+
}
|
|
21
|
+
function sentenceCase(value) {
|
|
22
|
+
return value
|
|
23
|
+
.replace(/[_\.]/g, " ")
|
|
24
|
+
.replace(/\s+/g, " ")
|
|
25
|
+
.trim()
|
|
26
|
+
.replace(/\b\w/g, (match) => match.toUpperCase());
|
|
27
|
+
}
|
|
28
|
+
function pickDefaultFields(table, foreignKey) {
|
|
29
|
+
const names = table.columns
|
|
30
|
+
.map((column) => column.name)
|
|
31
|
+
.filter((name) => name !== "created_at" && name !== "updated_at");
|
|
32
|
+
const prioritized = ["id", "name", "title", "label", "status", foreignKey];
|
|
33
|
+
const next = [];
|
|
34
|
+
prioritized.forEach((name) => {
|
|
35
|
+
if (names.includes(name) && !next.includes(name)) {
|
|
36
|
+
next.push(name);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
names.forEach((name) => {
|
|
40
|
+
if (!next.includes(name)) {
|
|
41
|
+
next.push(name);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return next.slice(0, 6);
|
|
45
|
+
}
|
|
46
|
+
export function useReverseRelatedRecords({ client, podId, tableName, recordId = null, relation = null, fields, limit = 20, offset, sortBy, order, tablesLimit = 100, enabled = true, autoLoad = true, }) {
|
|
47
|
+
const [parentTable, setParentTable] = useState(null);
|
|
48
|
+
const [relatedTable, setRelatedTable] = useState(null);
|
|
49
|
+
const [parentRecord, setParentRecord] = useState(null);
|
|
50
|
+
const [relations, setRelations] = useState([]);
|
|
51
|
+
const [selectedRelation, setSelectedRelation] = useState(null);
|
|
52
|
+
const [columns, setColumns] = useState([]);
|
|
53
|
+
const [records, setRecords] = useState([]);
|
|
54
|
+
const [total, setTotal] = useState(0);
|
|
55
|
+
const [nextPageToken, setNextPageToken] = useState(null);
|
|
56
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
57
|
+
const [error, setError] = useState(null);
|
|
58
|
+
const trimmedTableName = tableName.trim();
|
|
59
|
+
const trimmedRecordId = typeof recordId === "string" ? recordId.trim() : "";
|
|
60
|
+
const relationKey = stringifyComparable(relation);
|
|
61
|
+
const fieldsKey = stringifyComparable(fields);
|
|
62
|
+
const stableRelation = useMemo(() => relation, [relationKey]);
|
|
63
|
+
const stableFields = useMemo(() => fields, [fieldsKey]);
|
|
64
|
+
const isEnabled = enabled && trimmedTableName.length > 0 && trimmedRecordId.length > 0;
|
|
65
|
+
const refresh = useCallback(async () => {
|
|
66
|
+
if (!isEnabled) {
|
|
67
|
+
setParentTable(null);
|
|
68
|
+
setRelatedTable(null);
|
|
69
|
+
setParentRecord(null);
|
|
70
|
+
setRelations([]);
|
|
71
|
+
setSelectedRelation(null);
|
|
72
|
+
setColumns([]);
|
|
73
|
+
setRecords([]);
|
|
74
|
+
setTotal(0);
|
|
75
|
+
setNextPageToken(null);
|
|
76
|
+
setError(null);
|
|
77
|
+
setIsLoading(false);
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
setIsLoading(true);
|
|
81
|
+
setError(null);
|
|
82
|
+
try {
|
|
83
|
+
const scopedClient = resolvePodClient(client, podId);
|
|
84
|
+
const [tablesResponse, parentRecordResponse] = await Promise.all([
|
|
85
|
+
scopedClient.tables.list({ limit: tablesLimit }),
|
|
86
|
+
scopedClient.records.get(trimmedTableName, trimmedRecordId),
|
|
87
|
+
]);
|
|
88
|
+
const listedTables = tablesResponse.items ?? [];
|
|
89
|
+
const nextParentTable = listedTables.find((tableEntry) => tableEntry.name === trimmedTableName)
|
|
90
|
+
?? await scopedClient.tables.get(trimmedTableName);
|
|
91
|
+
const nextParentRecord = parentRecordResponse.data ?? null;
|
|
92
|
+
setParentTable(nextParentTable);
|
|
93
|
+
setParentRecord(nextParentRecord);
|
|
94
|
+
const nextRelations = listedTables.flatMap((candidateTable) => candidateTable.columns.flatMap((column) => {
|
|
95
|
+
const reference = column.foreign_key?.references
|
|
96
|
+
? parseForeignKeyReference(column.foreign_key.references)
|
|
97
|
+
: null;
|
|
98
|
+
if (!reference || reference.table !== trimmedTableName) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
return [{
|
|
102
|
+
tableName: candidateTable.name,
|
|
103
|
+
foreignKey: column.name,
|
|
104
|
+
referencedColumn: reference.column,
|
|
105
|
+
label: `${candidateTable.name}.${column.name} -> ${trimmedTableName}.${reference.column}`,
|
|
106
|
+
}];
|
|
107
|
+
}));
|
|
108
|
+
setRelations(nextRelations);
|
|
109
|
+
const nextSelectedRelation = stableRelation
|
|
110
|
+
? nextRelations.find((entry) => (entry.tableName === stableRelation.tableName
|
|
111
|
+
&& entry.foreignKey === stableRelation.foreignKey)) ?? null
|
|
112
|
+
: (nextRelations[0] ?? null);
|
|
113
|
+
setSelectedRelation(nextSelectedRelation);
|
|
114
|
+
if (!nextSelectedRelation) {
|
|
115
|
+
setRelatedTable(null);
|
|
116
|
+
setColumns([]);
|
|
117
|
+
setRecords([]);
|
|
118
|
+
setTotal(0);
|
|
119
|
+
setNextPageToken(null);
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
const nextRelatedTable = listedTables.find((tableEntry) => tableEntry.name === nextSelectedRelation.tableName)
|
|
123
|
+
?? await scopedClient.tables.get(nextSelectedRelation.tableName);
|
|
124
|
+
const referenceValue = nextParentRecord?.[nextSelectedRelation.referencedColumn]
|
|
125
|
+
?? (nextSelectedRelation.referencedColumn === "id" ? trimmedRecordId : undefined);
|
|
126
|
+
setRelatedTable(nextRelatedTable);
|
|
127
|
+
if (typeof referenceValue === "undefined" || referenceValue === null) {
|
|
128
|
+
setColumns([]);
|
|
129
|
+
setRecords([]);
|
|
130
|
+
setTotal(0);
|
|
131
|
+
setNextPageToken(null);
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const resolvedFields = (stableFields?.length ? stableFields : pickDefaultFields(nextRelatedTable, nextSelectedRelation.foreignKey))
|
|
135
|
+
.filter((field, index, allFields) => field.trim().length > 0 && allFields.indexOf(field) === index);
|
|
136
|
+
const response = await scopedClient.records.list(nextSelectedRelation.tableName, {
|
|
137
|
+
filters: [{
|
|
138
|
+
field: nextSelectedRelation.foreignKey,
|
|
139
|
+
op: "eq",
|
|
140
|
+
value: referenceValue,
|
|
141
|
+
}],
|
|
142
|
+
limit,
|
|
143
|
+
offset,
|
|
144
|
+
sortBy,
|
|
145
|
+
order,
|
|
146
|
+
});
|
|
147
|
+
const nextRecords = (response.items ?? []);
|
|
148
|
+
setColumns(resolvedFields.map((field) => ({
|
|
149
|
+
key: field,
|
|
150
|
+
field,
|
|
151
|
+
label: sentenceCase(field),
|
|
152
|
+
})));
|
|
153
|
+
setRecords(nextRecords);
|
|
154
|
+
setTotal(response.total ?? nextRecords.length);
|
|
155
|
+
setNextPageToken(response.next_page_token ?? null);
|
|
156
|
+
return nextRecords;
|
|
157
|
+
}
|
|
158
|
+
catch (refreshError) {
|
|
159
|
+
const normalized = normalizeError(refreshError, "Failed to load reverse-related records.");
|
|
160
|
+
setError(normalized);
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
setIsLoading(false);
|
|
165
|
+
}
|
|
166
|
+
}, [
|
|
167
|
+
client,
|
|
168
|
+
isEnabled,
|
|
169
|
+
limit,
|
|
170
|
+
offset,
|
|
171
|
+
order,
|
|
172
|
+
podId,
|
|
173
|
+
sortBy,
|
|
174
|
+
stableFields,
|
|
175
|
+
stableRelation,
|
|
176
|
+
tablesLimit,
|
|
177
|
+
trimmedRecordId,
|
|
178
|
+
trimmedTableName,
|
|
179
|
+
]);
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (!isEnabled) {
|
|
182
|
+
setParentTable(null);
|
|
183
|
+
setRelatedTable(null);
|
|
184
|
+
setParentRecord(null);
|
|
185
|
+
setRelations([]);
|
|
186
|
+
setSelectedRelation(null);
|
|
187
|
+
setColumns([]);
|
|
188
|
+
setRecords([]);
|
|
189
|
+
setTotal(0);
|
|
190
|
+
setNextPageToken(null);
|
|
191
|
+
setError(null);
|
|
192
|
+
setIsLoading(false);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!autoLoad)
|
|
196
|
+
return;
|
|
197
|
+
void refresh();
|
|
198
|
+
}, [autoLoad, isEnabled, refresh]);
|
|
199
|
+
return useMemo(() => ({
|
|
200
|
+
parentTable,
|
|
201
|
+
relatedTable,
|
|
202
|
+
parentRecord,
|
|
203
|
+
relations,
|
|
204
|
+
selectedRelation,
|
|
205
|
+
columns,
|
|
206
|
+
records,
|
|
207
|
+
total,
|
|
208
|
+
nextPageToken,
|
|
209
|
+
isLoading,
|
|
210
|
+
error,
|
|
211
|
+
refresh,
|
|
212
|
+
}), [
|
|
213
|
+
columns,
|
|
214
|
+
error,
|
|
215
|
+
isLoading,
|
|
216
|
+
nextPageToken,
|
|
217
|
+
parentRecord,
|
|
218
|
+
parentTable,
|
|
219
|
+
records,
|
|
220
|
+
refresh,
|
|
221
|
+
relatedTable,
|
|
222
|
+
relations,
|
|
223
|
+
selectedRelation,
|
|
224
|
+
total,
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { JsonSchemaLike, SchemaFormField } from "../schema-form.js";
|
|
2
|
+
export interface UseSchemaFormOptions {
|
|
3
|
+
schema?: JsonSchemaLike | null;
|
|
4
|
+
uiSchema?: Record<string, unknown> | null;
|
|
5
|
+
initialValues?: Record<string, unknown>;
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
onSubmit?: (data: Record<string, unknown>) => Promise<unknown> | unknown;
|
|
8
|
+
onError?: (error: unknown) => void;
|
|
9
|
+
}
|
|
10
|
+
export interface UseSchemaFormResult {
|
|
11
|
+
fields: SchemaFormField[];
|
|
12
|
+
values: Record<string, unknown>;
|
|
13
|
+
baselineValues: Record<string, unknown>;
|
|
14
|
+
fieldErrors: Record<string, string>;
|
|
15
|
+
isSubmitting: boolean;
|
|
16
|
+
isDirty: boolean;
|
|
17
|
+
error: Error | null;
|
|
18
|
+
setValue: (fieldName: string, value: unknown) => void;
|
|
19
|
+
setValues: (values: Record<string, unknown>) => void;
|
|
20
|
+
reset: (nextValues?: Record<string, unknown>) => void;
|
|
21
|
+
validate: () => boolean;
|
|
22
|
+
submit: () => Promise<Record<string, unknown> | null>;
|
|
23
|
+
}
|
|
24
|
+
export declare function useSchemaForm({ schema, uiSchema, initialValues, enabled, onSubmit, onError, }: UseSchemaFormOptions): UseSchemaFormResult;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
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
|
+
}
|
|
16
|
+
const EMPTY_VALUES = {};
|
|
17
|
+
export function useSchemaForm({ schema = null, uiSchema = null, initialValues = EMPTY_VALUES, enabled = true, onSubmit, onError, }) {
|
|
18
|
+
const [values, setValuesState] = useState({});
|
|
19
|
+
const [baselineValues, setBaselineValues] = useState({});
|
|
20
|
+
const [fieldErrors, setFieldErrors] = useState({});
|
|
21
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
22
|
+
const [error, setError] = useState(null);
|
|
23
|
+
const schemaKey = stringifyComparable(schema);
|
|
24
|
+
const uiSchemaKey = stringifyComparable(uiSchema);
|
|
25
|
+
const initialValuesKey = stringifyComparable(initialValues);
|
|
26
|
+
const stableSchema = useMemo(() => schema, [schemaKey]);
|
|
27
|
+
const stableUiSchema = useMemo(() => uiSchema, [uiSchemaKey]);
|
|
28
|
+
const stableInitialValues = useMemo(() => initialValues, [initialValuesKey]);
|
|
29
|
+
const fields = useMemo(() => buildSchemaFormFields(stableSchema ?? undefined, stableUiSchema ?? undefined), [stableSchema, stableUiSchema]);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!enabled) {
|
|
32
|
+
setValuesState({});
|
|
33
|
+
setBaselineValues({});
|
|
34
|
+
setFieldErrors({});
|
|
35
|
+
setError(null);
|
|
36
|
+
setIsSubmitting(false);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const nextValues = buildSchemaFormValues(stableSchema ?? undefined, stableInitialValues, stableUiSchema ?? undefined);
|
|
40
|
+
setValuesState(nextValues);
|
|
41
|
+
setBaselineValues(nextValues);
|
|
42
|
+
setFieldErrors({});
|
|
43
|
+
}, [enabled, stableInitialValues, stableSchema, stableUiSchema]);
|
|
44
|
+
const setValue = useCallback((fieldName, value) => {
|
|
45
|
+
setValuesState((current) => ({
|
|
46
|
+
...current,
|
|
47
|
+
[fieldName]: value,
|
|
48
|
+
}));
|
|
49
|
+
setFieldErrors((current) => {
|
|
50
|
+
if (!(fieldName in current))
|
|
51
|
+
return current;
|
|
52
|
+
const next = { ...current };
|
|
53
|
+
delete next[fieldName];
|
|
54
|
+
return next;
|
|
55
|
+
});
|
|
56
|
+
}, []);
|
|
57
|
+
const setValues = useCallback((nextValues) => {
|
|
58
|
+
setValuesState((current) => ({
|
|
59
|
+
...current,
|
|
60
|
+
...nextValues,
|
|
61
|
+
}));
|
|
62
|
+
}, []);
|
|
63
|
+
const reset = useCallback((nextValues) => {
|
|
64
|
+
const resolved = buildSchemaFormValues(stableSchema ?? undefined, nextValues ?? stableInitialValues, stableUiSchema ?? undefined);
|
|
65
|
+
setValuesState(resolved);
|
|
66
|
+
setBaselineValues(resolved);
|
|
67
|
+
setFieldErrors({});
|
|
68
|
+
setError(null);
|
|
69
|
+
}, [stableInitialValues, stableSchema, stableUiSchema]);
|
|
70
|
+
const validate = useCallback(() => {
|
|
71
|
+
const result = buildSchemaFormPayload(stableSchema ?? undefined, values, stableUiSchema ?? undefined);
|
|
72
|
+
setFieldErrors(result.errors);
|
|
73
|
+
return result.isValid;
|
|
74
|
+
}, [stableSchema, stableUiSchema, values]);
|
|
75
|
+
const submit = useCallback(async () => {
|
|
76
|
+
const result = buildSchemaFormPayload(stableSchema ?? undefined, values, stableUiSchema ?? undefined);
|
|
77
|
+
setFieldErrors(result.errors);
|
|
78
|
+
if (!result.isValid) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (!onSubmit) {
|
|
82
|
+
return result.data;
|
|
83
|
+
}
|
|
84
|
+
setIsSubmitting(true);
|
|
85
|
+
setError(null);
|
|
86
|
+
try {
|
|
87
|
+
await onSubmit(result.data);
|
|
88
|
+
setBaselineValues(values);
|
|
89
|
+
return result.data;
|
|
90
|
+
}
|
|
91
|
+
catch (submitError) {
|
|
92
|
+
const normalized = normalizeError(submitError, "Failed to submit schema form.");
|
|
93
|
+
setError(normalized);
|
|
94
|
+
onError?.(submitError);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
setIsSubmitting(false);
|
|
99
|
+
}
|
|
100
|
+
}, [onError, onSubmit, stableSchema, stableUiSchema, values]);
|
|
101
|
+
const isDirty = useMemo(() => stringifyComparable(values) !== stringifyComparable(baselineValues), [baselineValues, values]);
|
|
102
|
+
return useMemo(() => ({
|
|
103
|
+
fields,
|
|
104
|
+
values,
|
|
105
|
+
baselineValues,
|
|
106
|
+
fieldErrors,
|
|
107
|
+
isSubmitting,
|
|
108
|
+
isDirty,
|
|
109
|
+
error,
|
|
110
|
+
setValue,
|
|
111
|
+
setValues,
|
|
112
|
+
reset,
|
|
113
|
+
validate,
|
|
114
|
+
submit,
|
|
115
|
+
}), [baselineValues, error, fieldErrors, fields, isDirty, isSubmitting, reset, setValue, setValues, submit, validate, values]);
|
|
116
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LemmaClient } from "../client.js";
|
|
2
|
+
import type { Table } from "../types.js";
|
|
3
|
+
export interface UseTableOptions {
|
|
4
|
+
client: LemmaClient;
|
|
5
|
+
podId?: string;
|
|
6
|
+
tableName: string;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
autoLoad?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface UseTableResult {
|
|
11
|
+
table: Table | null;
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
error: Error | null;
|
|
14
|
+
refresh: () => Promise<Table | null>;
|
|
15
|
+
}
|
|
16
|
+
export declare function useTable({ client, podId, tableName, enabled, autoLoad, }: UseTableOptions): UseTableResult;
|