@softr/sdk 0.0.74-alpha.0 → 0.0.74

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.
@@ -5,7 +5,7 @@ import { AuthError, Result } from "./utils.js";
5
5
 
6
6
  /**
7
7
  * Changes the signed-in visitor's own name, photo, or both. A field left out
8
- * stays as it is, and `avatar: null` removes the photo.
8
+ * stays as it is. {@link removeAvatar} removes the photo.
9
9
  *
10
10
  * The photo can change only where `user.avatarEditable` is true. Elsewhere a
11
11
  * photo answers `avatarNotSupported`, so hide the photo controls instead.
@@ -20,7 +20,16 @@ import { AuthError, Result } from "./utils.js";
20
20
  */
21
21
  declare function updateProfile(changes: {
22
22
  fullName?: string;
23
- avatar?: File | null;
23
+ avatar?: File;
24
24
  }): Promise<Result<User, AuthError>>;
25
+ /**
26
+ * Removes the signed-in visitor's own photo. Like {@link updateProfile}, it
27
+ * works only where `user.avatarEditable` is true, and answers with the visitor
28
+ * as they are now.
29
+ *
30
+ * @example
31
+ * const result = await auth.removeAvatar()
32
+ */
33
+ declare function removeAvatar(): Promise<Result<User, AuthError>>;
25
34
  //#endregion
26
- export { updateProfile };
35
+ export { removeAvatar, updateProfile };
@@ -7,7 +7,7 @@ import { authError } from "./utils.js";
7
7
  //#region src/auth/methods/profile.ts
