lemma-sdk 0.2.31 → 0.2.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/react/index.d.ts +3 -1
- package/dist/react/index.js +1 -0
- package/dist/react/useCreateRecord.d.ts +33 -3
- package/dist/react/useCreateRecord.js +12 -2
- package/dist/react/useForeignKeyOptions.d.ts +18 -0
- package/dist/react/useFunctionRun.d.ts +17 -0
- package/dist/react/useJoinedRecords.d.ts +57 -2
- package/dist/react/useJoinedRecords.js +54 -5
- package/dist/react/useRecord.d.ts +16 -0
- package/dist/react/useRecordForm.d.ts +42 -3
- package/dist/react/useRecordForm.js +43 -6
- package/dist/react/useReferencingRecords.d.ts +66 -0
- package/dist/react/useReferencingRecords.js +159 -0
- package/dist/react/useRelatedRecords.d.ts +17 -0
- package/dist/react/useReverseRelatedRecords.d.ts +21 -0
- package/dist/react/useUpdateRecord.d.ts +34 -3
- package/dist/react/useUpdateRecord.js +13 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,9 +74,9 @@ import {
|
|
|
74
74
|
| Area | Hooks | Stability | Use when |
|
|
75
75
|
| --- | --- | --- | --- |
|
|
76
76
|
| Auth | `AuthGuard`, `useAuth`, `useCurrentUser`, `usePodAccess` | Stable | Gate an app, read signed-in user state, or request pod access. |
|
|
77
|
-
| Tables | `useTables`, `useTable`, `useRecords`, `useRecord`, `useJoinedRecords`, `useRelatedRecords`, `useReverseRelatedRecords` | Stable | Build custom table browsers, details views, related-record views, and relational reads. |
|
|
78
|
-
| Record mutations | `useCreateRecord`, `useUpdateRecord`, `useDeleteRecord`, `useBulkRecords` | Stable | Create, update, delete, or bulk-delete rows from headless UI. |
|
|
79
|
-
| Record forms | `useRecordSchema`, `useRecordForm`, `useForeignKeyOptions`, `useSchemaForm` | Stable | Render schema-driven forms, enum fields, and foreign-key selectors. |
|
|
77
|
+
| Tables | `useTables`, `useTable`, `useRecords`, `useRecord`, `useJoinedRecords`, `useRelatedRecords`, `useReverseRelatedRecords`, `useReferencingRecords` | Stable | Build custom table browsers, details views, related-record views, and relational reads. |
|
|
78
|
+
| Record mutations | `useCreateRecord`, `useUpdateRecord`, `useDeleteRecord`, `useBulkRecords` | Stable | Create, update, delete, or bulk-delete rows from headless UI. Function-backed mutations via `createVia`/`updateVia` options. |
|
|
79
|
+
| Record forms | `useRecordSchema`, `useRecordForm`, `useForeignKeyOptions`, `useSchemaForm` | Stable | Render schema-driven forms, enum fields, and foreign-key selectors. `useRecordForm` supports `submitVia: "function"`, `visibleFields`, `hiddenFields`. |
|
|
80
80
|
| Assistant | `useConversations`, `useConversation`, `useConversationMessages`, `useAssistantRun`, `useAssistantSession`, `useAssistantRuntime`, `useAssistantController` | Stable except controller/runtime | Build custom chat, conversation lists, streaming output, and final-output views. |
|
|
81
81
|
| Agents | `useAgentRun`, `useAgentRuns`, `useAgentInputSchema`, `useTaskSession` | Stable except raw session | Start agent tasks, submit follow-up input, read task history, and inspect input/output schemas. |
|
|
82
82
|
| Workflows | `useWorkflowStart`, `useWorkflowRun`, `useWorkflowRuns`, `useWorkflowResume` | Stable | Start, poll, resume, cancel, retry, and inspect workflow runs. |
|
|
@@ -86,6 +86,8 @@ import {
|
|
|
86
86
|
|
|
87
87
|
### Common Hook Shapes
|
|
88
88
|
|
|
89
|
+
For business-facing examples and a decision guide mapping "I want to..." to the right hook, see [docs/hooks-guide.md](docs/hooks-guide.md).
|
|
90
|
+
|
|
89
91
|
List hooks generally expose:
|
|
90
92
|
|
|
91
93
|
- `items` named for the resource, such as `records`, `runs`, or `members`
|
package/dist/react/index.d.ts
CHANGED
|
@@ -45,11 +45,13 @@ export type { UseDeleteRecordOptions, UseDeleteRecordResult } from "./useDeleteR
|
|
|
45
45
|
export { useBulkRecords } from "./useBulkRecords.js";
|
|
46
46
|
export type { UseBulkRecordsOptions, UseBulkRecordsResult } from "./useBulkRecords.js";
|
|
47
47
|
export { useJoinedRecords } from "./useJoinedRecords.js";
|
|
48
|
-
export type { UseJoinedRecordsOptions, UseJoinedRecordsResult } from "./useJoinedRecords.js";
|
|
48
|
+
export type { JoinedRecordsShorthandJoin, UseJoinedRecordsOptions, UseJoinedRecordsResult, } from "./useJoinedRecords.js";
|
|
49
49
|
export { useRelatedRecords } from "./useRelatedRecords.js";
|
|
50
50
|
export type { RelatedRecordsColumn, RelatedRecordsInclude, RelatedRecordsResolvedInclude, UseRelatedRecordsOptions, UseRelatedRecordsResult, } from "./useRelatedRecords.js";
|
|
51
51
|
export { useReverseRelatedRecords } from "./useReverseRelatedRecords.js";
|
|
52
52
|
export type { ReverseRelatedRecordsColumn, ReverseRelatedRelation, ReverseRelationSelector, UseReverseRelatedRecordsOptions, UseReverseRelatedRecordsResult, } from "./useReverseRelatedRecords.js";
|
|
53
|
+
export { useReferencingRecords } from "./useReferencingRecords.js";
|
|
54
|
+
export type { ReferencingRecordsColumn, UseReferencingRecordsOptions, UseReferencingRecordsResult, } from "./useReferencingRecords.js";
|
|
53
55
|
export { useForeignKeyOptions } from "./useForeignKeyOptions.js";
|
|
54
56
|
export type { ForeignKeyOption, UseForeignKeyOptionsOptions, UseForeignKeyOptionsResult, } from "./useForeignKeyOptions.js";
|
|
55
57
|
export { useRecordSchema } from "./useRecordSchema.js";
|
package/dist/react/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export { useBulkRecords } from "./useBulkRecords.js";
|
|
|
24
24
|
export { useJoinedRecords } from "./useJoinedRecords.js";
|
|
25
25
|
export { useRelatedRecords } from "./useRelatedRecords.js";
|
|
26
26
|
export { useReverseRelatedRecords } from "./useReverseRelatedRecords.js";
|
|
27
|
+
export { useReferencingRecords } from "./useReferencingRecords.js";
|
|
27
28
|
export { useForeignKeyOptions } from "./useForeignKeyOptions.js";
|
|
28
29
|
export { useRecordSchema } from "./useRecordSchema.js";
|
|
29
30
|
export { useRecordForm } from "./useRecordForm.js";
|
|
@@ -1,11 +1,41 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
|
-
import type { RecordResponse } from "../types.js";
|
|
2
|
+
import type { FunctionRun, RecordResponse } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* React hook for creating a single record. Manages loading/error state and
|
|
5
|
+
* exposes a `create` function you can call from event handlers.
|
|
6
|
+
*
|
|
7
|
+
* Supports two modes:
|
|
8
|
+
* - `"direct"` (default): calls `records.create` directly.
|
|
9
|
+
* - `"function"`: calls `functions.runs.create`, routing the create through
|
|
10
|
+
* a pod function that may enforce business logic.
|
|
11
|
+
*
|
|
12
|
+
* @example Direct create
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const { create, isSubmitting } = useCreateRecord({ client, tableName: "comments" });
|
|
15
|
+
* await create({ body: "Hello", issue_id: "123" });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @example Function-backed create
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const { create, isSubmitting } = useCreateRecord({
|
|
21
|
+
* client,
|
|
22
|
+
* tableName: "issues",
|
|
23
|
+
* createVia: "function",
|
|
24
|
+
* createFunctionName: "create-issue",
|
|
25
|
+
* });
|
|
26
|
+
* await create({ title: "Bug", team_id: "team_1" });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
3
29
|
export interface UseCreateRecordOptions {
|
|
4
30
|
client: LemmaClient;
|
|
5
31
|
podId?: string;
|
|
6
32
|
tableName: string;
|
|
7
33
|
enabled?: boolean;
|
|
8
|
-
|
|
34
|
+
/** How the record is created. `"direct"` calls `records.create`. `"function"` calls `functions.runs.create`. */
|
|
35
|
+
createVia?: "direct" | "function";
|
|
36
|
+
/** Function name to run when `createVia` is `"function"`. Falls back to `tableName` if omitted. */
|
|
37
|
+
createFunctionName?: string;
|
|
38
|
+
onSuccess?: (record: Record<string, unknown>, response: RecordResponse | FunctionRun) => void;
|
|
9
39
|
onError?: (error: unknown) => void;
|
|
10
40
|
}
|
|
11
41
|
export interface UseCreateRecordResult<TRecord extends Record<string, unknown> = Record<string, unknown>> {
|
|
@@ -15,4 +45,4 @@ export interface UseCreateRecordResult<TRecord extends Record<string, unknown> =
|
|
|
15
45
|
create: (data: Record<string, unknown>) => Promise<TRecord | null>;
|
|
16
46
|
reset: () => void;
|
|
17
47
|
}
|
|
18
|
-
export declare function useCreateRecord<TRecord extends Record<string, unknown> = Record<string, unknown>>({ client, podId, tableName, enabled, onSuccess, onError, }: UseCreateRecordOptions): UseCreateRecordResult<TRecord>;
|
|
48
|
+
export declare function useCreateRecord<TRecord extends Record<string, unknown> = Record<string, unknown>>({ client, podId, tableName, enabled, createVia, createFunctionName, onSuccess, onError, }: UseCreateRecordOptions): UseCreateRecordResult<TRecord>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { normalizeError, resolvePodClient } from "./utils.js";
|
|
3
|
-
export function useCreateRecord({ client, podId, tableName, enabled = true, onSuccess, onError, }) {
|
|
3
|
+
export function useCreateRecord({ client, podId, tableName, enabled = true, createVia = "direct", createFunctionName, onSuccess, onError, }) {
|
|
4
4
|
const [createdRecord, setCreatedRecord] = useState(null);
|
|
5
5
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
6
6
|
const [error, setError] = useState(null);
|
|
@@ -18,6 +18,16 @@ export function useCreateRecord({ client, podId, tableName, enabled = true, onSu
|
|
|
18
18
|
setError(null);
|
|
19
19
|
try {
|
|
20
20
|
const scopedClient = resolvePodClient(client, podId);
|
|
21
|
+
if (createVia === "function") {
|
|
22
|
+
const functionName = createFunctionName ?? trimmedTableName;
|
|
23
|
+
const run = await scopedClient.functions.runs.create(functionName, { input: data });
|
|
24
|
+
const nextRecord = (run.output_data ?? { id: run.id, ...data });
|
|
25
|
+
setCreatedRecord(nextRecord);
|
|
26
|
+
if (nextRecord) {
|
|
27
|
+
onSuccessRef.current?.(nextRecord, run);
|
|
28
|
+
}
|
|
29
|
+
return nextRecord;
|
|
30
|
+
}
|
|
21
31
|
const response = await scopedClient.records.create(trimmedTableName, data);
|
|
22
32
|
const nextRecord = (response.data ?? null);
|
|
23
33
|
setCreatedRecord(nextRecord);
|
|
@@ -35,7 +45,7 @@ export function useCreateRecord({ client, podId, tableName, enabled = true, onSu
|
|
|
35
45
|
finally {
|
|
36
46
|
setIsSubmitting(false);
|
|
37
47
|
}
|
|
38
|
-
}, [client, isEnabled, podId, trimmedTableName]);
|
|
48
|
+
}, [client, createFunctionName, createVia, isEnabled, podId, trimmedTableName]);
|
|
39
49
|
const reset = useCallback(() => {
|
|
40
50
|
setCreatedRecord(null);
|
|
41
51
|
setError(null);
|
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
2
|
import { type ForeignKeyReference } from "../datastore-query.js";
|
|
3
3
|
import type { ColumnSchema, Table } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* React hook that resolves a foreign-key column into dropdown options.
|
|
6
|
+
* Reads the FK metadata from the table schema, fetches records from the
|
|
7
|
+
* referenced table, and returns `{ value, label, record }` options ready
|
|
8
|
+
* for `<Select>` components.
|
|
9
|
+
*
|
|
10
|
+
* Auto-detects the best label field (name > title > label > email > slug).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const { options, isLoading } = useForeignKeyOptions({
|
|
15
|
+
* client,
|
|
16
|
+
* tableName: "issues",
|
|
17
|
+
* columnName: "team_id",
|
|
18
|
+
* });
|
|
19
|
+
* // options = [{ value: "team_1", label: "Engineering", record: {...} }, ...]
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
4
22
|
export interface ForeignKeyOption {
|
|
5
23
|
value: unknown;
|
|
6
24
|
label: string;
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import type { FunctionRun } from "../types.js";
|
|
2
2
|
import { type UseFunctionSessionOptions, type UseFunctionSessionResult } from "./useFunctionSession.js";
|
|
3
|
+
/**
|
|
4
|
+
* React hook for running a pod function and tracking its output.
|
|
5
|
+
* Wraps `useFunctionSession` with a simpler API — call `start(input)`
|
|
6
|
+
* from an event handler, read `output` / `finalOutput`.
|
|
7
|
+
*
|
|
8
|
+
* @example Run a function on button click
|
|
9
|
+
* ```tsx
|
|
10
|
+
* const { start, output, isFinished, isPolling } = useFunctionRun({
|
|
11
|
+
* client,
|
|
12
|
+
* functionName: "update-issue-status",
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* <button onClick={() => start({ issue_id: "123", status: "closed" })}>
|
|
16
|
+
* Close issue
|
|
17
|
+
* </button>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
3
20
|
export interface UseFunctionRunOptions extends UseFunctionSessionOptions {
|
|
4
21
|
}
|
|
5
22
|
export interface UseFunctionRunResult extends Omit<UseFunctionSessionResult, "start" | "listHistory"> {
|
|
@@ -1,9 +1,64 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
2
|
import { type JoinedRecordsQueryDefinition } from "../datastore-query.js";
|
|
3
|
+
/**
|
|
4
|
+
* A simplified join descriptor. The hook auto-resolves the join condition
|
|
5
|
+
* from the foreign-key metadata on the base table's column named by `on`.
|
|
6
|
+
*/
|
|
7
|
+
export interface JoinedRecordsShorthandJoin {
|
|
8
|
+
/** The table to join into (e.g. "teams"). */
|
|
9
|
+
table: string;
|
|
10
|
+
/**
|
|
11
|
+
* The FK column on the base table that references the join table
|
|
12
|
+
* (e.g. "team_id"). The hook reads the FK metadata on this column
|
|
13
|
+
* to determine the join condition automatically.
|
|
14
|
+
*/
|
|
15
|
+
on: string;
|
|
16
|
+
/** Optional alias for the joined table. Defaults to the table name. */
|
|
17
|
+
alias?: string;
|
|
18
|
+
/** Join type. Defaults to "left". */
|
|
19
|
+
type?: "inner" | "left" | "left outer" | "right" | "right outer" | "full" | "full outer";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* React hook for cross-table join queries. Supports two API styles:
|
|
23
|
+
*
|
|
24
|
+
* 1. **Full query** via the `query` option — for complex joins with custom
|
|
25
|
+
* select lists, filters, and expressions.
|
|
26
|
+
*
|
|
27
|
+
* 2. **Shorthand** via `baseTable` + `joins` — the hook auto-resolves the
|
|
28
|
+
* join conditions from the schema's foreign-key metadata, so you don't
|
|
29
|
+
* need to spell out the `on` condition manually.
|
|
30
|
+
*
|
|
31
|
+
* @example Full query (existing API)
|
|
32
|
+
* ```tsx
|
|
33
|
+
* const { records, isLoading } = useJoinedRecords({
|
|
34
|
+
* client,
|
|
35
|
+
* query: {
|
|
36
|
+
* from: "issues",
|
|
37
|
+
* joins: [{ table: "teams", on: { left: "issues.team_id", right: "teams.id" } }],
|
|
38
|
+
* },
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @example Shorthand with FK auto-resolution
|
|
43
|
+
* ```tsx
|
|
44
|
+
* const { records, isLoading } = useJoinedRecords({
|
|
45
|
+
* client,
|
|
46
|
+
* baseTable: "issues",
|
|
47
|
+
* joins: [{ table: "teams", on: "team_id" }],
|
|
48
|
+
* });
|
|
49
|
+
* // The hook reads the FK metadata on issues.team_id to find that
|
|
50
|
+
* // it references teams.id, and builds the join automatically.
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
3
53
|
export interface UseJoinedRecordsOptions {
|
|
4
54
|
client: LemmaClient;
|
|
5
55
|
podId?: string;
|
|
6
|
-
query
|
|
56
|
+
/** Full join query definition. Mutually exclusive with `baseTable` + `joins`. */
|
|
57
|
+
query?: JoinedRecordsQueryDefinition;
|
|
58
|
+
/** Base table for shorthand mode. Mutually exclusive with `query`. */
|
|
59
|
+
baseTable?: string;
|
|
60
|
+
/** Shorthand join descriptors. Auto-resolves join conditions from FK metadata. */
|
|
61
|
+
joins?: JoinedRecordsShorthandJoin[];
|
|
7
62
|
enabled?: boolean;
|
|
8
63
|
autoLoad?: boolean;
|
|
9
64
|
}
|
|
@@ -15,4 +70,4 @@ export interface UseJoinedRecordsResult<TRecord extends Record<string, unknown>
|
|
|
15
70
|
error: Error | null;
|
|
16
71
|
refresh: () => Promise<TRecord[]>;
|
|
17
72
|
}
|
|
18
|
-
export declare function useJoinedRecords<TRecord extends Record<string, unknown> = Record<string, unknown>>({ client, podId, query, enabled, autoLoad, }: UseJoinedRecordsOptions): UseJoinedRecordsResult<TRecord>;
|
|
73
|
+
export declare function useJoinedRecords<TRecord extends Record<string, unknown> = Record<string, unknown>>({ client, podId, query, baseTable, joins: shorthandJoins, enabled, autoLoad, }: UseJoinedRecordsOptions): UseJoinedRecordsResult<TRecord>;
|
|
@@ -1,18 +1,51 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
-
import { buildJoinedRecordsQuery } from "../datastore-query.js";
|
|
2
|
+
import { buildJoinedRecordsQuery, parseForeignKeyReference, } from "../datastore-query.js";
|
|
3
3
|
import { normalizeError, resolvePodId, stringifyComparable } from "./utils.js";
|
|
4
|
-
|
|
4
|
+
async function buildShorthandQuery(client, podId, baseTableName, shorthandJoins) {
|
|
5
|
+
const resolvedPodId = resolvePodId(client, podId);
|
|
6
|
+
const scopedClient = resolvedPodId === client.podId ? client : client.withPod(resolvedPodId);
|
|
7
|
+
const baseTable = await scopedClient.tables.get(baseTableName.trim());
|
|
8
|
+
const joins = await Promise.all(shorthandJoins.map(async (entry) => {
|
|
9
|
+
const baseColumn = baseTable.columns.find((col) => col.name === entry.on) ?? null;
|
|
10
|
+
const reference = baseColumn?.foreign_key?.references
|
|
11
|
+
? parseForeignKeyReference(baseColumn.foreign_key.references)
|
|
12
|
+
: null;
|
|
13
|
+
if (!baseColumn) {
|
|
14
|
+
throw new Error(`Column "${entry.on}" was not found on table "${baseTableName}".`);
|
|
15
|
+
}
|
|
16
|
+
if (!reference) {
|
|
17
|
+
throw new Error(`Column "${entry.on}" on "${baseTableName}" is not a foreign key. Use the full query API for non-FK joins.`);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
type: entry.type ?? "left",
|
|
21
|
+
table: entry.table,
|
|
22
|
+
alias: entry.alias,
|
|
23
|
+
on: {
|
|
24
|
+
left: { table: baseTableName, column: entry.on },
|
|
25
|
+
right: { table: entry.alias ?? entry.table, column: reference.column },
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}));
|
|
29
|
+
return {
|
|
30
|
+
from: { table: baseTableName, alias: baseTableName },
|
|
31
|
+
joins,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function useJoinedRecords({ client, podId, query, baseTable, joins: shorthandJoins, enabled = true, autoLoad = true, }) {
|
|
5
35
|
const [records, setRecords] = useState([]);
|
|
6
36
|
const [total, setTotal] = useState(0);
|
|
37
|
+
const [sql, setSql] = useState("");
|
|
7
38
|
const [isLoading, setIsLoading] = useState(false);
|
|
8
39
|
const [error, setError] = useState(null);
|
|
40
|
+
const hasShorthand = !query && !!baseTable && !!shorthandJoins?.length;
|
|
9
41
|
const queryKey = stringifyComparable(query);
|
|
42
|
+
const shorthandKey = stringifyComparable({ baseTable, shorthandJoins });
|
|
10
43
|
const stableQuery = useMemo(() => query, [queryKey]);
|
|
11
|
-
const sql = useMemo(() => buildJoinedRecordsQuery(stableQuery), [stableQuery]);
|
|
12
44
|
const refresh = useCallback(async (signal) => {
|
|
13
45
|
if (!enabled) {
|
|
14
46
|
setRecords([]);
|
|
15
47
|
setTotal(0);
|
|
48
|
+
setSql("");
|
|
16
49
|
setError(null);
|
|
17
50
|
setIsLoading(false);
|
|
18
51
|
return [];
|
|
@@ -22,7 +55,22 @@ export function useJoinedRecords({ client, podId, query, enabled = true, autoLoa
|
|
|
22
55
|
try {
|
|
23
56
|
const resolvedPodId = resolvePodId(client, podId);
|
|
24
57
|
const scopedClient = resolvedPodId === client.podId ? client : client.withPod(resolvedPodId);
|
|
25
|
-
|
|
58
|
+
let resolvedQuery = stableQuery;
|
|
59
|
+
if (hasShorthand && baseTable && shorthandJoins) {
|
|
60
|
+
resolvedQuery = await buildShorthandQuery(client, podId, baseTable, shorthandJoins);
|
|
61
|
+
}
|
|
62
|
+
if (!resolvedQuery) {
|
|
63
|
+
setRecords([]);
|
|
64
|
+
setTotal(0);
|
|
65
|
+
setSql("");
|
|
66
|
+
setIsLoading(false);
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
if (signal?.aborted)
|
|
70
|
+
return [];
|
|
71
|
+
const nextSql = buildJoinedRecordsQuery(resolvedQuery);
|
|
72
|
+
setSql(nextSql);
|
|
73
|
+
const response = await scopedClient.datastore.query(nextSql);
|
|
26
74
|
if (signal?.aborted)
|
|
27
75
|
return [];
|
|
28
76
|
const nextRecords = (response.items ?? []);
|
|
@@ -41,11 +89,12 @@ export function useJoinedRecords({ client, podId, query, enabled = true, autoLoa
|
|
|
41
89
|
if (!signal?.aborted)
|
|
42
90
|
setIsLoading(false);
|
|
43
91
|
}
|
|
44
|
-
}, [client, enabled, podId,
|
|
92
|
+
}, [client, enabled, hasShorthand, baseTable, podId, shorthandJoins, stableQuery]);
|
|
45
93
|
useEffect(() => {
|
|
46
94
|
if (!enabled) {
|
|
47
95
|
setRecords([]);
|
|
48
96
|
setTotal(0);
|
|
97
|
+
setSql("");
|
|
49
98
|
setError(null);
|
|
50
99
|
setIsLoading(false);
|
|
51
100
|
return;
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
|
+
/**
|
|
3
|
+
* React hook for fetching a single record by ID. Unwraps `.data`
|
|
4
|
+
* automatically so `record` is the plain object, not the API envelope.
|
|
5
|
+
*
|
|
6
|
+
* Perfect for detail panels — pair with `useRecords` for the list.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* const { record, isLoading } = useRecord({
|
|
11
|
+
* client,
|
|
12
|
+
* tableName: "issues",
|
|
13
|
+
* recordId: selectedId,
|
|
14
|
+
* });
|
|
15
|
+
* // record is the issue object directly (or null)
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
2
18
|
export interface UseRecordOptions {
|
|
3
19
|
client: LemmaClient;
|
|
4
20
|
podId?: string;
|
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
2
|
import { type RecordSchemaField } from "../record-form.js";
|
|
3
|
-
import type { RecordResponse } from "../types.js";
|
|
3
|
+
import type { FunctionRun, RecordResponse } from "../types.js";
|
|
4
4
|
import { type UseRecordSchemaResult } from "./useRecordSchema.js";
|
|
5
|
+
/**
|
|
6
|
+
* React hook for schema-driven record forms with validation, dirty tracking,
|
|
7
|
+
* and create/update auto-detection.
|
|
8
|
+
*
|
|
9
|
+
* Supports two submit modes:
|
|
10
|
+
* - `"direct"` (default): persists via `records.create` / `records.update`.
|
|
11
|
+
* - `"function"`: persists via `functions.runs.create`, routing the form
|
|
12
|
+
* payload through a pod function that may enforce business logic
|
|
13
|
+
* (e.g. auto-generating identifiers, logging history).
|
|
14
|
+
*
|
|
15
|
+
* @example Direct create/update form
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const form = useRecordForm({ client, tableName: "issues" });
|
|
18
|
+
* // form.fields → all schema fields, form.editableFields → user-fillable fields
|
|
19
|
+
* await form.submit(); // calls records.create or records.update
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @example Function-backed form
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const form = useRecordForm({
|
|
25
|
+
* client,
|
|
26
|
+
* tableName: "issues",
|
|
27
|
+
* submitVia: "function",
|
|
28
|
+
* submitFunctionName: "create-issue",
|
|
29
|
+
* hiddenFields: ["identifier", "created_at"],
|
|
30
|
+
* });
|
|
31
|
+
* await form.submit(); // calls functions.runs.create("create-issue", { input: payload })
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
5
34
|
export interface UseRecordFormOptions {
|
|
6
35
|
client: LemmaClient;
|
|
7
36
|
podId?: string;
|
|
@@ -11,7 +40,17 @@ export interface UseRecordFormOptions {
|
|
|
11
40
|
mode?: "auto" | "create" | "update";
|
|
12
41
|
enabled?: boolean;
|
|
13
42
|
autoLoad?: boolean;
|
|
14
|
-
|
|
43
|
+
/** Field names to include in `fields` and `editableFields`. Takes precedence over `hiddenFields`. */
|
|
44
|
+
visibleFields?: string[];
|
|
45
|
+
/** Field names to exclude from `fields` and `editableFields`. Ignored for any field also listed in `visibleFields`. */
|
|
46
|
+
hiddenFields?: string[];
|
|
47
|
+
/** How the form persists data. `"direct"` calls `records.create`/`records.update`. `"function"` calls `functions.runs.create`. */
|
|
48
|
+
submitVia?: "direct" | "function";
|
|
49
|
+
/** Function name to run when `submitVia` is `"function"`. Required when `submitVia` is `"function"`. */
|
|
50
|
+
submitFunctionName?: string;
|
|
51
|
+
/** Transforms the form payload before passing it as function input. Receives the validated payload, returns the function input object. */
|
|
52
|
+
submitFunctionInput?: (payload: Record<string, unknown>) => Record<string, unknown>;
|
|
53
|
+
onSubmitSuccess?: (record: Record<string, unknown>, response: RecordResponse | FunctionRun) => void;
|
|
15
54
|
onError?: (error: unknown) => void;
|
|
16
55
|
}
|
|
17
56
|
export interface UseRecordFormResult {
|
|
@@ -39,4 +78,4 @@ export interface UseRecordFormResult {
|
|
|
39
78
|
mode?: "create" | "update";
|
|
40
79
|
}) => Promise<Record<string, unknown> | null>;
|
|
41
80
|
}
|
|
42
|
-
export declare function useRecordForm({ client, podId, tableName, recordId, initialValues, mode, enabled, autoLoad, onSubmitSuccess, onError, }: UseRecordFormOptions): UseRecordFormResult;
|
|
81
|
+
export declare function useRecordForm({ client, podId, tableName, recordId, initialValues, mode, enabled, autoLoad, visibleFields, hiddenFields, submitVia, submitFunctionName, submitFunctionInput, onSubmitSuccess, onError, }: UseRecordFormOptions): UseRecordFormResult;
|
|
@@ -3,7 +3,7 @@ import { buildRecordFormValues, buildRecordPayload, } from "../record-form.js";
|
|
|
3
3
|
import { normalizeError, resolvePodClient, stringifyComparable } from "./utils.js";
|
|
4
4
|
import { useRecordSchema, } from "./useRecordSchema.js";
|
|
5
5
|
const EMPTY_VALUES = {};
|
|
6
|
-
export function useRecordForm({ client, podId, tableName, recordId = null, initialValues = EMPTY_VALUES, mode = "auto", enabled = true, autoLoad = true, onSubmitSuccess, onError, }) {
|
|
6
|
+
export function useRecordForm({ client, podId, tableName, recordId = null, initialValues = EMPTY_VALUES, mode = "auto", enabled = true, autoLoad = true, visibleFields, hiddenFields, submitVia = "direct", submitFunctionName, submitFunctionInput, onSubmitSuccess, onError, }) {
|
|
7
7
|
const schema = useRecordSchema({
|
|
8
8
|
client,
|
|
9
9
|
podId,
|
|
@@ -11,6 +11,30 @@ export function useRecordForm({ client, podId, tableName, recordId = null, initi
|
|
|
11
11
|
enabled,
|
|
12
12
|
autoLoad,
|
|
13
13
|
});
|
|
14
|
+
const visibleSet = useMemo(() => visibleFields ? new Set(visibleFields) : null, [visibleFields]);
|
|
15
|
+
const hiddenSet = useMemo(() => hiddenFields ? new Set(hiddenFields) : null, [hiddenFields]);
|
|
16
|
+
const filteredFields = useMemo(() => {
|
|
17
|
+
if (!visibleSet && !hiddenSet)
|
|
18
|
+
return schema.fields;
|
|
19
|
+
return schema.fields.filter((field) => {
|
|
20
|
+
if (visibleSet)
|
|
21
|
+
return visibleSet.has(field.name);
|
|
22
|
+
if (hiddenSet)
|
|
23
|
+
return !hiddenSet.has(field.name);
|
|
24
|
+
return true;
|
|
25
|
+
});
|
|
26
|
+
}, [hiddenSet, schema.fields, visibleSet]);
|
|
27
|
+
const filteredEditableFields = useMemo(() => {
|
|
28
|
+
if (!visibleSet && !hiddenSet)
|
|
29
|
+
return schema.editableFields;
|
|
30
|
+
return schema.editableFields.filter((field) => {
|
|
31
|
+
if (visibleSet)
|
|
32
|
+
return visibleSet.has(field.name);
|
|
33
|
+
if (hiddenSet)
|
|
34
|
+
return !hiddenSet.has(field.name);
|
|
35
|
+
return true;
|
|
36
|
+
});
|
|
37
|
+
}, [hiddenSet, schema.editableFields, visibleSet]);
|
|
14
38
|
const [record, setRecord] = useState(null);
|
|
15
39
|
const [values, setValuesState] = useState({});
|
|
16
40
|
const [baselineValues, setBaselineValues] = useState({});
|
|
@@ -147,6 +171,19 @@ export function useRecordForm({ client, podId, tableName, recordId = null, initi
|
|
|
147
171
|
setRecordError(null);
|
|
148
172
|
try {
|
|
149
173
|
const scopedClient = resolvePodClient(client, podId);
|
|
174
|
+
if (submitVia === "function") {
|
|
175
|
+
const functionName = submitFunctionName ?? tableName;
|
|
176
|
+
const functionInput = submitFunctionInput ? submitFunctionInput(payload.data) : payload.data;
|
|
177
|
+
const run = await scopedClient.functions.runs.create(functionName, { input: functionInput });
|
|
178
|
+
const nextRecord = run.output_data ?? { id: run.id, ...functionInput };
|
|
179
|
+
setRecord(nextRecord);
|
|
180
|
+
hydrateValues({
|
|
181
|
+
...(nextRecord ?? {}),
|
|
182
|
+
...stableInitialValues,
|
|
183
|
+
});
|
|
184
|
+
onSubmitSuccess?.(nextRecord ?? {}, run);
|
|
185
|
+
return nextRecord;
|
|
186
|
+
}
|
|
150
187
|
const response = resolvedMode === "update" && recordId
|
|
151
188
|
? await scopedClient.records.update(tableName, recordId, payload.data)
|
|
152
189
|
: await scopedClient.records.create(tableName, payload.data);
|
|
@@ -168,14 +205,14 @@ export function useRecordForm({ client, podId, tableName, recordId = null, initi
|
|
|
168
205
|
finally {
|
|
169
206
|
setIsSubmitting(false);
|
|
170
207
|
}
|
|
171
|
-
}, [client, hydrateValues, mode, onError, onSubmitSuccess, podId, recordId, schemaTable, stableInitialValues, tableName, values]);
|
|
208
|
+
}, [client, hydrateValues, mode, onError, onSubmitSuccess, podId, recordId, schemaTable, stableInitialValues, submitFunctionInput, submitFunctionName, submitVia, tableName, values]);
|
|
172
209
|
const isDirty = useMemo(() => {
|
|
173
210
|
return stringifyComparable(values) !== stringifyComparable(baselineValues);
|
|
174
211
|
}, [baselineValues, values]);
|
|
175
212
|
return useMemo(() => ({
|
|
176
213
|
table: schema.table,
|
|
177
|
-
fields:
|
|
178
|
-
editableFields:
|
|
214
|
+
fields: filteredFields,
|
|
215
|
+
editableFields: filteredEditableFields,
|
|
179
216
|
defaults: schema.defaults,
|
|
180
217
|
values,
|
|
181
218
|
baselineValues,
|
|
@@ -197,6 +234,8 @@ export function useRecordForm({ client, podId, tableName, recordId = null, initi
|
|
|
197
234
|
}), [
|
|
198
235
|
baselineValues,
|
|
199
236
|
fieldErrors,
|
|
237
|
+
filteredEditableFields,
|
|
238
|
+
filteredFields,
|
|
200
239
|
isDirty,
|
|
201
240
|
isLoadingRecord,
|
|
202
241
|
isSubmitting,
|
|
@@ -206,9 +245,7 @@ export function useRecordForm({ client, podId, tableName, recordId = null, initi
|
|
|
206
245
|
refreshRecord,
|
|
207
246
|
reset,
|
|
208
247
|
schema.defaults,
|
|
209
|
-
schema.editableFields,
|
|
210
248
|
schema.error,
|
|
211
|
-
schema.fields,
|
|
212
249
|
schema.isLoading,
|
|
213
250
|
schema.refresh,
|
|
214
251
|
schema.table,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { LemmaClient } from "../client.js";
|
|
2
|
+
import type { Table } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* React hook for fetching records from a referencing table that point back
|
|
5
|
+
* to a specific record via a foreign key.
|
|
6
|
+
*
|
|
7
|
+
* Unlike `useReverseRelatedRecords` which starts from a parent table and
|
|
8
|
+
* discovers what references it, this hook starts from the child table and
|
|
9
|
+
* says "give me all rows in this table where this FK column equals this ID."
|
|
10
|
+
*
|
|
11
|
+
* @example Fetch all comments for an issue
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const { records, isLoading } = useReferencingRecords({
|
|
14
|
+
* client,
|
|
15
|
+
* table: "comments",
|
|
16
|
+
* foreignKey: "issue_id",
|
|
17
|
+
* recordId: "issue_123",
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @example Fetch history entries
|
|
22
|
+
* ```tsx
|
|
23
|
+
* const { records, columns, isLoading } = useReferencingRecords({
|
|
24
|
+
* client,
|
|
25
|
+
* table: "issue_history",
|
|
26
|
+
* foreignKey: "issue_id",
|
|
27
|
+
* recordId: selectedIssueId,
|
|
28
|
+
* sortBy: "created_at",
|
|
29
|
+
* order: "desc",
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export interface ReferencingRecordsColumn {
|
|
34
|
+
key: string;
|
|
35
|
+
field: string;
|
|
36
|
+
label: string;
|
|
37
|
+
}
|
|
38
|
+
export interface UseReferencingRecordsOptions {
|
|
39
|
+
client: LemmaClient;
|
|
40
|
+
podId?: string;
|
|
41
|
+
/** The referencing (child) table, e.g. "comments". */
|
|
42
|
+
table: string;
|
|
43
|
+
/** The foreign-key column in the referencing table, e.g. "issue_id". */
|
|
44
|
+
foreignKey: string;
|
|
45
|
+
/** The record ID value to match against the foreign-key column. */
|
|
46
|
+
recordId?: string | null;
|
|
47
|
+
/** Fields to select. Auto-resolved from the table schema if omitted. */
|
|
48
|
+
fields?: string[];
|
|
49
|
+
limit?: number;
|
|
50
|
+
offset?: number;
|
|
51
|
+
sortBy?: string;
|
|
52
|
+
order?: "asc" | "desc" | string;
|
|
53
|
+
enabled?: boolean;
|
|
54
|
+
autoLoad?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface UseReferencingRecordsResult<TRow extends Record<string, unknown> = Record<string, unknown>> {
|
|
57
|
+
referencedTable: Table | null;
|
|
58
|
+
columns: ReferencingRecordsColumn[];
|
|
59
|
+
records: TRow[];
|
|
60
|
+
total: number;
|
|
61
|
+
nextPageToken: string | null;
|
|
62
|
+
isLoading: boolean;
|
|
63
|
+
error: Error | null;
|
|
64
|
+
refresh: () => Promise<TRow[]>;
|
|
65
|
+
}
|
|
66
|
+
export declare function useReferencingRecords<TRow extends Record<string, unknown> = Record<string, unknown>>({ client, podId, table, foreignKey, recordId, fields, limit, offset, sortBy, order, enabled, autoLoad, }: UseReferencingRecordsOptions): UseReferencingRecordsResult<TRow>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { normalizeError, resolvePodClient, stringifyComparable } from "./utils.js";
|
|
3
|
+
function sentenceCase(value) {
|
|
4
|
+
return value
|
|
5
|
+
.replace(/[_\.]/g, " ")
|
|
6
|
+
.replace(/\s+/g, " ")
|
|
7
|
+
.trim()
|
|
8
|
+
.replace(/\b\w/g, (match) => match.toUpperCase());
|
|
9
|
+
}
|
|
10
|
+
function pickDefaultFields(table, foreignKey) {
|
|
11
|
+
const names = table.columns
|
|
12
|
+
.map((column) => column.name)
|
|
13
|
+
.filter((name) => name !== "created_at" && name !== "updated_at");
|
|
14
|
+
const prioritized = ["id", "name", "title", "label", "status", foreignKey];
|
|
15
|
+
const next = [];
|
|
16
|
+
prioritized.forEach((name) => {
|
|
17
|
+
if (names.includes(name) && !next.includes(name)) {
|
|
18
|
+
next.push(name);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
names.forEach((name) => {
|
|
22
|
+
if (!next.includes(name)) {
|
|
23
|
+
next.push(name);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return next.slice(0, 6);
|
|
27
|
+
}
|
|
28
|
+
export function useReferencingRecords({ client, podId, table, foreignKey, recordId = null, fields, limit = 20, offset, sortBy, order, enabled = true, autoLoad = true, }) {
|
|
29
|
+
const [referencedTable, setReferencedTable] = useState(null);
|
|
30
|
+
const [columns, setColumns] = useState([]);
|
|
31
|
+
const [records, setRecords] = useState([]);
|
|
32
|
+
const [total, setTotal] = useState(0);
|
|
33
|
+
const [nextPageToken, setNextPageToken] = useState(null);
|
|
34
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
35
|
+
const [error, setError] = useState(null);
|
|
36
|
+
const trimmedTable = table.trim();
|
|
37
|
+
const trimmedRecordId = typeof recordId === "string" ? recordId.trim() : "";
|
|
38
|
+
const fieldsKey = stringifyComparable(fields);
|
|
39
|
+
const stableFields = useMemo(() => fields, [fieldsKey]);
|
|
40
|
+
const isEnabled = enabled && trimmedTable.length > 0 && trimmedRecordId.length > 0;
|
|
41
|
+
const refresh = useCallback(async (signal) => {
|
|
42
|
+
if (!isEnabled) {
|
|
43
|
+
setReferencedTable(null);
|
|
44
|
+
setColumns([]);
|
|
45
|
+
setRecords([]);
|
|
46
|
+
setTotal(0);
|
|
47
|
+
setNextPageToken(null);
|
|
48
|
+
setError(null);
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
setIsLoading(true);
|
|
53
|
+
setError(null);
|
|
54
|
+
try {
|
|
55
|
+
const scopedClient = resolvePodClient(client, podId);
|
|
56
|
+
const tableResponse = await scopedClient.tables.get(trimmedTable);
|
|
57
|
+
if (signal?.aborted)
|
|
58
|
+
return [];
|
|
59
|
+
setReferencedTable(tableResponse);
|
|
60
|
+
const resolvedFields = (stableFields?.length ? stableFields : pickDefaultFields(tableResponse, foreignKey))
|
|
61
|
+
.filter((field, index, allFields) => field.trim().length > 0 && allFields.indexOf(field) === index);
|
|
62
|
+
const response = await scopedClient.records.list(trimmedTable, {
|
|
63
|
+
filters: [{
|
|
64
|
+
field: foreignKey,
|
|
65
|
+
op: "eq",
|
|
66
|
+
value: trimmedRecordId,
|
|
67
|
+
}],
|
|
68
|
+
limit,
|
|
69
|
+
offset,
|
|
70
|
+
sortBy,
|
|
71
|
+
order,
|
|
72
|
+
});
|
|
73
|
+
if (signal?.aborted)
|
|
74
|
+
return [];
|
|
75
|
+
const nextRecords = (response.items ?? []);
|
|
76
|
+
setColumns(resolvedFields.map((field) => ({
|
|
77
|
+
key: field,
|
|
78
|
+
field,
|
|
79
|
+
label: sentenceCase(field),
|
|
80
|
+
})));
|
|
81
|
+
setRecords(nextRecords);
|
|
82
|
+
setTotal(response.total ?? nextRecords.length);
|
|
83
|
+
setNextPageToken(response.next_page_token ?? null);
|
|
84
|
+
return nextRecords;
|
|
85
|
+
}
|
|
86
|
+
catch (refreshError) {
|
|
87
|
+
if (signal?.aborted)
|
|
88
|
+
return [];
|
|
89
|
+
const normalized = normalizeError(refreshError, "Failed to load referencing records.");
|
|
90
|
+
setError(normalized);
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
if (!signal?.aborted)
|
|
95
|
+
setIsLoading(false);
|
|
96
|
+
}
|
|
97
|
+
}, [
|
|
98
|
+
client,
|
|
99
|
+
foreignKey,
|
|
100
|
+
isEnabled,
|
|
101
|
+
limit,
|
|
102
|
+
offset,
|
|
103
|
+
order,
|
|
104
|
+
podId,
|
|
105
|
+
sortBy,
|
|
106
|
+
stableFields,
|
|
107
|
+
trimmedRecordId,
|
|
108
|
+
trimmedTable,
|
|
109
|
+
]);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!isEnabled) {
|
|
112
|
+
setReferencedTable(null);
|
|
113
|
+
setColumns([]);
|
|
114
|
+
setRecords([]);
|
|
115
|
+
setTotal(0);
|
|
116
|
+
setNextPageToken(null);
|
|
117
|
+
setError(null);
|
|
118
|
+
setIsLoading(false);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!autoLoad)
|
|
122
|
+
return;
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
let cancelled = false;
|
|
125
|
+
(async () => {
|
|
126
|
+
try {
|
|
127
|
+
await refresh(controller.signal);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
if (!cancelled) {
|
|
131
|
+
setError(normalizeError(new Error("Failed to load referencing records."), "Failed to load referencing records."));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
135
|
+
return () => {
|
|
136
|
+
cancelled = true;
|
|
137
|
+
controller.abort();
|
|
138
|
+
};
|
|
139
|
+
}, [autoLoad, isEnabled, refresh]);
|
|
140
|
+
return useMemo(() => ({
|
|
141
|
+
referencedTable,
|
|
142
|
+
columns,
|
|
143
|
+
records,
|
|
144
|
+
total,
|
|
145
|
+
nextPageToken,
|
|
146
|
+
isLoading,
|
|
147
|
+
error,
|
|
148
|
+
refresh,
|
|
149
|
+
}), [
|
|
150
|
+
columns,
|
|
151
|
+
error,
|
|
152
|
+
isLoading,
|
|
153
|
+
nextPageToken,
|
|
154
|
+
records,
|
|
155
|
+
refresh,
|
|
156
|
+
referencedTable,
|
|
157
|
+
total,
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
2
|
import type { Table } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* React hook for fetching base-table rows with their FK-related data
|
|
5
|
+
* joined in a single query. You specify which FK columns to include and
|
|
6
|
+
* the hook auto-resolves the referenced table and join columns.
|
|
7
|
+
*
|
|
8
|
+
* The result has nested objects: `{ id, name, team: { id, name } }`.
|
|
9
|
+
*
|
|
10
|
+
* @example Issues with their team
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const { records, isLoading } = useRelatedRecords({
|
|
13
|
+
* client,
|
|
14
|
+
* tableName: "issues",
|
|
15
|
+
* include: [{ foreignKey: "team_id" }],
|
|
16
|
+
* });
|
|
17
|
+
* // records[0] = { id: "1", title: "Bug", team: { id: "t1", name: "Eng" } }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
3
20
|
export interface RelatedRecordsInclude {
|
|
4
21
|
foreignKey: string;
|
|
5
22
|
as?: string;
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
2
|
import type { Table } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* React hook for finding records in *other* tables that reference a given
|
|
5
|
+
* record. Starts from the parent table, discovers all tables with FK
|
|
6
|
+
* columns pointing back to it, and fetches the referencing rows.
|
|
7
|
+
*
|
|
8
|
+
* For the simpler case where you already know the referencing table and
|
|
9
|
+
* FK column, prefer `useReferencingRecords` — it has a more intuitive API.
|
|
10
|
+
*
|
|
11
|
+
* @example All comments and history entries for an issue
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const { relations, records, isLoading } = useReverseRelatedRecords({
|
|
14
|
+
* client,
|
|
15
|
+
* tableName: "issues",
|
|
16
|
+
* recordId: "issue_123",
|
|
17
|
+
* relation: { tableName: "comments", foreignKey: "issue_id" },
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @see useReferencingRecords — flipped-perspective alias for the common
|
|
22
|
+
* "show me all rows in table X where FK = Y" pattern.
|
|
23
|
+
*/
|
|
3
24
|
export interface ReverseRelationSelector {
|
|
4
25
|
tableName: string;
|
|
5
26
|
foreignKey: string;
|
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
|
-
import type { RecordResponse } from "../types.js";
|
|
2
|
+
import type { FunctionRun, RecordResponse } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* React hook for updating a single record. Manages loading/error state and
|
|
5
|
+
* exposes an `update` function you can call from event handlers.
|
|
6
|
+
*
|
|
7
|
+
* Supports two modes:
|
|
8
|
+
* - `"direct"` (default): calls `records.update` directly.
|
|
9
|
+
* - `"function"`: calls `functions.runs.create`, routing the update through
|
|
10
|
+
* a pod function (e.g. for status transitions that log history).
|
|
11
|
+
*
|
|
12
|
+
* @example Direct update
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const { update, isSubmitting } = useUpdateRecord({ client, tableName: "issues", recordId: "123" });
|
|
15
|
+
* await update({ status: "closed" });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @example Function-backed update
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const { update, isSubmitting } = useUpdateRecord({
|
|
21
|
+
* client,
|
|
22
|
+
* tableName: "issues",
|
|
23
|
+
* recordId: "123",
|
|
24
|
+
* updateVia: "function",
|
|
25
|
+
* updateFunctionName: "update-issue-status",
|
|
26
|
+
* });
|
|
27
|
+
* await update({ status: "in_progress" });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
3
30
|
export interface UseUpdateRecordOptions {
|
|
4
31
|
client: LemmaClient;
|
|
5
32
|
podId?: string;
|
|
6
33
|
tableName: string;
|
|
7
34
|
recordId?: string | null;
|
|
8
35
|
enabled?: boolean;
|
|
9
|
-
|
|
36
|
+
/** How the record is updated. `"direct"` calls `records.update`. `"function"` calls `functions.runs.create`. */
|
|
37
|
+
updateVia?: "direct" | "function";
|
|
38
|
+
/** Function name to run when `updateVia` is `"function"`. Falls back to `tableName` if omitted. */
|
|
39
|
+
updateFunctionName?: string;
|
|
40
|
+
onSuccess?: (record: Record<string, unknown>, response: RecordResponse | FunctionRun) => void;
|
|
10
41
|
onError?: (error: unknown) => void;
|
|
11
42
|
}
|
|
12
43
|
export interface UseUpdateRecordResult<TRecord extends Record<string, unknown> = Record<string, unknown>> {
|
|
@@ -18,4 +49,4 @@ export interface UseUpdateRecordResult<TRecord extends Record<string, unknown> =
|
|
|
18
49
|
}) => Promise<TRecord | null>;
|
|
19
50
|
reset: () => void;
|
|
20
51
|
}
|
|
21
|
-
export declare function useUpdateRecord<TRecord extends Record<string, unknown> = Record<string, unknown>>({ client, podId, tableName, recordId, enabled, onSuccess, onError, }: UseUpdateRecordOptions): UseUpdateRecordResult<TRecord>;
|
|
52
|
+
export declare function useUpdateRecord<TRecord extends Record<string, unknown> = Record<string, unknown>>({ client, podId, tableName, recordId, enabled, updateVia, updateFunctionName, onSuccess, onError, }: UseUpdateRecordOptions): UseUpdateRecordResult<TRecord>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { normalizeError, resolvePodClient } from "./utils.js";
|
|
3
|
-
export function useUpdateRecord({ client, podId, tableName, recordId = null, enabled = true, onSuccess, onError, }) {
|
|
3
|
+
export function useUpdateRecord({ client, podId, tableName, recordId = null, enabled = true, updateVia = "direct", updateFunctionName, onSuccess, onError, }) {
|
|
4
4
|
const [updatedRecord, setUpdatedRecord] = useState(null);
|
|
5
5
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
6
6
|
const [error, setError] = useState(null);
|
|
@@ -22,6 +22,17 @@ export function useUpdateRecord({ client, podId, tableName, recordId = null, ena
|
|
|
22
22
|
setError(null);
|
|
23
23
|
try {
|
|
24
24
|
const scopedClient = resolvePodClient(client, podId);
|
|
25
|
+
if (updateVia === "function") {
|
|
26
|
+
const functionName = updateFunctionName ?? trimmedTableName;
|
|
27
|
+
const input = { ...data, id: nextRecordId, record_id: nextRecordId };
|
|
28
|
+
const run = await scopedClient.functions.runs.create(functionName, { input });
|
|
29
|
+
const nextRecord = (run.output_data ?? { id: nextRecordId, ...data });
|
|
30
|
+
setUpdatedRecord(nextRecord);
|
|
31
|
+
if (nextRecord) {
|
|
32
|
+
onSuccessRef.current?.(nextRecord, run);
|
|
33
|
+
}
|
|
34
|
+
return nextRecord;
|
|
35
|
+
}
|
|
25
36
|
const response = await scopedClient.records.update(trimmedTableName, nextRecordId, data);
|
|
26
37
|
const nextRecord = (response.data ?? null);
|
|
27
38
|
setUpdatedRecord(nextRecord);
|
|
@@ -39,7 +50,7 @@ export function useUpdateRecord({ client, podId, tableName, recordId = null, ena
|
|
|
39
50
|
finally {
|
|
40
51
|
setIsSubmitting(false);
|
|
41
52
|
}
|
|
42
|
-
}, [client, isEnabled, podId, trimmedRecordId, trimmedTableName]);
|
|
53
|
+
}, [client, isEnabled, podId, trimmedRecordId, trimmedTableName, updateFunctionName, updateVia]);
|
|
43
54
|
const reset = useCallback(() => {
|
|
44
55
|
setUpdatedRecord(null);
|
|
45
56
|
setError(null);
|