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