8
8
  /**
9
9
  * Changes the signed-in visitor's own name, photo, or both. A field left out
10
- * stays as it is, and `avatar: null` removes the photo.
10
+ * stays as it is. {@link removeAvatar} removes the photo.
11
11
  *
12
12
  * The photo can change only where `user.avatarEditable` is true. Elsewhere a
13
13
  * photo answers `avatarNotSupported`, so hide the photo controls instead.
@@ -26,9 +26,23 @@ async function updateProfile(changes) {
26
26
  if (token === null) return authError("notAuthenticated", "Nobody is signed in.");
27
27
  const form = new FormData();
28
28
  if (changes.fullName !== void 0) form.set("fullName", changes.fullName);
29
- if (changes.avatar === null) form.set("removeAvatar", "true");
30
29
  if (changes.avatar) form.set("avatar", changes.avatar);
31
- const result = await makePlatform(sessionConfig().applicationId).updateProfile(token, form);
30
+ return answer(await makePlatform(sessionConfig().applicationId).updateProfile(token, form));
31
+ }
32
+ /**
33
+ * Removes the signed-in visitor's own photo. Like {@link updateProfile}, it
34
+ * works only where `user.avatarEditable` is true, and answers with the visitor
35
+ * as they are now.
36
+ *
37
+ * @example
38
+ * const result = await auth.removeAvatar()
39
+ */
40
+ async function removeAvatar() {
41
+ const token = readSessionToken();
42
+ if (token === null) return authError("notAuthenticated", "Nobody is signed in.");
43
+ return answer(await makePlatform(sessionConfig().applicationId).removeAvatar(token));
44
+ }
45
+ function answer(result) {
32
46
  if (!result.ok) {
33
47
  switch (result.status) {
34
48
  case 400: return authError("invalidName", result.message);
@@ -49,4 +63,4 @@ async function updateProfile(changes) {
49
63
  }
50
64
 
51
65
  //#endregion
52
- export { updateProfile };
66
+ export { removeAvatar, updateProfile };
@@ -272,6 +272,10 @@ function makePlatform(applicationId) {
272
272
  token,
273
273
  form
274
274
  }, userSchema),
275
+ removeAvatar: (token) => request(`/v1/vibe/applications/${applicationId}/users/me/profile/avatar`, {
276
+ method: "DELETE",
277
+ token
278
+ }, userSchema),
275
279
  signOut: (token) => request(`/v1/applications/${applicationId}/users/sign-out`, {
276
280
  method: "POST",
277
281
  token
@@ -6,7 +6,7 @@ import { checkPasswordLink, requestPasswordReset, setPasswordWithLink } from "./
6
6
  import { settings } from "./methods/settings.js";
7
7
  import { confirmSignUp, signUp } from "./methods/signup.js";
8
8
  import { signInWithGoogle } from "./methods/social.js";
9
- import { updateProfile } from "./methods/profile.js";
9
+ import { removeAvatar, updateProfile } from "./methods/profile.js";
10
10
  import { currentUserRow } from "./current-user-row.js";
11
11
  import "@tanstack/react-start/server-only";
12
12
 
@@ -36,6 +36,7 @@ declare const auth: {
36
36
  removeAuthenticator: typeof removeAuthenticator;
37
37
  settings: typeof settings;
38
38
  updateProfile: typeof updateProfile;
39
+ removeAvatar: typeof removeAvatar;
39
40
  currentUserRow: typeof currentUserRow;
40
41
  };
41
42
  //#endregion
@@ -6,7 +6,7 @@ import { signIn, signOut } from "./methods/password.js";
6
6
  import { checkPasswordLink, requestPasswordReset, setPasswordWithLink } from "./methods/passwordLink.js";
7
7
  import { confirmSignUp, signUp } from "./methods/signup.js";
8
8
  import { signInWithGoogle } from "./methods/social.js";
9
- import { updateProfile } from "./methods/profile.js";
9
+ import { removeAvatar, updateProfile } from "./methods/profile.js";
10
10
  import { currentUserRow } from "./current-user-row.js";
11
11
  import "@tanstack/react-start/server-only";
12
12
 
@@ -35,6 +35,7 @@ const auth = {
35
35
  removeAuthenticator,
36
36
  settings,
37
37
  updateProfile,
38
+ removeAvatar,
38
39
  currentUserRow
39
40
  };
40
41
 
@@ -161,7 +161,7 @@ function writeValue(shape, context) {
161
161
  case "UPDATED_AT": return "IsoDateTime";
162
162
  case "TIME": return "IsoTime";
163
163
  case "DATE_RANGE": return "{ from: IsoDate; to: IsoDate }";
164
- case "SELECT": return "SelectChoiceInput";
164
+ case "SELECT": return selectInput(shape.options);
165
165
  case "ATTACHMENT": return "AttachmentInput";
166
166
  case "LINKED_RECORD": return "LinkedRecordInput";
167
167
  case "USER": return "WorkspaceUser";
@@ -208,6 +208,19 @@ function selectValue(options) {
208
208
  if (choices === null) return "SelectChoice";
209
209
  return union(choices.map(({ id, label }) => `{ id: ${quote(id)}; label: ${quote(label)} }`));
210
210
  }
211
+ /**
212
+ * A write carries the id alone. The API resolves it against the choice list and writes that
213
+ * choice's own label, so a label sent alongside is dropped.
214
+ *
215
+ * The API also takes `{ label }` with no id, and a label matching nothing creates a choice.
216
+ * The union omits that: it would make every misspelling type-check, and the cost of one is a
217
+ * new choice in the field rather than an error.
218
+ */
219
+ function selectInput(options) {
220
+ const choices = choiceEntries(options);
221
+ if (choices === null) return "SelectChoiceInput";
222
+ return union(choices.map(({ id }) => `{ id: ${quote(id)} }`));
223
+ }
211
224
  function datePrecision(options) {
212
225
  const precision = option(options, "precision");
213
226
  return typeof precision === "string" ? precision : null;
@@ -1,22 +1,7 @@
1
- import { ActionShape, DatasourceAction, ReadShape } from "../action.js";
1
+ import { DatasourceAction, ReadShape } from "../action.js";
2
2
  import "@tanstack/react-start/server-only";
3
3
 
4
4
  //#region src/core/options.d.ts
5
- /** One choice of a SELECT field, as the field holds it now. */
6
- type FieldChoice = {
7
- id: string;
8
- label: string;
9
- color: string | null;
10
- };
11
- /**
12
- * The choices a SELECT field holds right now, read from Softr on every call. A choice the
13
- * builder adds or renames later shows up without a new build. The field must be one the
14
- * action declares. Any query or mutation over the table works.
15
- *
16
- * @example
17
- * const loadStatuses = createServerFn().handler(() => ds.choices(listProjects, 'status'))
18
- */
19
- declare function listChoices<TShape extends ActionShape>(action: DatasourceAction<TShape>, field: keyof TShape["fields"] & string): Promise<Array<FieldChoice>>;
20
5
  /** One row a linked-record picker offers. */
21
6
  type RecordOption = {
22
7
  id: string;
@@ -48,4 +33,4 @@ declare function listOptions<TShape extends ReadShape>(query: DatasourceAction<T
48
33
  limit?: number;
49
34
  }): Promise<RecordOptions>;
50
35
  //#endregion
51
- export { FieldChoice, RecordOption, RecordOptions, listChoices, listOptions };
36
+ export { RecordOption, RecordOptions, listOptions };
@@ -1,37 +1,9 @@
1
- import { assert } from "../assertions.js";
2
- import { fetch } from "../fetch.js";
3
- import { readSessionToken } from "../auth/session.js";
4
1
  import { makeChain } from "../datasource/fields.js";
5
- import { describeFailure, dsUrl, listRecords, refererHeader, resolveTarget } from "../datasource/record.js";
2
+ import { listRecords } from "../datasource/record.js";
6
3
  import "@tanstack/react-start/server-only";
7
4
 
8
5
  //#region src/core/options.ts
9
6
  /**
10
- * The choices a SELECT field holds right now, read from Softr on every call. A choice the
11
- * builder adds or renames later shows up without a new build. The field must be one the
12
- * action declares. Any query or mutation over the table works.
13
- *
14
- * @example
15
- * const loadStatuses = createServerFn().handler(() => ds.choices(listProjects, 'status'))
16
- */
17
- async function listChoices(action, field) {
18
- const { actionId, select } = resolveTarget(action, action.resource.operation);
19
- const fieldId = select[field];
20
- assert(fieldId, () => `action "${actionId}" declares no field "${field}"`);
21
- const res = await fetch(`${dsUrl(actionId)}/metadata`, { headers: {
22
- Authorization: `Bearer ${readSessionToken()}`,
23
- ...refererHeader()
24
- } });
25
- if (!res.ok) throw new Error(`datasource metadata failed for "${actionId}": ${await describeFailure(res)}`);
26
- const metadataField = (await res.json()).fields.find(({ id }) => id === fieldId);
27
- assert(metadataField?.type === "SELECT", () => `"${field}" on action "${actionId}" is not a SELECT field`);
28
- return (metadataField.options?.choices ?? []).map(({ id, label, color }) => ({
29
- id,
30
- label,
31
- color: color ?? null
32
- }));
33
- }
34
- /**
35
7
  * The rows a linked field can point at, for a picker: each record of the query as its id and the text
36
8
  * of one field, searched on that field when a term is given, a page at a time.
37
9
  *
@@ -64,4 +36,4 @@ function labelOf(value) {
64
36
  }
65
37
 
66
38
  //#endregion
67
- export { listChoices, listOptions };
39
+ export { listOptions };
@@ -1,5 +1,5 @@
1
1
  import { createRecord, deleteRecord, getRecord, listRecords, updateRecord } from "./record.js";
2
- import { listChoices, listOptions } from "../core/options.js";
2
+ import { listOptions } from "../core/options.js";
3
3
  import { chart, metric } from "./chart.js";
4
4
  import "@tanstack/react-start/server-only";
5
5
 
@@ -13,7 +13,6 @@ declare const ds: {
13
13
  metric: typeof metric;
14
14
  chart: typeof chart;
15
15
  options: typeof listOptions;
16
- choices: typeof listChoices;
17
16
  };
18
17
  //#endregion
19
18
  export { ds };
@@ -1,6 +1,6 @@
1
1
  import { createRecord, deleteRecord, getRecord, listRecords, updateRecord } from "./record.js";
2
2
  import { chart, metric } from "./chart.js";
3
- import { listChoices, listOptions } from "../core/options.js";
3
+ import { listOptions } from "../core/options.js";
4
4
  import "@tanstack/react-start/server-only";
5
5
 
6
6
  //#region src/datasource/datasource.ts
@@ -12,8 +12,7 @@ const ds = {
12
12
  delete: deleteRecord,
13
13
  metric,
14
14
  chart,
15
- options: listOptions,
16
- choices: listChoices
15
+ options: listOptions
17
16
  };
18
17
 
19
18
  //#endregion
@@ -19,21 +19,17 @@ type NumberChain = EmptyChain & {
19
19
  lte(value: number): ConstructableFilter;
20
20
  between(start: number, end: number): ConstructableFilter;
21
21
  };
22
- type ArrayChain = EmptyChain & {
23
- is(values: Array<string>): ConstructableFilter;
24
- isOneOf(values: Array<string>): ConstructableFilter;
25
- isNoneOf(values: Array<string>): ConstructableFilter;
26
- hasAllOf(values: Array<string>): ConstructableFilter;
22
+ type ArrayChain<TMember$1 extends string> = EmptyChain & {
23
+ is(values: Array<TMember$1>): ConstructableFilter;
24
+ isOneOf(values: Array<TMember$1>): ConstructableFilter;
25
+ isNoneOf(values: Array<TMember$1>): ConstructableFilter;
26
+ hasAllOf(values: Array<TMember$1>): ConstructableFilter;
27
27
  };
28
- /**
29
- * Compares choice ids as plain strings. A select's choices change in Studio after the build,
30
- * and an id read at runtime from ds.choices has to be a valid filter value.
31
- */
32
- type ChoiceChain = EmptyChain & {
33
- is(value: string): ConstructableFilter;
34
- isNot(value: string): ConstructableFilter;
35
- isOneOf(values: Array<string>): ConstructableFilter;
36
- isNoneOf(values: Array<string>): ConstructableFilter;
28
+ type ChoiceChain<TId$1 extends string> = EmptyChain & {
29
+ is(value: TId$1): ConstructableFilter;
30
+ isNot(value: TId$1): ConstructableFilter;
31
+ isOneOf(values: Array<TId$1>): ConstructableFilter;
32
+ isNoneOf(values: Array<TId$1>): ConstructableFilter;
37
33
  };
38
34
  /** What DSv2 filters generically: matched whole or scanned, never against a set. */
39
35
  type GenericChain = EmptyChain & {
@@ -77,20 +73,32 @@ type AnyChain = EmptyChain & {
77
73
  between(start: number | string, end: number | string): ConstructableFilter;
78
74
  isNotBetween(start: number | string, end: number | string): ConstructableFilter;
79
75
  };
76
+ type IdOf<TValue> = TValue extends {
77
+ id: infer TId extends string;
78
+ } ? TId : string;
79
+ /**
80
+ * What a choice-shaped field compares against: its own ids where codegen read the choice
81
+ * list, and any string where it did not. A linked record is the open case, and it is the
82
+ * only thing distinguishing one from a select at the type level.
83
+ */
84
+ type ChoiceIds<TValue> = string extends IdOf<TValue> ? string : IdOf<TValue>;
80
85
  type TextFieldType = 'SINGLE_LINE_TEXT' | 'LONG_TEXT' | 'URL' | 'EMAIL' | 'PHONE' | 'ADDRESS' | 'RECORD_ID' | 'LOCATION' | 'TIME' | 'BUTTON';
81
86
  type NumberFieldType = 'NUMBER' | 'PERCENT' | 'CURRENCY' | 'PROGRESS' | 'RATING' | 'DURATION' | 'COUNT' | 'AUTONUMBER';
82
87
  type DateFieldType = 'DATE' | 'DATETIME' | 'CREATED_AT' | 'UPDATED_AT';
83
88
  type ChoiceFieldType = 'SELECT' | 'LINKED_RECORD' | 'CREATED_BY' | 'UPDATED_BY';
84
89
  /** The types with no single value to compare: only whether the field holds one. */
85
90
  type EmptyFieldType = 'ATTACHMENT' | 'DATE_RANGE';
91
+ type ElementOf<TValue> = TValue extends Array<infer TMember> ? TMember : TValue;
86
92
  /**
87
93
  * The operators one alias offers, chosen from the type codegen declared for the field rather
88
94
  * than from what it reads as: a `DATETIME` and a text field both read as `string`, and only
89
95
  * the declared type tells them apart.
90
96
  *
91
- * A field with no declared type lands on the permissive chain rather than on none.
97
+ * `TValue` is still needed for the literal choice ids, which live on the read type and
98
+ * nowhere else. A field with no declared type lands on the permissive chain rather than on
99
+ * none.
92
100
  */
93
- type ChainFor<TFieldType, TValue> = [TFieldType] extends [never] ? AnyChain : TFieldType extends `${infer TBase}[]` ? TBase extends EmptyFieldType ? EmptyChain : ArrayChain : TFieldType extends TextFieldType ? TextChain<TValue & string> : TFieldType extends NumberFieldType ? NumberChain : TFieldType extends 'USER' ? GenericChain : TFieldType extends 'CHECKBOX' ? BooleanChain : TFieldType extends DateFieldType ? DateChain : TFieldType extends ChoiceFieldType ? ChoiceChain : TFieldType extends EmptyFieldType ? EmptyChain : AnyChain;
101
+ type ChainFor<TFieldType, TValue> = [TFieldType] extends [never] ? AnyChain : TFieldType extends `${infer TBase}[]` ? TBase extends EmptyFieldType ? EmptyChain : ArrayChain<ChoiceIds<ElementOf<TValue>>> : TFieldType extends TextFieldType ? TextChain<TValue & string> : TFieldType extends NumberFieldType ? NumberChain : TFieldType extends 'USER' ? GenericChain : TFieldType extends 'CHECKBOX' ? BooleanChain : TFieldType extends DateFieldType ? DateChain : TFieldType extends ChoiceFieldType ? ChoiceChain<ChoiceIds<TValue>> : TFieldType extends EmptyFieldType ? EmptyChain : AnyChain;
94
102
  type Combinators = {
95
103
  $and(...members: Array<AnyConstructable>): ConstructableFilterGroup;
96
104
  $or(...members: Array<AnyConstructable>): ConstructableFilterGroup;
@@ -1,4 +1,4 @@
1
- import * as react2 from "react";
1
+ import * as react0 from "react";
2
2
  import { ReactNode } from "react";
3
3
  import { z } from "zod";
4
4
 
@@ -94,7 +94,7 @@ declare function FormattingScope({
94
94
  /** A BCP 47 locale, such as `de-DE`. It drops the app's fixed date format inside the scope. */
95
95
  locale?: string;
96
96
  children?: ReactNode;
97
- }): react2.FunctionComponentElement<react2.ProviderProps<Scope>>;
97
+ }): react0.FunctionComponentElement<react0.ProviderProps<Scope>>;
98
98
  /**
99
99
  * A date in the app's format, safe to render on the server. A timestamp whose
100
100
  * zone is not known yet renders `fallback` until the browser supplies it, so
@@ -115,6 +115,6 @@ declare function FormattedDate({
115
115
  time?: boolean;
116
116
  /** What renders while the visitor's zone is unknown. */
117
117
  fallback?: ReactNode;
118
- }): react2.FunctionComponentElement<react2.FragmentProps>;
118
+ }): react0.FunctionComponentElement<react0.FragmentProps>;
119
119
  //#endregion
120
120
  export { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting };
package/dist/mcp.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DatasourceAction } from "./action.js";
2
- import * as _tanstack_react_start0 from "@tanstack/react-start";
2
+ import * as _tanstack_react_start5 from "@tanstack/react-start";
3
3
 
4
4
  //#region src/mcp.d.ts
5
5
 
@@ -31,6 +31,6 @@ type SoftrMcpOptions = {
31
31
  * ],
32
32
  * }))
