@softr/sdk 0.0.22 → 0.0.23

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.
@@ -1,10 +1,20 @@
1
- import { ChartDataOptions, ChartDataResult, CreateOptions, GetOptions, ListOptions, ListResult, MetricOptions, MetricResult, SoftrRecord, UpdateOptions } from "../types.js";
1
+ import { ChartDataOptions, ChartDataResult, CreateOptions, GetOptions, ListOptions, ListResult, MetricOptions, MetricResult, RecordFieldsFor, SoftrRecord, UpdateOptions } from "../types.js";
2
2
 
3
3
  //#region src/core/datasource.d.ts
4
- declare function listRecords(datasourceId: string, options?: ListOptions): Promise<ListResult>;
5
- declare function getRecord(datasourceId: string, options: GetOptions): Promise<SoftrRecord | null>;
6
- declare function createRecord(datasourceId: string, options: CreateOptions): Promise<SoftrRecord>;
7
- declare function updateRecord(datasourceId: string, options: UpdateOptions): Promise<SoftrRecord>;
4
+ declare function listRecords<Id extends string, Sel extends Record<string, string> = Record<never, never>>(datasourceId: Id, options?: Omit<ListOptions, 'select'> & {
5
+ select?: Sel;
6
+ }): Promise<ListResult<SoftrRecord<RecordFieldsFor<Id, Sel>>>>;
7
+ declare function getRecord<Id extends string, Sel extends Record<string, string> = Record<never, never>>(datasourceId: Id, options: Omit<GetOptions, 'select'> & {
8
+ select?: Sel;
9
+ }): Promise<SoftrRecord<RecordFieldsFor<Id, Sel>> | null>;
10
+ declare function createRecord<Id extends string, Sel extends Record<string, string> = Record<never, never>>(datasourceId: Id, options: Omit<CreateOptions, 'select' | 'data'> & {
11
+ select?: Sel;
12
+ data: Partial<RecordFieldsFor<Id, Sel>>;
13
+ }): Promise<SoftrRecord<RecordFieldsFor<Id, Sel>>>;
14
+ declare function updateRecord<Id extends string, Sel extends Record<string, string> = Record<never, never>>(datasourceId: Id, options: Omit<UpdateOptions, 'select' | 'data'> & {
15
+ select?: Sel;
16
+ data: Partial<RecordFieldsFor<Id, Sel>>;
17
+ }): Promise<SoftrRecord<RecordFieldsFor<Id, Sel>>>;
8
18
  declare function deleteRecord(datasourceId: string, id: string): Promise<void>;
9
19
  declare function getMetric(datasourceId: string, options: MetricOptions): Promise<MetricResult>;
10
20
  declare function getChartData(datasourceId: string, options: ChartDataOptions): Promise<ChartDataResult>;
@@ -3,6 +3,7 @@ import { panic } from "../assertions.js";
3
3
  import { readUserTimezone } from "../cookies.js";
4
4
  import { sessionConfig } from "../auth/config.js";
5
5
  import { readSessionToken } from "../auth/session.js";
6
+ import { assertResponseOk } from "./utils.js";
6
7
 
7
8
  //#region src/core/datasource.ts
8
9
  const MAX_RECORD_COUNT = 100;
@@ -36,7 +37,7 @@ async function listRecords(datasourceId, options = {}) {
36
37
  }
37
38
  })
38
39
  });
39
- if (!res.ok) throw new Error(`Softr datasource list failed for "${datasourceId}": ${res.status}`);
40
+ await assertResponseOk(res, `Softr datasource list failed for "${datasourceId}"`);
40
41
  const payload = await res.json();
41
42
  const nextOffset = payload.offset < payload.total ? payload.offset : null;
42
43
  return {
@@ -60,13 +61,18 @@ async function getRecord(datasourceId, options) {
60
61
  },
61
62
  body: JSON.stringify({ options: { userLocale: "en-US" } })
62
63
  });
63
- if (!res.ok) throw new Error(`Softr datasource get failed for "${datasourceId}": ${res.status}`);
64
+ await assertResponseOk(res, `Softr datasource get failed for "${datasourceId}"`);
64
65
  const contentLength = res.headers.get("Content-Length");
65
66
  if (contentLength && parseInt(contentLength, 10) === 0) return null;
66
- return res.json();
67
+ const record = await res.json();
68
+ return {
69
+ id: record.id,
70
+ fields: options.select ? aliasFields(record.fields, options.select) : record.fields
71
+ };
67
72
  }
68
73
  return (await listRecords(datasourceId, {
69
74
  where: options.where,
75
+ select: options.select,
70
76
  limit: 1,
71
77
  timeZone: options.timeZone
72
78
  })).records[0] ?? null;
