lemma-sdk 0.2.28 → 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/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 -2407
|
@@ -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;
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
export function useTable({ client, podId, tableName, enabled = true, autoLoad = true, }) {
|
|
13
|
+
const [table, setTable] = useState(null);
|
|
14
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
15
|
+
const [error, setError] = useState(null);
|
|
16
|
+
const trimmedTableName = tableName.trim();
|
|
17
|
+
const isEnabled = enabled && trimmedTableName.length > 0;
|
|
18
|
+
const refresh = useCallback(async () => {
|
|
19
|
+
if (!isEnabled) {
|
|
20
|
+
setTable(null);
|
|
21
|
+
setError(null);
|
|
22
|
+
setIsLoading(false);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
setIsLoading(true);
|
|
26
|
+
setError(null);
|
|
27
|
+
try {
|
|
28
|
+
const scopedClient = resolvePodClient(client, podId);
|
|
29
|
+
const nextTable = await scopedClient.tables.get(trimmedTableName);
|
|
30
|
+
setTable(nextTable);
|
|
31
|
+
return nextTable;
|
|
32
|
+
}
|
|
33
|
+
catch (refreshError) {
|
|
34
|
+
const normalized = normalizeError(refreshError, "Failed to load table.");
|
|
35
|
+
setError(normalized);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
setIsLoading(false);
|
|
40
|
+
}
|
|
41
|
+
}, [client, isEnabled, podId, trimmedTableName]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!isEnabled) {
|
|
44
|
+
setTable(null);
|
|
45
|
+
setError(null);
|
|
46
|
+
setIsLoading(false);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!autoLoad)
|
|
50
|
+
return;
|
|
51
|
+
void refresh();
|
|
52
|
+
}, [autoLoad, isEnabled, refresh]);
|
|
53
|
+
return useMemo(() => ({
|
|
54
|
+
table,
|
|
55
|
+
isLoading,
|
|
56
|
+
error,
|
|
57
|
+
refresh,
|
|
58
|
+
}), [error, isLoading, refresh, table]);
|
|
59
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LemmaClient } from "../client.js";
|
|
2
|
+
import type { Table } from "../types.js";
|
|
3
|
+
export interface UseTablesOptions {
|
|
4
|
+
client: LemmaClient;
|
|
5
|
+
podId?: string;
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
autoLoad?: boolean;
|
|
8
|
+
limit?: number;
|
|
9
|
+
pageToken?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface UseTablesResult {
|
|
12
|
+
tables: Table[];
|
|
13
|
+
total: number;
|
|
14
|
+
nextPageToken: string | null;
|
|
15
|
+
isLoading: boolean;
|
|
16
|
+
error: Error | null;
|
|
17
|
+
refresh: (overrides?: {
|
|
18
|
+
limit?: number;
|
|
19
|
+
pageToken?: string;
|
|
20
|
+
}) => Promise<Table[]>;
|
|
21
|
+
}
|
|
22
|
+
export declare function useTables({ client, podId, enabled, autoLoad, limit, pageToken, }: UseTablesOptions): UseTablesResult;
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
export function useTables({ client, podId, enabled = true, autoLoad = true, limit = 100, pageToken, }) {
|
|
13
|
+
const [tables, setTables] = useState([]);
|
|
14
|
+
const [total, setTotal] = useState(0);
|
|
15
|
+
const [nextPageToken, setNextPageToken] = useState(null);
|
|
16
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
17
|
+
const [error, setError] = useState(null);
|
|
18
|
+
const refresh = useCallback(async (overrides = {}) => {
|
|
19
|
+
if (!enabled) {
|
|
20
|
+
setTables([]);
|
|
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.tables.list({
|
|
32
|
+
limit: overrides.limit ?? limit,
|
|
33
|
+
pageToken: overrides.pageToken ?? pageToken,
|
|
34
|
+
});
|
|
35
|
+
const nextTables = response.items ?? [];
|
|
36
|
+
setTables(nextTables);
|
|
37
|
+
setTotal(nextTables.length);
|
|
38
|
+
setNextPageToken(response.next_page_token ?? null);
|
|
39
|
+
return nextTables;
|
|
40
|
+
}
|
|
41
|
+
catch (refreshError) {
|
|
42
|
+
const normalized = normalizeError(refreshError, "Failed to load tables.");
|
|
43
|
+
setError(normalized);
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
setIsLoading(false);
|
|
48
|
+
}
|
|
49
|
+
}, [client, enabled, limit, pageToken, podId]);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!enabled) {
|
|
52
|
+
setTables([]);
|
|
53
|
+
setTotal(0);
|
|
54
|
+
setNextPageToken(null);
|
|
55
|
+
setError(null);
|
|
56
|
+
setIsLoading(false);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!autoLoad)
|
|
60
|
+
return;
|
|
61
|
+
void refresh();
|
|
62
|
+
}, [autoLoad, enabled, refresh]);
|
|
63
|
+
return useMemo(() => ({
|
|
64
|
+
tables,
|
|
65
|
+
total,
|
|
66
|
+
nextPageToken,
|
|
67
|
+
isLoading,
|
|
68
|
+
error,
|
|
69
|
+
refresh,
|
|
70
|
+
}), [error, isLoading, nextPageToken, refresh, tables, total]);
|
|
71
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { LemmaClient } from "../client.js";
|
|
2
|
+
import type { RecordResponse } from "../types.js";
|
|
3
|
+
export interface UseUpdateRecordOptions {
|
|
4
|
+
client: LemmaClient;
|
|
5
|
+
podId?: string;
|
|
6
|
+
tableName: string;
|
|
7
|
+
recordId?: string | null;
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
onSuccess?: (record: Record<string, unknown>, response: RecordResponse) => void;
|
|
10
|
+
onError?: (error: unknown) => void;
|
|
11
|
+
}
|
|
12
|
+
export interface UseUpdateRecordResult<TRecord extends Record<string, unknown> = Record<string, unknown>> {
|
|
13
|
+
updatedRecord: TRecord | null;
|
|
14
|
+
isSubmitting: boolean;
|
|
15
|
+
error: Error | null;
|
|
16
|
+
update: (data: Record<string, unknown>, overrides?: {
|
|
17
|
+
recordId?: string | null;
|
|
18
|
+
}) => Promise<TRecord | null>;
|
|
19
|
+
reset: () => void;
|
|
20
|
+
}
|
|
21
|
+
export declare function useUpdateRecord<TRecord extends Record<string, unknown> = Record<string, unknown>>({ client, podId, tableName, recordId, enabled, onSuccess, onError, }: UseUpdateRecordOptions): UseUpdateRecordResult<TRecord>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useCallback, 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
|
+
export function useUpdateRecord({ client, podId, tableName, recordId = null, enabled = true, onSuccess, onError, }) {
|
|
13
|
+
const [updatedRecord, setUpdatedRecord] = useState(null);
|
|
14
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
15
|
+
const [error, setError] = useState(null);
|
|
16
|
+
const trimmedTableName = tableName.trim();
|
|
17
|
+
const trimmedRecordId = typeof recordId === "string" ? recordId.trim() : "";
|
|
18
|
+
const isEnabled = enabled && trimmedTableName.length > 0;
|
|
19
|
+
const update = useCallback(async (data, overrides = {}) => {
|
|
20
|
+
const nextRecordId = typeof overrides.recordId === "string"
|
|
21
|
+
? overrides.recordId.trim()
|
|
22
|
+
: trimmedRecordId;
|
|
23
|
+
if (!isEnabled || nextRecordId.length === 0) {
|
|
24
|
+
const disabledError = new Error("Record update requires a table and record ID.");
|
|
25
|
+
setError(disabledError);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
setIsSubmitting(true);
|
|
29
|
+
setError(null);
|
|
30
|
+
try {
|
|
31
|
+
const scopedClient = resolvePodClient(client, podId);
|
|
32
|
+
const response = await scopedClient.records.update(trimmedTableName, nextRecordId, data);
|
|
33
|
+
const nextRecord = (response.data ?? null);
|
|
34
|
+
setUpdatedRecord(nextRecord);
|
|
35
|
+
if (nextRecord) {
|
|
36
|
+
onSuccess?.(nextRecord, response);
|
|
37
|
+
}
|
|
38
|
+
return nextRecord;
|
|
39
|
+
}
|
|
40
|
+
catch (mutationError) {
|
|
41
|
+
const normalized = normalizeError(mutationError, "Failed to update record.");
|
|
42
|
+
setError(normalized);
|
|
43
|
+
onError?.(mutationError);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
setIsSubmitting(false);
|
|
48
|
+
}
|
|
49
|
+
}, [client, isEnabled, onError, onSuccess, podId, trimmedRecordId, trimmedTableName]);
|
|
50
|
+
const reset = useCallback(() => {
|
|
51
|
+
setUpdatedRecord(null);
|
|
52
|
+
setError(null);
|
|
53
|
+
setIsSubmitting(false);
|
|
54
|
+
}, []);
|
|
55
|
+
return useMemo(() => ({
|
|
56
|
+
updatedRecord,
|
|
57
|
+
isSubmitting,
|
|
58
|
+
error,
|
|
59
|
+
update,
|
|
60
|
+
reset,
|
|
61
|
+
}), [error, isSubmitting, reset, update, updatedRecord]);
|
|
62
|
+
}
|