33
33
  */
34
- declare function softrMcpMiddleware(options?: SoftrMcpOptions): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>;
34
+ declare function softrMcpMiddleware(options?: SoftrMcpOptions): _tanstack_react_start5.RequestMiddlewareAfterServer<{}, undefined, undefined>;
35
35
  //#endregion
36
36
  export { softrMcpMiddleware };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.74-alpha.0";
2
+ var version = "0.0.74";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/dist/pwa.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as react0 from "react";
1
+ import * as react4 from "react";
2
2
  import "@tanstack/react-start";
3
3
 
4
4
  //#region src/pwa.d.ts
@@ -23,7 +23,7 @@ import "@tanstack/react-start";
23
23
  * )
24
24
  * }
25
25
  */
26
- declare function SoftrPwaHead(): react0.FunctionComponentElement<react0.FragmentProps>;
26
+ declare function SoftrPwaHead(): react4.FunctionComponentElement<react4.FragmentProps>;
27
27
  type PwaPlatform = "ios" | "android" | "desktop";
28
28
  type PwaInstall = {
29
29
  available: false;
package/dist/server.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { upload } from "./core/file-upload.js";
2
2
  import { auth } from "./auth/server.js";
3
- import { FieldChoice, RecordOption, RecordOptions } from "./core/options.js";
3
+ import { RecordOption, RecordOptions } from "./core/options.js";
4
4
  import { ds } from "./datasource/datasource.js";
5
5
  import { workflow } from "./core/workflow.js";
6
6
  import { integration } from "./core/integration.js";
7
7
  import "@tanstack/react-start/server-only";
8
- export { type FieldChoice, type RecordOption, type RecordOptions, auth, ds, integration, upload, workflow };
8
+ export { type RecordOption, type RecordOptions, auth, ds, integration, upload, workflow };
package/package.json CHANGED
@@ -1,37 +1,16 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.74-alpha.0",
3
+ "version": "0.0.74",
4
4
  "type": "module",
5
5
  "exports": {
6
- ".": {
7
- "types": "./dist/index.d.ts",
8
- "import": "./dist/index.js"
9
- },
10
- "./client": {
11
- "types": "./dist/client.d.ts",
12
- "import": "./dist/client.js"
13
- },
14
- "./server": {
15
- "types": "./dist/server.d.ts",
16
- "import": "./dist/server.js"
17
- },
18
- "./vite": {
19
- "types": "./dist/vite.d.ts",
20
- "import": "./dist/vite.js"
21
- }
22
- },
23
- "bin": {
24
- "softr-sdk": "./dist/cli.js"
25
- },
26
- "files": [
27
- "dist",
28
- "schema",
29
- "skills",
30
- "!skills/_artifacts"
31
- ],
32
- "publishConfig": {
33
- "access": "public"
34
- },
6
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
7
+ "./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js" },
8
+ "./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js" },
9
+ "./vite": { "types": "./dist/vite.d.ts", "import": "./dist/vite.js" }
10
+ },
11
+ "bin": { "softr-sdk": "./dist/cli.js" },
12
+ "files": ["dist", "schema", "skills", "!skills/_artifacts"],
13
+ "publishConfig": { "access": "public" },
35
14
  "scripts": {
36
15
  "build": "tsdown",
37
16
  "test": "node --import ./src/test-utils/setup.ts --test",
@@ -63,9 +42,7 @@
63
42
  "optional": true
64
43
  }