@@ -81,12 +87,12 @@ async function createRecord(datasourceId, options) {
81
87
  },
82
88
  body: JSON.stringify({ fields: rawFields })
83
89
  });
84
- if (!res.ok) throw new Error(`Softr datasource create failed for "${datasourceId}": ${res.status}`);
90
+ await assertResponseOk(res, `Softr datasource create failed for "${datasourceId}"`);
85
91
  const record = await res.json();
86
- return options.select ? {
92
+ return {
87
93
  id: record.id,
88
- fields: aliasFields(record.fields, options.select)
89
- } : record;
94
+ fields: options.select ? aliasFields(record.fields, options.select) : record.fields
95
+ };
90
96
  }
91
97
  async function updateRecord(datasourceId, options) {
92
98
  if (!options.id) throw new Error(`Softr datasource update: id must not be empty`);
@@ -99,19 +105,18 @@ async function updateRecord(datasourceId, options) {
99
105
  },
100
106
  body: JSON.stringify({ fields: rawFields })
101
107
  });
102
- if (!res.ok) throw new Error(`Softr datasource update failed for "${datasourceId}": ${res.status}`);
108
+ await assertResponseOk(res, `Softr datasource update failed for "${datasourceId}"`);
103
109
  const record = await res.json();
104
- return options.select ? {
110
+ return {
105
111
  id: record.id,
106
- fields: aliasFields(record.fields, options.select)
107
- } : record;
112
+ fields: options.select ? aliasFields(record.fields, options.select) : record.fields
113
+ };
108
114
  }
109
115
  async function deleteRecord(datasourceId, id) {
110
- const res = await fetch(`${dsUrl(datasourceId)}/records/${id}`, {
116
+ await assertResponseOk(await fetch(`${dsUrl(datasourceId)}/records/${id}`, {
111
117
  method: "DELETE",
112
118
  headers: { "Authorization": `Bearer ${readSessionToken()}` }
113
- });
114
- if (!res.ok) throw new Error(`Softr datasource delete failed for "${datasourceId}": ${res.status}`);
119
+ }), `Softr datasource delete failed for "${datasourceId}"`);
115
120
  }
116
121
  async function getMetric(datasourceId, options) {
117
122
  const { select, where, aggregation, field } = options;
@@ -133,7 +138,7 @@ async function getMetric(datasourceId, options) {
133
138
  filterCriteria
134
139
  })
135
140
  });
136
- if (!res.ok) throw new Error(`Softr datasource metric failed for "${datasourceId}": ${res.status}`);
141
+ await assertResponseOk(res, `Softr datasource metric failed for "${datasourceId}"`);
137
142
  const payload = await res.json();
138
143
  const executionTimestamp = /* @__PURE__ */ new Date(payload.executionTimestamp * 1e3);
139
144
  let value;
@@ -171,7 +176,7 @@ async function getChartData(datasourceId, options) {
171
176
  yAxisFields
172
177
  })
173
178
  });
174
- if (!res.ok) throw new Error(`Softr datasource chartData failed for "${datasourceId}": ${res.status}`);
179
+ await assertResponseOk(res, `Softr datasource chartData failed for "${datasourceId}"`);
175
180
  const payload = await res.json();
176
181
  const executionTimestamp = /* @__PURE__ */ new Date(payload.executionTimestamp * 1e3);
177
182
  const aliases = {};
