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,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
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { LemmaClient } from "../client.js";
|
|
2
|
+
import type { FlowRun, FlowStartType, Workflow, WorkflowRunInputs } from "../types.js";
|
|
3
|
+
import { type UseFlowSessionResult } from "./useFlowSession.js";
|
|
4
|
+
export interface UseWorkflowStartOptions {
|
|
5
|
+
client: LemmaClient;
|
|
6
|
+
podId?: string;
|
|
7
|
+
workflowName: string;
|
|
8
|
+
runId?: string | null;
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
autoLoad?: boolean;
|
|
11
|
+
autoPoll?: boolean;
|
|
12
|
+
pollIntervalMs?: number;
|
|
13
|
+
onRun?: (run: FlowRun) => void;
|
|
14
|
+
onError?: (error: unknown) => void;
|
|
15
|
+
}
|
|
16
|
+
export interface UseWorkflowStartResult extends Omit<UseFlowSessionResult, "start" | "listHistory"> {
|
|
17
|
+
workflow: Workflow | null;
|
|
18
|
+
startType: FlowStartType | "MANUAL";
|
|
19
|
+
inputSchema: Record<string, unknown> | null;
|
|
20
|
+
inputUiSchema: Record<string, unknown> | null;
|
|
21
|
+
isLoadingWorkflow: boolean;
|
|
22
|
+
isStarting: boolean;
|
|
23
|
+
error: Error | null;
|
|
24
|
+
refreshWorkflow: () => Promise<Workflow | null>;
|
|
25
|
+
listHistory: (options?: {
|
|
26
|
+
limit?: number;
|
|
27
|
+
pageToken?: string;
|
|
28
|
+
}) => Promise<FlowRun[]>;
|
|
29
|
+
start: (inputs?: WorkflowRunInputs, options?: {
|
|
30
|
+
forceResume?: boolean;
|
|
31
|
+
}) => Promise<FlowRun>;
|
|
32
|
+
}
|
|
33
|
+
export declare function useWorkflowStart({ client, podId, workflowName, runId, enabled, autoLoad, autoPoll, pollIntervalMs, onRun, onError, }: UseWorkflowStartOptions): UseWorkflowStartResult;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { normalizeRunStatus } from "../run-utils.js";
|
|
3
|
+
import { useFlowSession, } from "./useFlowSession.js";
|
|
4
|
+
function normalizeError(error, fallback) {
|
|
5
|
+
if (error instanceof Error)
|
|
6
|
+
return error;
|
|
7
|
+
return new Error(fallback);
|
|
8
|
+
}
|
|
9
|
+
function resolvePodClient(client, podId) {
|
|
10
|
+
if (!podId || podId === client.podId)
|
|
11
|
+
return client;
|
|
12
|
+
return client.withPod(podId);
|
|
13
|
+
}
|
|
14
|
+
function findFirstFormNode(workflow) {
|
|
15
|
+
if (!workflow?.nodes?.length)
|
|
16
|
+
return null;
|
|
17
|
+
for (const node of workflow.nodes) {
|
|
18
|
+
if (!node || typeof node !== "object")
|
|
19
|
+
continue;
|
|
20
|
+
if (!("config" in node) || typeof node.config !== "object" || !node.config)
|
|
21
|
+
continue;
|
|
22
|
+
if ("input_schema" in node.config) {
|
|
23
|
+
return node;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function isWaitingFlowRun(run) {
|
|
29
|
+
if (!run)
|
|
30
|
+
return false;
|
|
31
|
+
const normalizedStatus = normalizeRunStatus(run.status);
|
|
32
|
+
return normalizedStatus === "WAITING"
|
|
33
|
+
|| !!run.waiting_function_run_id
|
|
34
|
+
|| !!run.waiting_task_id
|
|
35
|
+
|| !!run.waiting_trigger_id;
|
|
36
|
+
}
|
|
37
|
+
export function useWorkflowStart({ client, podId, workflowName, runId = null, enabled = true, autoLoad = true, autoPoll = true, pollIntervalMs = 2000, onRun, onError, }) {
|
|
38
|
+
const [workflow, setWorkflow] = useState(null);
|
|
39
|
+
const [workflowError, setWorkflowError] = useState(null);
|
|
40
|
+
const [isLoadingWorkflow, setIsLoadingWorkflow] = useState(false);
|
|
41
|
+
const [isStarting, setIsStarting] = useState(false);
|
|
42
|
+
const hasWorkflowName = workflowName.trim().length > 0;
|
|
43
|
+
const isEnabled = enabled && hasWorkflowName;
|
|
44
|
+
const session = useFlowSession({
|
|
45
|
+
client,
|
|
46
|
+
podId,
|
|
47
|
+
flowName: workflowName,
|
|
48
|
+
runId,
|
|
49
|
+
autoPoll,
|
|
50
|
+
pollIntervalMs,
|
|
51
|
+
onRun,
|
|
52
|
+
onError,
|
|
53
|
+
});
|
|
54
|
+
const refreshWorkflow = useCallback(async () => {
|
|
55
|
+
if (!isEnabled) {
|
|
56
|
+
setWorkflow(null);
|
|
57
|
+
setWorkflowError(null);
|
|
58
|
+
setIsLoadingWorkflow(false);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
setIsLoadingWorkflow(true);
|
|
62
|
+
setWorkflowError(null);
|
|
63
|
+
try {
|
|
64
|
+
const scopedClient = resolvePodClient(client, podId);
|
|
65
|
+
const nextWorkflow = await scopedClient.workflows.get(workflowName);
|
|
66
|
+
setWorkflow(nextWorkflow);
|
|
67
|
+
return nextWorkflow;
|
|
68
|
+
}
|
|
69
|
+
catch (refreshError) {
|
|
70
|
+
const normalized = normalizeError(refreshError, "Failed to fetch workflow definition.");
|
|
71
|
+
setWorkflowError(normalized);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
setIsLoadingWorkflow(false);
|
|
76
|
+
}
|
|
77
|
+
}, [client, isEnabled, podId, workflowName]);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!isEnabled) {
|
|
80
|
+
setWorkflow(null);
|
|
81
|
+
setWorkflowError(null);
|
|
82
|
+
setIsLoadingWorkflow(false);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!autoLoad)
|
|
86
|
+
return;
|
|
87
|
+
void refreshWorkflow();
|
|
88
|
+
}, [autoLoad, isEnabled, refreshWorkflow]);
|
|
89
|
+
const listHistory = useCallback((options = {}) => {
|
|
90
|
+
return session.listHistory({
|
|
91
|
+
flowName: workflowName,
|
|
92
|
+
limit: options.limit,
|
|
93
|
+
pageToken: options.pageToken,
|
|
94
|
+
});
|
|
95
|
+
}, [session, workflowName]);
|
|
96
|
+
const start = useCallback(async (inputs = {}, options = {}) => {
|
|
97
|
+
if (!hasWorkflowName) {
|
|
98
|
+
const missingWorkflowError = new Error("workflowName is required.");
|
|
99
|
+
setWorkflowError(missingWorkflowError);
|
|
100
|
+
throw missingWorkflowError;
|
|
101
|
+
}
|
|
102
|
+
setIsStarting(true);
|
|
103
|
+
setWorkflowError(null);
|
|
104
|
+
try {
|
|
105
|
+
const currentWorkflow = workflow?.name === workflowName
|
|
106
|
+
? workflow
|
|
107
|
+
: await refreshWorkflow();
|
|
108
|
+
const startType = currentWorkflow?.start?.type ?? "MANUAL";
|
|
109
|
+
if (startType === "MANUAL") {
|
|
110
|
+
const created = await session.start({
|
|
111
|
+
flowName: workflowName,
|
|
112
|
+
inputs: {},
|
|
113
|
+
});
|
|
114
|
+
const shouldResume = !!created.id && (options.forceResume === true
|
|
115
|
+
|| Object.keys(inputs).length > 0
|
|
116
|
+
|| isWaitingFlowRun(created));
|
|
117
|
+
if (!shouldResume || !created.id) {
|
|
118
|
+
return created;
|
|
119
|
+
}
|
|
120
|
+
return session.resume({
|
|
121
|
+
runId: created.id,
|
|
122
|
+
inputs,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return session.start({
|
|
126
|
+
flowName: workflowName,
|
|
127
|
+
inputs,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (startError) {
|
|
131
|
+
const normalized = normalizeError(startError, "Failed to start workflow.");
|
|
132
|
+
setWorkflowError(normalized);
|
|
133
|
+
throw normalized;
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
setIsStarting(false);
|
|
137
|
+
}
|
|
138
|
+
}, [hasWorkflowName, refreshWorkflow, session, workflow, workflowName]);
|
|
139
|
+
const formNode = findFirstFormNode(workflow);
|
|
140
|
+
const startType = workflow?.start?.type ?? "MANUAL";
|
|
141
|
+
const error = workflowError ?? session.error;
|
|
142
|
+
return {
|
|
143
|
+
...session,
|
|
144
|
+
workflow,
|
|
145
|
+
startType,
|
|
146
|
+
inputSchema: formNode?.config.input_schema ?? null,
|
|
147
|
+
inputUiSchema: formNode?.config.ui_schema ?? null,
|
|
148
|
+
isLoadingWorkflow,
|
|
149
|
+
isStarting,
|
|
150
|
+
error,
|
|
151
|
+
refreshWorkflow,
|
|
152
|
+
listHistory,
|
|
153
|
+
start,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ForeignKeyReference } from "./datastore-query.js";
|
|
2
|
+
import { type ColumnSchema, type Table } from "./types.js";
|
|
3
|
+
export type RecordSchemaFieldKind = "text" | "textarea" | "number" | "boolean" | "json" | "date" | "datetime" | "select" | "foreign-key" | "uuid";
|
|
4
|
+
export interface RecordSchemaField {
|
|
5
|
+
name: string;
|
|
6
|
+
label: string;
|
|
7
|
+
kind: RecordSchemaFieldKind;
|
|
8
|
+
column: ColumnSchema;
|
|
9
|
+
required: boolean;
|
|
10
|
+
readOnly: boolean;
|
|
11
|
+
system: boolean;
|
|
12
|
+
computed: boolean;
|
|
13
|
+
auto: boolean;
|
|
14
|
+
options: string[];
|
|
15
|
+
foreignKey: ForeignKeyReference | null;
|
|
16
|
+
}
|
|
17
|
+
export interface BuildRecordPayloadOptions {
|
|
18
|
+
mode?: "create" | "update";
|
|
19
|
+
}
|
|
20
|
+
export interface BuildRecordPayloadResult {
|
|
21
|
+
data: Record<string, unknown>;
|
|
22
|
+
errors: Record<string, string>;
|
|
23
|
+
isValid: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function getRecordFieldKind(column: ColumnSchema): RecordSchemaFieldKind;
|
|
26
|
+
export declare function buildRecordSchemaFields(table: Table): RecordSchemaField[];
|
|
27
|
+
export declare function getEditableRecordFields(table: Table): RecordSchemaField[];
|
|
28
|
+
export declare function formatRecordValueForForm(column: ColumnSchema, value: unknown): unknown;
|
|
29
|
+
export declare function buildRecordFormValues(table: Table, values?: Record<string, unknown>): Record<string, unknown>;
|
|
30
|
+
export declare function buildRecordPayload(table: Table, values: Record<string, unknown>, options?: BuildRecordPayloadOptions): BuildRecordPayloadResult;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { parseForeignKeyReference } from "./datastore-query.js";
|
|
2
|
+
import { DatastoreDataType } from "./types.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 isBlank(value) {
|
|
11
|
+
return value === null || typeof value === "undefined" || (typeof value === "string" && value.trim() === "");
|
|
12
|
+
}
|
|
13
|
+
function isIntegerLike(value) {
|
|
14
|
+
return Number.isInteger(value);
|
|
15
|
+
}
|
|
16
|
+
function getRawColumnDefault(column) {
|
|
17
|
+
return column.default;
|
|
18
|
+
}
|
|
19
|
+
function shouldUseTextarea(column) {
|
|
20
|
+
if (typeof column.max_length === "number" && column.max_length >= 240) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return /(description|content|body|note|instruction|summary|message)/i.test(column.name);
|
|
24
|
+
}
|
|
25
|
+
function toDateInputValue(value) {
|
|
26
|
+
if (typeof value === "string") {
|
|
27
|
+
return value.includes("T") ? value.slice(0, 10) : value;
|
|
28
|
+
}
|
|
29
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
30
|
+
return value.toISOString().slice(0, 10);
|
|
31
|
+
}
|
|
32
|
+
return String(value);
|
|
33
|
+
}
|
|
34
|
+
function toDateTimeLocalInputValue(value) {
|
|
35
|
+
if (typeof value === "string") {
|
|
36
|
+
const timestamp = new Date(value);
|
|
37
|
+
if (!Number.isNaN(timestamp.getTime())) {
|
|
38
|
+
const pad = (part) => String(part).padStart(2, "0");
|
|
39
|
+
return [
|
|
40
|
+
timestamp.getFullYear(),
|
|
41
|
+
pad(timestamp.getMonth() + 1),
|
|
42
|
+
pad(timestamp.getDate()),
|
|
43
|
+
].join("-") + `T${pad(timestamp.getHours())}:${pad(timestamp.getMinutes())}`;
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
48
|
+
const pad = (part) => String(part).padStart(2, "0");
|
|
49
|
+
return [
|
|
50
|
+
value.getFullYear(),
|
|
51
|
+
pad(value.getMonth() + 1),
|
|
52
|
+
pad(value.getDate()),
|
|
53
|
+
].join("-") + `T${pad(value.getHours())}:${pad(value.getMinutes())}`;
|
|
54
|
+
}
|
|
55
|
+
return String(value);
|
|
56
|
+
}
|
|
57
|
+
export function getRecordFieldKind(column) {
|
|
58
|
+
if (column.foreign_key?.references)
|
|
59
|
+
return "foreign-key";
|
|
60
|
+
if (column.type === DatastoreDataType.ENUM)
|
|
61
|
+
return "select";
|
|
62
|
+
if (column.type === DatastoreDataType.BOOLEAN)
|
|
63
|
+
return "boolean";
|
|
64
|
+
if (column.type === DatastoreDataType.JSON || column.type === DatastoreDataType.VECTOR)
|
|
65
|
+
return "json";
|
|
66
|
+
if (column.type === DatastoreDataType.INTEGER
|
|
67
|
+
|| column.type === DatastoreDataType.FLOAT
|
|
68
|
+
|| column.type === DatastoreDataType.SERIAL) {
|
|
69
|
+
return "number";
|
|
70
|
+
}
|
|
71
|
+
if (column.type === DatastoreDataType.DATE)
|
|
72
|
+
return "date";
|
|
73
|
+
if (column.type === DatastoreDataType.DATETIME)
|
|
74
|
+
return "datetime";
|
|
75
|
+
if (column.type === DatastoreDataType.UUID)
|
|
76
|
+
return "uuid";
|
|
77
|
+
if (column.type === DatastoreDataType.TEXT && shouldUseTextarea(column))
|
|
78
|
+
return "textarea";
|
|
79
|
+
return "text";
|
|
80
|
+
}
|
|
81
|
+
export function buildRecordSchemaFields(table) {
|
|
82
|
+
return table.columns.map((column) => {
|
|
83
|
+
const foreignKey = column.foreign_key?.references
|
|
84
|
+
? parseForeignKeyReference(column.foreign_key.references)
|
|
85
|
+
: null;
|
|
86
|
+
return {
|
|
87
|
+
name: column.name,
|
|
88
|
+
label: sentenceCase(column.name),
|
|
89
|
+
kind: getRecordFieldKind(column),
|
|
90
|
+
column,
|
|
91
|
+
required: column.required === true,
|
|
92
|
+
readOnly: column.system === true || column.computed === true || column.auto === true,
|
|
93
|
+
system: column.system === true,
|
|
94
|
+
computed: column.computed === true,
|
|
95
|
+
auto: column.auto === true,
|
|
96
|
+
options: column.options ?? [],
|
|
97
|
+
foreignKey,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export function getEditableRecordFields(table) {
|
|
102
|
+
return buildRecordSchemaFields(table).filter((field) => !field.readOnly);
|
|
103
|
+
}
|
|
104
|
+
export function formatRecordValueForForm(column, value) {
|
|
105
|
+
const kind = getRecordFieldKind(column);
|
|
106
|
+
if (value === null || typeof value === "undefined") {
|
|
107
|
+
if (kind === "boolean")
|
|
108
|
+
return false;
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
if (kind === "boolean") {
|
|
112
|
+
return Boolean(value);
|
|
113
|
+
}
|
|
114
|
+
if (kind === "json") {
|
|
115
|
+
if (typeof value === "string")
|
|
116
|
+
return value;
|
|
117
|
+
try {
|
|
118
|
+
return JSON.stringify(value, null, 2);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return String(value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (kind === "date") {
|
|
125
|
+
return toDateInputValue(value);
|
|
126
|
+
}
|
|
127
|
+
if (kind === "datetime") {
|
|
128
|
+
return toDateTimeLocalInputValue(value);
|
|
129
|
+
}
|
|
130
|
+
if (kind === "number") {
|
|
131
|
+
return String(value);
|
|
132
|
+
}
|
|
133
|
+
return String(value);
|
|
134
|
+
}
|
|
135
|
+
export function buildRecordFormValues(table, values = {}) {
|
|
136
|
+
const next = {};
|
|
137
|
+
buildRecordSchemaFields(table).forEach((field) => {
|
|
138
|
+
const provided = values[field.name];
|
|
139
|
+
const rawDefault = getRawColumnDefault(field.column);
|
|
140
|
+
const source = typeof provided !== "undefined" ? provided : rawDefault;
|
|
141
|
+
next[field.name] = formatRecordValueForForm(field.column, source);
|
|
142
|
+
});
|
|
143
|
+
return next;
|
|
144
|
+
}
|
|
145
|
+
function coerceEmptyValue(mode, name, target) {
|
|
146
|
+
if (mode === "update") {
|
|
147
|
+
target[name] = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function buildRecordPayload(table, values, options = {}) {
|
|
151
|
+
const mode = options.mode ?? "create";
|
|
152
|
+
const data = {};
|
|
153
|
+
const errors = {};
|
|
154
|
+
getEditableRecordFields(table).forEach((field) => {
|
|
155
|
+
const rawValue = values[field.name];
|
|
156
|
+
const value = typeof rawValue === "string" ? rawValue : rawValue;
|
|
157
|
+
if (field.kind === "boolean") {
|
|
158
|
+
data[field.name] = Boolean(rawValue);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (isBlank(value)) {
|
|
162
|
+
if (field.required) {
|
|
163
|
+
errors[field.name] = `${field.label} is required.`;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
coerceEmptyValue(mode, field.name, data);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (field.kind === "number") {
|
|
170
|
+
const parsed = Number(value);
|
|
171
|
+
if (!Number.isFinite(parsed)) {
|
|
172
|
+
errors[field.name] = `${field.label} must be a valid number.`;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if ((field.column.type === DatastoreDataType.INTEGER || field.column.type === DatastoreDataType.SERIAL)
|
|
176
|
+
&& !isIntegerLike(parsed)) {
|
|
177
|
+
errors[field.name] = `${field.label} must be a whole number.`;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
data[field.name] = parsed;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (field.kind === "json") {
|
|
184
|
+
try {
|
|
185
|
+
data[field.name] = typeof value === "string" ? JSON.parse(value) : value;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
errors[field.name] = `${field.label} must be valid JSON.`;
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
data[field.name] = typeof value === "string" ? value : String(value);
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
data,
|
|
196
|
+
errors,
|
|
197
|
+
isValid: Object.keys(errors).length === 0,
|
|
198
|
+
};
|
|
199
|
+
}
|