65
44
  },
66
- "keywords": [
67
- "tanstack-intent"
68
- ],
45
+ "keywords": ["tanstack-intent"],
69
46
  "intent": {
70
47
  "version": 1,
71
48
  "repo": "@softr/sdk",
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: core
14
14
  library: softr-sdk
15
- library_version: 0.0.71
15
+ library_version: 0.0.74
16
16
  ---
17
17
 
18
18
  # Softr SDK core
@@ -18,7 +18,7 @@ description: >-
18
18
  metadata:
19
19
  type: sub-skill
20
20
  library: softr-sdk
21
- library_version: 0.0.71
21
+ library_version: 0.0.74
22
22
  ---
23
23
 
24
24
  **Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
@@ -85,7 +85,8 @@ Changing the visitor's own profile:
85
85
 
86
86
  | Call | Does |
87
87
  | --- | --- |
88
- | `auth.updateProfile({ fullName?, avatar? })` | Changes their name, their photo, or both. `avatar: null` removes the photo |
88
+ | `auth.updateProfile({ fullName?, avatar? })` | Changes their name, their photo, or both |
89
+ | `auth.removeAvatar()` | Removes their photo |
89
90
 
90
91
  ## Setup
91
92
 
@@ -461,7 +462,7 @@ After a reset, enrol the new app with `startAuthenticatorSetup` and `confirmAuth
461
462
  ```ts
462
463
  const result = await auth.updateProfile({ fullName: 'Ann Lee' })
463
464
  const result = await auth.updateProfile({ avatar: file }) // a File from an <input type="file">
464
- const result = await auth.updateProfile({ avatar: null }) // removes the photo
465
+ const result = await auth.removeAvatar() // removes the photo
465
466
  ```
466
467
 
467
468
  It answers with the updated `User`. Pass the file through a server function that takes `FormData`. The photo must be a PNG, JPEG or GIF of at most 5 MB, or it answers `notAnImage` or `fileTooLarge`. A blank or unacceptable name answers `invalidName`.
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.71
15
+ library_version: 0.0.74
16
16
  ---
17
17
 
18
18
  **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. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.71
12
+ library_version: 0.0.74
13
13
  ---
14
14
 
15
15
  # The current user
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.71
13
+ library_version: 0.0.74
14
14
  ---
15
15
 
16
16
  **Boundary.** `upload` lives on `@softr/sdk/server` and runs inside server functions. `useFileUpload` and `FileItem` live on `@softr/sdk` and run in components. `FileAttachment` comes from `@softr/sdk` and can be imported anywhere.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.71
13
+ library_version: 0.0.74
14
14
  ---
15
15
 
16
16
  # Dates, times and numbers
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.71
13
+ library_version: 0.0.74
14
14
  ---
15
15
 
16
16
  **Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineIntegration` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.71
12
+ library_version: 0.0.74
13
13
  ---
14
14
 
15
15
  **Boundary.** `softrMcpMiddleware` lives on `@softr/sdk` and is registered once, in `src/start.ts`. The actions you hand it are the same `defineQuery` / `defineMutation` values the app's own pages use — declared wherever they already live, not duplicated for MCP.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.71
12
+ library_version: 0.0.74
13
13
  ---
14
14
 
15
15
  # Installing the app
@@ -11,7 +11,7 @@ description: >-
11
11
  metadata:
12
12
  type: sub-skill
13
13
  library: softr-sdk
14
- library_version: 0.0.71
14
+ library_version: 0.0.74
15
15
  ---
16
16
 
17
17
  **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineMutation` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -155,7 +155,7 @@ A form select for a `LINKED_RECORD` field needs its options from the linked tabl
155
155
 
156
156
  Each key of `data` accepts exactly what its field takes (see [record-queries](../record-queries/SKILL.md#typing)):
157
157
 
158
- - SELECT and LINKED_RECORD take `{ id }`. Not a bare string, and not the `{ id, label }` a read returns; the label is display data and a write does not accept it. For a form select that should follow the field's choices in Studio, read them with `ds.choices` (see [record-queries](../record-queries/SKILL.md)).
158
+ - SELECT and LINKED_RECORD take `{ id }`. Not a bare string, and not the `{ id, label }` a read returns; the label is display data and a write does not accept it. A generated SELECT narrows the id to the field's own choices.
159
159
  - CHECKBOX takes a boolean. Dates take ISO strings, never `Date` instances.
160
160
  - ATTACHMENT takes `{ url }`, optionally with `filename`, `id` and `type`.
161
161
  - A multi-entry field takes an array of the value.
@@ -13,7 +13,7 @@ description: >-
13
13
  metadata:
14
14
  type: sub-skill
15
15
  library: softr-sdk
16
- library_version: 0.0.71
16
+ library_version: 0.0.74
17
17
  ---
18
18
 
19
19
  **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. The same holds for any helper that calls `ds`: declare it inside the handler. A function at module level that calls `ds` keeps the server-only import in the browser bundle, and every page stops loading until it moves. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -200,12 +200,6 @@ const loadProductStatuses = createServerFn()
200
200
 
201
201
  Skip this request when the options are predefined and fixed — hardcode them instead.
202
202
 
203
- A `SELECT` field's options are its choices, and the builder can add and rename them in Studio after the app is built. When a dropdown should follow those changes, read them with `ds.choices`. When the app means a fixed choice, such as a "Done" tab, use its id directly. A mistyped id is not caught at compile time, and a write with an id the field lacks adds it as a new choice. `ds.choices` takes any query or mutation that declares the field and answers `[{ id, label, color }]` as the field holds them now. Each `id` is what a `where` and a write take, for a filter dropdown and a form select alike:
204
-
205
- ```ts
206
- const loadStatuses = createServerFn().handler(() => ds.choices(listProjects, 'status'))
207
- ```
208
-
209
203
  A read the datasource turns down throws rather than coming back empty, and it throws a plain `Error` carrying the status, followed by the reason the datasource gives when it sends one. There is no typed predicate to narrow it with. Handle it where you would handle any other failed fetch: an `errorComponent` on the route, or a check in the handler.
210
204
 
211
205
  ## Typing
@@ -83,7 +83,7 @@ Every value is an ISO date string, either `'2026-01-31'` or a full timestamp. A
83
83
  | `.isOneOf(values)` / `.isNoneOf(values)` | Any of / none of |
84
84
  | `.isEmpty()` / `.isNotEmpty()` | No value / has a value |
85
85
 
86
- Values are choice ids, never labels. A SELECT takes any choice id as a string, so a filter dropdown can pass the ids `ds.choices` returns. A label still matches nothing. A LINKED_RECORD compares against the linked record's id.
86
+ Values are choice ids, never labels. A generated SELECT narrows the argument to the field's own choice ids, so a label or a typo is a type error. A LINKED_RECORD compares against the linked record's id.
87
87
 
88
88
  ### Multi-value fields
89
89
 
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.71
12
+ library_version: 0.0.74
13
13
  ---
14
14
 
15
15
  **Boundary.** `Audience` and `defineVisibility` come from `@softr/sdk` and work anywhere.
@@ -13,7 +13,7 @@ description: >-
13
13
  metadata:
14
14
  type: sub-skill
15
15
  library: softr-sdk
16
- library_version: 0.0.71
16
+ library_version: 0.0.74
17
17
  ---
18
18
 
19
19
  **Boundary.** `defineVisibility`, the access functions and the error predicates come from `@softr/sdk` and work anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.71
12
+ library_version: 0.0.74
13
13
  ---
14
14
 
15
15
  **Boundary.** `workflow` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineWorkflow` and every type come from `@softr/sdk` and can be imported anywhere.