@@ -0,0 +1,14 @@
1
+ //#region src/core/utils.ts
2
+ async function assertResponseOk(res, message) {
3
+ if (!res.ok) {
4
+ let fullMessage = `${message}: ${res.status}`;
5
+ if (process.env.NODE_ENV !== "production") {
6
+ const body = await res.text().catch(() => "");
7
+ if (body) fullMessage += `\n\n${body}`;
8
+ }
9
+ throw new Error(fullMessage);
10
+ }
11
+ }
12
+
13
+ //#endregion
14
+ export { assertResponseOk };
@@ -0,0 +1,13 @@
1
+ import * as react0 from "react";
2
+
3
+ //#region src/default-error-overlay.d.ts
4
+ type Props = {
5
+ error: Error;
6
+ reset?: () => void;
7
+ };
8
+ declare function DefaultErrorOverlay({
9
+ error,
10
+ reset
11
+ }: Props): react0.JSX.Element;
12
+ //#endregion
13
+ export { DefaultErrorOverlay };
@@ -0,0 +1,319 @@
1
+ import { useRef, useState } from "react";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+
4
+ //#region src/default-error-overlay.tsx
5
+ function DefaultErrorOverlay({ error, reset }) {
6
+ if (process.env.NODE_ENV === "production") return /* @__PURE__ */ jsx("div", {
7
+ style: s.overlay,
8
+ children: /* @__PURE__ */ jsxs("div", {
9
+ style: s.card,
10
+ children: [
11
+ /* @__PURE__ */ jsx(AlertIcon, {}),
12
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
13
+ style: s.heading,
14
+ children: "Something went wrong"
15
+ }), /* @__PURE__ */ jsx("p", {
16
+ style: s.body,
17
+ children: "This page couldn't be shown. Try again in a moment."
18
+ })] }),
19
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(RetryButton, { onClick: reset }) })
20
+ ]
21
+ })
22
+ });
23
+ return /* @__PURE__ */ jsx(DevView, {
24
+ error,
25
+ reset
26
+ });
27
+ }
28
+ function DevView({ error, reset }) {
29
+ return /* @__PURE__ */ jsx("div", {
30
+ style: s.overlay,
31
+ children: /* @__PURE__ */ jsxs("div", {
32
+ style: s.card,
33
+ children: [
34
+ /* @__PURE__ */ jsx(AlertIcon, {}),
35
+ /* @__PURE__ */ jsxs("div", { children: [
36
+ /* @__PURE__ */ jsx("h1", {
37
+ style: s.heading,
38
+ children: "Something went wrong"
39
+ }),
40
+ /* @__PURE__ */ jsx("p", {
41
+ style: s.body,
42
+ children: "A render error was caught. Copy the details to the agent to help fix the issue."
43
+ }),
44
+ /* @__PURE__ */ jsx("p", {
45
+ style: s.note,
46
+ children: "Only shown in development mode."
47
+ })
48
+ ] }),
49
+ /* @__PURE__ */ jsx("pre", {
50
+ style: s.message,
51
+ children: error.message
52
+ }),
53
+ error.stack && /* @__PURE__ */ jsx(StackTrace, { stack: error.stack }),
54
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(RetryButton, { onClick: reset }) })
55
+ ]
56
+ })
57
+ });
58
+ }
59
+ function cleanStack(stack) {
60
+ return stack.split("\n").slice(1).filter((line) => line.trim().startsWith("at ") && !line.includes("/node_modules/")).map((line) => line.replace(/https?:\/\/[^/]+\//, "").replace(/\?[^):]+/g, "")).join("\n");
61
+ }
62
+ function StackTrace({ stack }) {
63
+ const cleaned = cleanStack(stack);
64
+ const [open, setOpen] = useState(false);
65
+ const [copied, setCopied] = useState(false);
66
+ const ref = useRef(null);
67
+ if (!cleaned) return null;
68
+ async function copy() {
69
+ try {
70
+ await navigator.clipboard.writeText(cleaned);
71
+ setCopied(true);
72
+ setTimeout(() => setCopied(false), 1500);
73
+ } catch {
74
+ if (ref.current) getSelection()?.selectAllChildren(ref.current);
75
+ }
76
+ }
77
+ return /* @__PURE__ */ jsxs("div", {
78
+ style: {
79
+ display: "flex",
80
+ flexDirection: "column",
81
+ gap: "0.5rem"
82
+ },
83
+ children: [/* @__PURE__ */ jsxs("button", {
84
+ type: "button",
85
+ style: s.toggle,
86
+ "aria-expanded": open,
87
+ onClick: () => setOpen((v) => !v),
88
+ children: [/* @__PURE__ */ jsx("svg", {
89
+ style: {
90
+ width: "0.875rem",
91
+ height: "0.875rem",
92
+ flexShrink: 0,
93
+ transition: "transform 150ms",
94
+ transform: open ? "rotate(90deg)" : "none"
95
+ },
96
+ viewBox: "0 0 24 24",
97
+ fill: "none",
98
+ stroke: "currentColor",
99
+ strokeWidth: "2",
100
+ strokeLinecap: "round",
101
+ strokeLinejoin: "round",
102
+ children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" })
103
+ }), "Stack trace"]
104
+ }), open && /* @__PURE__ */ jsxs("div", {
105
+ style: { position: "relative" },
106
+ children: [/* @__PURE__ */ jsx("pre", {
107
+ ref,
108
+ style: s.stack,
109
+ children: cleaned
110
+ }), /* @__PURE__ */ jsx("button", {
111
+ type: "button",
112
+ style: s.copy,
113
+ "aria-label": "Copy stack trace",
114
+ onClick: copy,
115
+ children: copied ? /* @__PURE__ */ jsx("svg", {
116
+ style: icon.sm,
117
+ viewBox: "0 0 24 24",
118
+ fill: "none",
119
+ stroke: "currentColor",
120
+ strokeWidth: "2",
121
+ strokeLinecap: "round",
122
+ strokeLinejoin: "round",
123
+ children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" })
124
+ }) : /* @__PURE__ */ jsxs("svg", {
125
+ style: icon.sm,
126
+ viewBox: "0 0 24 24",
127
+ fill: "none",
128
+ stroke: "currentColor",
129
+ strokeWidth: "2",
130
+ strokeLinecap: "round",
131
+ strokeLinejoin: "round",
132
+ children: [/* @__PURE__ */ jsx("rect", {
133
+ width: "14",
134
+ height: "14",
135
+ x: "8",
136
+ y: "8",
137
+ rx: "2",
138
+ ry: "2"
139
+ }), /* @__PURE__ */ jsx("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" })]
140
+ })
141
+ })]
142
+ })]
143
+ });
144
+ }
145
+ function AlertIcon() {
146
+ return /* @__PURE__ */ jsx("div", {
147
+ style: s.alert,
148
+ children: /* @__PURE__ */ jsxs("svg", {
149
+ style: icon.md,
150
+ viewBox: "0 0 24 24",
151
+ fill: "none",
152
+ stroke: "currentColor",
153
+ strokeWidth: "2",
154
+ strokeLinecap: "round",
155
+ strokeLinejoin: "round",
156
+ children: [
157
+ /* @__PURE__ */ jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }),
158
+ /* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
159
+ /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
160
+ ]
161
+ })
162
+ });
163
+ }
164
+ function RetryButton({ onClick }) {
165
+ return /* @__PURE__ */ jsxs("button", {
166
+ type: "button",
167
+ style: s.retry,
168
+ onClick,
169
+ children: [/* @__PURE__ */ jsxs("svg", {
170
+ style: icon.sm,
171
+ viewBox: "0 0 24 24",
172
+ fill: "none",
173
+ stroke: "currentColor",
174
+ strokeWidth: "2",
175
+ strokeLinecap: "round",
176
+ strokeLinejoin: "round",
177
+ children: [
178
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }),
179
+ /* @__PURE__ */ jsx("path", { d: "M21 3v5h-5" }),
180
+ /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }),
181
+ /* @__PURE__ */ jsx("path", { d: "M8 16H3v5" })
182
+ ]
183
+ }), "Try again"]
184
+ });
185
+ }
186
+ const icon = {
187
+ sm: {
188
+ width: "0.875rem",
189
+ height: "0.875rem"
190
+ },
191
+ md: {
192
+ width: "1.25rem",
193
+ height: "1.25rem"
194
+ }
195
+ };
196
+ const s = {
197
+ overlay: {
198
+ minHeight: "100vh",
199
+ display: "grid",
200
+ placeItems: "center",
201
+ gridTemplateColumns: "minmax(0, 1fr)",
202
+ padding: "1.5rem"
203
+ },
204
+ card: {
205
+ width: "100%",
206
+ maxWidth: "36rem",
207
+ display: "flex",
208
+ flexDirection: "column",
209
+ gap: "1.25rem",
210
+ padding: "1.5rem",
211
+ borderRadius: "calc(var(--radius, 0.625rem) + 4px)",
212
+ background: "var(--card, oklch(1 0 0))",
213
+ boxShadow: "0 0 0 1px color-mix(in oklch, var(--foreground, oklch(0.145 0 0)) 10%, transparent)"
214
+ },
215
+ alert: {
216
+ display: "flex",
217
+ alignItems: "center",
218
+ justifyContent: "center",
219
+ width: "2.5rem",
220
+ height: "2.5rem",
221
+ borderRadius: "var(--radius, 0.625rem)",
222
+ background: "color-mix(in oklch, var(--destructive, oklch(0.577 0.245 27.325)) 12%, transparent)",
223
+ color: "var(--destructive, oklch(0.577 0.245 27.325))",
224
+ flexShrink: 0
225
+ },
226
+ heading: {
227
+ margin: 0,
228
+ fontSize: "1rem",
229
+ fontWeight: 500,
230
+ lineHeight: 1.375
231
+ },
232
+ body: {
233
+ margin: "0.375rem 0 0",
234
+ fontSize: "0.875rem",
235
+ color: "var(--muted-foreground, oklch(0.556 0 0))"
236
+ },
237
+ note: {
238
+ margin: "0.75rem 0 0",
239
+ fontSize: "0.875rem",
240
+ color: "var(--muted-foreground, oklch(0.556 0 0))",
241
+ opacity: .65
242
+ },
243
+ message: {
244
+ margin: 0,
245
+ padding: "0.75rem",
246
+ borderRadius: "var(--radius, 0.625rem)",
247
+ background: "var(--muted, oklch(0.97 0 0))",
248
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
249
+ fontSize: "0.8125rem",
250
+ lineHeight: 1.6,
251
+ whiteSpace: "pre-wrap",
252
+ overflowWrap: "anywhere",
253
+ maxHeight: "12rem",
254
+ overflow: "auto"
255
+ },
256
+ toggle: {
257
+ display: "inline-flex",
258
+ alignItems: "center",
259
+ gap: "0.375rem",
260
+ height: "2rem",
261
+ padding: "0 0.75rem 0 0.5rem",
262
+ border: "1px solid var(--input, oklch(0.922 0 0))",
263
+ borderRadius: "var(--radius, 0.625rem)",
264
+ background: "var(--input-fill, oklch(1 0 0))",
265
+ color: "var(--primary-foreground, oklch(0.985 0 0))",
266
+ fontFamily: "inherit",
267
+ fontSize: "0.875rem",
268
+ fontWeight: 500,
269
+ cursor: "pointer",
270
+ userSelect: "none"
271
+ },
272
+ stack: {
273
+ margin: 0,
274
+ padding: "0.75rem",
275
+ paddingRight: "2.75rem",
276
+ borderRadius: "var(--radius, 0.625rem)",
277
+ background: "var(--muted, oklch(0.97 0 0))",
278
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
279
+ fontSize: "0.75rem",
280
+ lineHeight: 1.6,
281
+ maxHeight: "16rem",
282
+ overflow: "auto"
283
+ },
284
+ copy: {
285
+ position: "absolute",
286
+ top: "0.375rem",
287
+ right: "0.375rem",
288
+ display: "inline-flex",
289
+ alignItems: "center",
290
+ justifyContent: "center",
291
+ width: "1.75rem",
292
+ height: "1.75rem",
293
+ padding: 0,
294
+ border: "1px solid var(--input, oklch(0.922 0 0))",
295
+ borderRadius: "calc(var(--radius, 0.625rem) - 2px)",
296
+ background: "var(--card, oklch(1 0 0))",
297
+ color: "var(--muted-foreground, oklch(0.556 0 0))",
298
+ cursor: "pointer"
299
+ },
300
+ retry: {
301
+ display: "inline-flex",
302
+ alignItems: "center",
303
+ gap: "0.375rem",
304
+ height: "2rem",
305
+ padding: "0 0.75rem",
306
+ border: "1px solid transparent",
307
+ borderRadius: "var(--radius, 0.625rem)",
308
+ background: "var(--primary, oklch(0.205 0 0))",
309
+ color: "var(--primary-foreground, oklch(0.985 0 0))",
310
+ fontFamily: "inherit",
311
+ fontSize: "0.875rem",
312
+ fontWeight: 500,
313
+ cursor: "pointer",
314
+ userSelect: "none"
315
+ }
316
+ };
317
+
318
+ //#endregion
319
+ export { DefaultErrorOverlay };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { q } from "./core/query.js";
2
2
  import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
3
- import { AggregationFunction, ChartDataOptions, ChartDataResult, CreateOptions, DatasourcePermissions, GetOptions, JsonValue, ListOptions, ListResult, MetricOptions, MetricResult, SoftrMe, SoftrPermissions, SoftrRecord, SoftrUser, UpdateOptions } from "./types.js";
3
+ import { AggregationFunction, ChartDataOptions, ChartDataResult, CreateOptions, DatasourcePermissions, DatasourceTypeMap, GetOptions, JsonValue, ListOptions, ListResult, MetricOptions, MetricResult, RecordFieldsFor, SoftrMe, SoftrPermissions, SoftrRecord, SoftrUser, UpdateOptions } from "./types.js";
4
4
  import { Permissions } from "./auth/permissions.js";
5
5
  import { SoftrContext } from "./auth/context.js";
6
6
  import { AccessBehaviour, AccessCheck, AccessError, AccessReason, RouteVisibility, VisibilityHandle, defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
7
7
  import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions } from "./auth/isomorphic.js";
8
8
  import { useUserTimezone } from "./timezone.js";
9
- export { type AccessBehaviour, type AccessCheck, type AccessError, type AccessReason, type AggregationFunction, type ChartDataOptions, type ChartDataResult, type CreateOptions, type DatasourcePermissions, type GetOptions, type JsonValue, type ListOptions, type ListResult, type MetricOptions, type MetricResult, type Permissions, type RouteVisibility, type SoftrContext, type SoftrMe, type SoftrPermissions, type SoftrRecord, type SoftrUser, type SoftrUserGroupId, type SoftrUserGroups, type UpdateOptions, type User, type VisibilityHandle, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, q, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
9
+ import { DefaultErrorOverlay } from "./default-error-overlay.js";
10
+ export { type AccessBehaviour, type AccessCheck, type AccessError, type AccessReason, type AggregationFunction, type ChartDataOptions, type ChartDataResult, type CreateOptions, type DatasourcePermissions, type DatasourceTypeMap, DefaultErrorOverlay, type GetOptions, type JsonValue, type ListOptions, type ListResult, type MetricOptions, type MetricResult, type Permissions, type RecordFieldsFor, type RouteVisibility, type SoftrContext, type SoftrMe, type SoftrPermissions, type SoftrRecord, type SoftrUser, type SoftrUserGroupId, type SoftrUserGroups, type UpdateOptions, type User, type VisibilityHandle, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, q, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
package/dist/index.js CHANGED
@@ -2,5 +2,6 @@ import { q } from "./core/query.js";
2
2
  import { defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
3
3
  import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions } from "./auth/isomorphic.js";
4
4
  import { useUserTimezone } from "./timezone.js";
5
+ import { DefaultErrorOverlay } from "./default-error-overlay.js";
5
6
 
6
- export { defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, q, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
7
+ export { DefaultErrorOverlay, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, q, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
package/dist/types.d.ts CHANGED
@@ -26,11 +26,13 @@ type GetByIdOptions = {
26
26
  id: string;
27
27
  where?: never;
28
28
  timeZone?: string;
29
+ select?: Record<string, string>;
29
30
  };
30
31
  type GetByWhereOptions = {
31
32
  id?: never;
32
33
  where: AnyConstructable;
33
34
  timeZone?: string;
35
+ select?: Record<string, string>;
34
36
  };
35
37
  type GetOptions = GetByIdOptions | GetByWhereOptions;
36
38
  interface ListOptions {
@@ -66,6 +68,14 @@ interface ListResult<T extends SoftrRecord = SoftrRecord> {
66
68
  /** Offset to pass as `offset` in the next call, or `null` when there are no more pages. */
67
69
  nextOffset: number | null;
68
70
  }
71
+ /**
72
+ * Augment this interface in a generated datasource-types file to enable
73
+ * typed `ds.list()` / `ds.get()` return values per datasource ID.
74
+ * Maps datasource ID → raw field shape (keyed by field ID).
75
+ */
76
+ interface DatasourceTypeMap {}
77
+ type AliasedFields<Sel extends Record<string, string>, Raw extends Record<string, JsonValue>> = { [K in keyof Sel]: Sel[K] extends keyof Raw ? Raw[Sel[K]] : JsonValue };
78
+ type RecordFieldsFor<Id extends string, Sel extends Record<string, string>> = Id extends keyof DatasourceTypeMap ? [keyof Sel] extends [never] ? DatasourceTypeMap[Id] : AliasedFields<Sel, DatasourceTypeMap[Id]> : Record<string, JsonValue>;
69
79
  type AggregationFunction = 'SUM' | 'AVG' | 'MAX' | 'MIN' | 'COUNT' | 'DISTINCT';
70
80
  interface MetricOptions {
71
81
  select?: Record<string, string>;
@@ -115,4 +125,4 @@ interface UpdateOptions {
115
125
  data: Record<string, JsonValue>;
116
126
  }
117
127
  //#endregion
118
- export { AggregationFunction, ChartDataOptions, ChartDataResult, CreateOptions, DatasourcePermissions, GetOptions, JsonValue, ListOptions, ListResult, MetricOptions, MetricResult, SoftrMe, SoftrPermissions, SoftrRecord, SoftrUser, UpdateOptions };
128
+ export { AggregationFunction, ChartDataOptions, ChartDataResult, CreateOptions, DatasourcePermissions, DatasourceTypeMap, GetOptions, JsonValue, ListOptions, ListResult, MetricOptions, MetricResult, RecordFieldsFor, SoftrMe, SoftrPermissions, SoftrRecord, SoftrUser, UpdateOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
@@ -6,11 +6,12 @@ description: >-
6
6
  boundary between them, softrAuthMiddleware, getSoftrContext,
7
7
  initialSoftrContext, SoftrContext, and which sub-skill to load for route
8
8
  visibility, the current user, reading records, writing records, or auth. Load
9
- this before any other Softr SDK skill.
9
+ this when unsure which sub-skill applies, or when touching the wiring; each
10
+ sub-skill stands alone for its own task.
10
11
  metadata:
11
12
  type: core
12
13
  library: softr-sdk
13
- library_version: 0.0.22
14
+ library_version: 0.0.23
14
15
  ---
15
16
 
16
17
  # Softr SDK core
@@ -22,7 +23,7 @@ metadata:
22
23
  > **CRITICAL**: The app is already wired. `src/start.ts`, `src/router.tsx` and `src/routes/__root.tsx` register the middleware and resolve the session. Do not add a second registration.
23
24
  > **CRITICAL**: `ds.metric`, `ds.chartData`, `workflow.trigger` and `integration.fetch` are exploratory. They exist on the entrypoints below and they typecheck, but their endpoints are not final. Do not build a feature on any of them. Tell the user the capability is not ready and ask how they want to proceed.
24
25
 
25
- Load this skill, then load **one** sub-skill for the task at hand. Add a second only when the work genuinely crosses that boundary. A page that lists records behind a group restriction needs `reading-records` plus `routes-and-visibility`.
26
+ Each sub-skill stands alone for its task and repeats the boundary above, so most work needs **one** sub-skill and not this file. Load this one when you are unsure which sub-skill applies, or when the wiring below is what you are touching. Add a second sub-skill only when the work genuinely crosses that boundary: a page that lists records behind a group restriction needs `reading-records` plus `routes-and-visibility`.
26
27
 
27
28
  ## Entrypoints
28
29
 
@@ -123,4 +124,4 @@ type User = {
123
124
 
124
125
  ## Version
125
126
 
126
- Targets `@softr/sdk` v0.0.21.
127
+ Targets `@softr/sdk` v0.0.23.
@@ -10,10 +10,10 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.22
13
+ library_version: 0.0.23
14
14
  ---
15
15
 
16
- This skill builds on softr-sdk. Read it first for the entrypoints and the client/server boundary.
16
+ **Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineVisibility`, the hooks and every type come from `@softr/sdk`. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
17
17
 
18
18
  # Auth
19
19
 
@@ -10,10 +10,10 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.22
13
+ library_version: 0.0.23
14
14
  ---
15
15
 
16
- This skill builds on softr-sdk. Read it first for the entrypoints and the wiring that puts a visitor on route context.
16
+ **Boundary.** `useCurrentUser`, `usePermissions` and every type come from `@softr/sdk` and work anywhere; `ds` and `auth` live on `@softr/sdk/server` and run only inside a `createServerFn` handler. The app is already wired — `src/start.ts` registers `softrAuthMiddleware`, `src/routes/__root.tsx` publishes the visitor as route context — never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
17
17
 
18
18
  # The current user
19
19
 
@@ -8,14 +8,14 @@ description: >-
8
8
  metadata:
9
9
  type: sub-skill
10
10
  library: softr-sdk
11
- library_version: 0.0.22
11
+ library_version: 0.0.23
12
12
  ---
13
13
 
14
- This skill builds on softr-sdk. Read it first for the entrypoints and the client/server boundary.
14
+ **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `q` and every type come from `@softr/sdk` and can be imported anywhere. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
15
15
 
16
16
  # Writing records
17
17
 
18
- `ds` writes records to a Softr datasource. Like every read, a write lives on `@softr/sdk/server` and runs on the server only, so it happens inside a `createServerFn` handler and never in a component.
18
+ `ds` writes records to a Softr datasource, from the server only.
19
19
 
20
20
  ## Setup
21
21
 
@@ -49,7 +49,7 @@ const createProject = createServerFn({ method: 'POST' })
49
49
  )
50
50
  ```
51
51
 
52
- Returns the created record as `{ id, fields }`.
52
+ Resolves to `{ id, fields }`, and on a Softr table `fields` holds **only the keys you wrote** — not the rest of the record. See "What a write answers with" below.
53
53
 
54
54
  **The payload key is `data`, not `fields`.** A record answers with `fields`, which is where the confusion comes from, but the option you pass is `data`. `CreateOptions` requires it, so a call keyed by `fields` does not type-check.
55
55
 
@@ -73,7 +73,24 @@ const renameProject = createServerFn({ method: 'POST' })
73
73
 
74
74
  The record id is its own option, so keep it out of `data`.
75
75
 
76
- Returns the updated record. `update` writes the keys in `data` and leaves every other field alone, so a field you leave out is a field you are not writing. An empty `id` throws.
76
+ `update` writes the keys in `data` and leaves every other field alone, so a field you leave out is a field you are not writing. An empty `id` throws.
77
+
78
+ ### What a write answers with
79
+
80
+ `create` and `update` resolve to `{ id, fields }`, but `fields` is **not the record**. On a Softr table it carries only the keys you sent, aliased through `select`; a status change answers with `{ id, fields: { status } }` and nothing else. (Other datasource types may answer with more. Rely on neither.)
81
+
82
+ So the answer is a receipt, not a replacement for what the page holds. Putting it where a loader-fetched record was blanks every column you did not write. Two honest ways to show the result:
83
+
84
+ - **A quick change to one field** — a status select, a card dragged to another column, an archive toggle — is optimistic: change that one field in your own state immediately, keep the rest of the record as the loader gave it, save in the background, and on a rejected write restore the old value with a clear message. Do not `router.invalidate()` or re-fetch after a successful write; the local change is already what the user sees.
85
+ - **A saved form, or anything that changes several fields**, refreshes the route's data — `router.invalidate()`, see `@tanstack/router-core#router-core/data-loading` — so the list or detail re-reads through its own `select`.
86
+
87
+ Do not re-read the one record with `ds.get` for either: it takes no `select`, so it answers with raw field ids.
88
+
89
+ ### Writing a choice field
90
+
91
+ A `SELECT` or `LINKED_RECORD` field reads back as `{ id, label }` (see [field-types](../reading-records/references/field-types.md)). Writing one accepts the option's `id`, its `label`, or `{ id, label }`, matched case-insensitively.
92
+
93
+ **A value that matches no option is not an error — it silently adds a new option to the field.** A typo'd label leaves a junk choice in the schema. So write `{ label }` (or the `{ id, label }` you read), copy labels from the table's schema verbatim, and never invent one.
77
94
 
78
95
  ### Delete a record
79
96
 
@@ -113,8 +130,6 @@ Stamping a record with who created it, or scoping an update to the person making
113
130
 
114
131
  **Expecting `select` on a read-then-write round trip.** `ds.get` takes no `select`, so a record fetched by id answers with raw field ids. Load `reading-records` before pairing a read with a write.
115
132
 
116
- **A write outside a server function.** `ds` is server-only. Importing it into a component fails the build.
117
-
118
133
  **Reading the visitor from somewhere other than `context`.** There is nothing to import. See [current-user](../current-user/SKILL.md).
119
134
 
120
135
  ## References
@@ -10,14 +10,14 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.22
13
+ library_version: 0.0.23
14
14
  ---
15
15
 
16
- This skill builds on softr-sdk. Read it first for the entrypoints and the client/server boundary.
16
+ **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `q` and every type come from `@softr/sdk` and can be imported anywhere. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
17
17
 
18
18
  # Reading records
19
19
 
20
- `ds` reads records out of a Softr datasource. It lives on `@softr/sdk/server` and runs on the server only, so every read happens inside a `createServerFn` handler or a server-side route hook, never in a component.
20
+ `ds` reads records out of a Softr datasource, from the server only.
21
21
 
22
22
  ## Setup
23
23
 
@@ -27,8 +27,6 @@ import { ds } from '@softr/sdk/server'
27
27
  import { q } from '@softr/sdk'
28
28
  ```
29
29
 
30
- `ds` is server-only and lives on `@softr/sdk/server`. `q` builds queries and nothing more, so it ships on the root entrypoint and can be imported anywhere.
31
-
32
30
  Every `ds` call names its datasource by an opaque UUID from the table's registration on the application (Softr Studio's Data tab, or the `register_application_data_source` tool when an agent does it). It is not the table id, and it is never derived from the table's name — `'ds_posts'` or a name glued to a uuid fails at request time as not found or forbidden.
33
31
 
34
32
  **The datasource id must be a string literal at the call site.** Do not assign it to a variable and pass the variable — the build reads ids statically and cannot resolve a name. Write the UUID directly as the first argument:
@@ -12,10 +12,10 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.22
15
+ library_version: 0.0.23
16
16
  ---
17
17
 
18
- This skill builds on softr-sdk. Read it first for the entrypoints and the wiring that puts a visitor on route context.
18
+ **Boundary.** `defineVisibility`, the error predicates and every type come from `@softr/sdk` and work anywhere; `ds` and `auth` live on `@softr/sdk/server` and run only inside a `createServerFn` handler. The app is already wired — `src/start.ts` registers `softrAuthMiddleware`, `src/routes/__root.tsx` publishes the visitor as route context — never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
19
19
 
20
20
  # Route visibility
21
21
 
@@ -82,7 +82,7 @@ export const Route = createFileRoute('/_app')({
82
82
 
83
83
  The whole match chain runs, so a layout guards everything beneath it. Sibling routes do not guard each other. `/admin/index.tsx` does nothing for `/admin/users.tsx` unless an `admin.tsx` layout exists. Routes below a guarded layout still declare their own rule, because the declaration is what Studio reads.
84
84
 
85
- A thrown error renders in place of the guarded route's component. Guard the layout that draws your nav and chrome, and the error screen replaces the chrome along with it. Put the guard on a layout inside the shell rather than on the shell itself.
85
+ A thrown error renders in place of the guarded route's component. The template's `_app.tsx` shell is guarded with `LOGGED_IN_USERS`, whose default for a signed-out visitor is a redirect, so nothing ever renders there. A rule that can *throw* — a `userGroupIds` rule, whose `forbidden` default is throw — should not sit on the shell itself, or the refusal replaces the nav and chrome. Put it on a layout inside the shell, or on the restricted routes, so the refusal renders inside the chrome.
86
86
 
87
87
  ### Change what a denied visitor gets
88
88