@softr/sdk 0.0.75 → 0.0.77

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.
@@ -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 selectInput(shape.options);
164
+ case "SELECT": return "SelectChoiceInput";
165
165
  case "ATTACHMENT": return "AttachmentInput";
166
166
  case "LINKED_RECORD": return "LinkedRecordInput";
167
167
  case "USER": return "WorkspaceUser";
@@ -208,19 +208,6 @@ 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
- }
224
211
  function datePrecision(options) {
225
212
  const precision = option(options, "precision");
226
213
  return typeof precision === "string" ? precision : null;
@@ -1,7 +1,22 @@
1
- import { DatasourceAction, ReadShape } from "../action.js";
1
+ import { ActionShape, 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 loadCategories = createServerFn().handler(() => ds.choices(products, 'category'))
18
+ */
19
+ declare function listChoices<TShape extends ActionShape>(action: DatasourceAction<TShape>, field: keyof TShape["fields"] & string): Promise<Array<FieldChoice>>;
5
20
  /** One row a linked-record picker offers. */
6
21
  type RecordOption = {
7
22
  id: string;
@@ -33,4 +48,4 @@ declare function listOptions<TShape extends ReadShape>(query: DatasourceAction<T
33
48
  limit?: number;
34
49
  }): Promise<RecordOptions>;
35
50
  //#endregion
36
- export { RecordOption, RecordOptions, listOptions };
51
+ export { FieldChoice, RecordOption, RecordOptions, listChoices, listOptions };
@@ -1,9 +1,37 @@
1
+ import { assert } from "../assertions.js";
2
+ import { fetch } from "../fetch.js";
3
+ import { readSessionToken } from "../auth/session.js";
1
4
  import { makeChain } from "../datasource/fields.js";
2
- import { listRecords } from "../datasource/record.js";
5
+ import { describeFailure, dsUrl, listRecords, refererHeader, resolveTarget } from "../datasource/record.js";
3
6
  import "@tanstack/react-start/server-only";
4
7
 
5
8
  //#region src/core/options.ts
6
9
  /**
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 loadCategories = createServerFn().handler(() => ds.choices(products, 'category'))
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
+ /**
7
35
  * The rows a linked field can point at, for a picker: each record of the query as its id and the text
8
36
  * of one field, searched on that field when a term is given, a page at a time.
9
37
  *
@@ -36,4 +64,4 @@ function labelOf(value) {
36
64
  }
37
65
 
38
66
  //#endregion
39
- export { listOptions };
67
+ export { listChoices, listOptions };
@@ -1,5 +1,5 @@
1
1
  import { createRecord, deleteRecord, getRecord, listRecords, updateRecord } from "./record.js";
2
- import { listOptions } from "../core/options.js";
2
+ import { listChoices, listOptions } from "../core/options.js";
3
3
  import { chart, metric } from "./chart.js";
4
4
  import "@tanstack/react-start/server-only";
5
5
 
@@ -13,6 +13,7 @@ declare const ds: {
13
13
  metric: typeof metric;
14
14
  chart: typeof chart;
15
15
  options: typeof listOptions;
16
+ choices: typeof listChoices;
16
17
  };
17
18
  //#endregion
18
19
  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 { listOptions } from "../core/options.js";
3
+ import { listChoices, listOptions } from "../core/options.js";
4
4
  import "@tanstack/react-start/server-only";
5
5
 
6
6
  //#region src/datasource/datasource.ts
@@ -12,7 +12,8 @@ const ds = {
12
12
  delete: deleteRecord,
13
13
  metric,
14
14
  chart,
15
- options: listOptions
15
+ options: listOptions,
16
+ choices: listChoices
16
17
  };
17
18
 
18
19
  //#endregion
@@ -19,17 +19,21 @@ type NumberChain = EmptyChain & {
19
19
  lte(value: number): ConstructableFilter;
20
20
  between(start: number, end: number): ConstructableFilter;
21
21
  };
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;
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;
27
27
  };
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;
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;
33
37
  };
34
38
  /** What DSv2 filters generically: matched whole or scanned, never against a set. */
35
39
  type GenericChain = EmptyChain & {
@@ -73,32 +77,20 @@ type AnyChain = EmptyChain & {
73
77
  between(start: number | string, end: number | string): ConstructableFilter;
74
78
  isNotBetween(start: number | string, end: number | string): ConstructableFilter;
75
79
  };
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>;
85
80
  type TextFieldType = 'SINGLE_LINE_TEXT' | 'LONG_TEXT' | 'URL' | 'EMAIL' | 'PHONE' | 'ADDRESS' | 'RECORD_ID' | 'LOCATION' | 'TIME' | 'BUTTON';
86
81
  type NumberFieldType = 'NUMBER' | 'PERCENT' | 'CURRENCY' | 'PROGRESS' | 'RATING' | 'DURATION' | 'COUNT' | 'AUTONUMBER';
87
82
  type DateFieldType = 'DATE' | 'DATETIME' | 'CREATED_AT' | 'UPDATED_AT';
88
83
  type ChoiceFieldType = 'SELECT' | 'LINKED_RECORD' | 'CREATED_BY' | 'UPDATED_BY';
89
84
  /** The types with no single value to compare: only whether the field holds one. */
90
85
  type EmptyFieldType = 'ATTACHMENT' | 'DATE_RANGE';
91
- type ElementOf<TValue> = TValue extends Array<infer TMember> ? TMember : TValue;
92
86
  /**
93
87
  * The operators one alias offers, chosen from the type codegen declared for the field rather
94
88
  * than from what it reads as: a `DATETIME` and a text field both read as `string`, and only
95
89
  * the declared type tells them apart.
96
90
  *
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.
91
+ * A field with no declared type lands on the permissive chain rather than on none.
100
92
  */
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;
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;
102
94
  type Combinators = {
103
95
  $and(...members: Array<AnyConstructable>): ConstructableFilterGroup;
104
96
  $or(...members: Array<AnyConstructable>): ConstructableFilterGroup;
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.75";
2
+ var version = "0.0.77";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/dist/server.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { upload } from "./core/file-upload.js";
2
2
  import { comments } from "./comments/comments.js";
3
3
  import { auth } from "./auth/server.js";
4
- import { RecordOption, RecordOptions } from "./core/options.js";
4
+ import { FieldChoice, RecordOption, RecordOptions } from "./core/options.js";
5
5
  import { ds } from "./datasource/datasource.js";
6
6
  import { workflow } from "./core/workflow.js";
7
7
  import { integration } from "./core/integration.js";
8
8
  import "@tanstack/react-start/server-only";
9
- export { type RecordOption, type RecordOptions, auth, comments, ds, integration, upload, workflow };
9
+ export { type FieldChoice, type RecordOption, type RecordOptions, auth, comments, ds, integration, upload, workflow };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.75",
3
+ "version": "0.0.77",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
@@ -1,18 +1,14 @@
1
1
  ---
2
2
  name: softr-sdk
3
3
  description: >-
4
- Entry point for @softr/sdk, the layer a Softr app uses to reach Softr: the
5
- four entrypoints (@softr/sdk, /server, /client, /vite) and the client-server
6
- boundary between them, softrAuthMiddleware, getSoftrContext,
7
- initialSoftrContext, SoftrContext, and which sub-skill to load for route
8
- visibility, the current user, reading records, charts and metrics, writing
9
- records, auth, or showing dates and numbers. Load this when unsure which
10
- sub-skill applies, or when touching the wiring; each sub-skill stands alone
11
- for its own task.
4
+ Entry point for @softr/sdk: its four entrypoints (@softr/sdk, /server,
5
+ /client, /vite) and their client-server boundary, softrAuthMiddleware and the
6
+ Softr context, and which sub-skill covers each task. Load it when unsure which
7
+ sub-skill applies, or when touching the wiring.
12
8
  metadata:
13
9
  type: core
14
10
  library: softr-sdk
15
- library_version: 0.0.75
11
+ library_version: 0.0.77
16
12
  ---
17
13
 
18
14
  # Softr SDK core
@@ -1,24 +1,15 @@
1
1
  ---
2
2
  name: auth
3
3
  description: >-
4
- Sign a Softr visitor in, out or up from a server function: auth.signIn,
5
- signInWithCode, signInWithGoogle, signInWithMagicLink, signUp, confirmSignUp
6
- and signOut from @softr/sdk/server, the authenticator-app calls that finish a
7
- sign-in (verifyAuthenticator, startAuthenticatorSetup,
8
- confirmAuthenticatorSetup, skipAuthenticatorSetup) and the ones that change an
9
- authenticator from a settings page (sendAuthenticatorResetCode,
10
- resetAuthenticator, removeAuthenticator), updateProfile for the visitor's own
11
- name and photo, the emailed password links (requestPasswordReset,
12
- checkPasswordLink, setPasswordWithLink), and getAuthSettings,
13
- AuthSettingsProvider and useAuthSettings for which sign-in and sign-up methods
14
- the app has enabled. Covers the Result shape they answer with and its error
15
- codes (invalidCredentials, emailTaken, notAuthenticated, notAnImage and the
16
- rest), the two-step sign-up, why a successful sign-in does not always mean the
17
- visitor is signed in, and toReturnPath for where to land after signing in.
4
+ Sign a visitor in, out or up from a server function with auth from
5
+ @softr/sdk/server: password, code, Google and magic link, sign-up and its
6
+ confirmation, the authenticator-app steps, password reset links,
7
+ updateProfile, and getAuthSettings and useAuthSettings for the methods the app
8
+ enables. Covers the Result shape, its error codes, and toReturnPath.
18
9
  metadata:
19
10
  type: sub-skill
20
11
  library: softr-sdk
21
- library_version: 0.0.75
12
+ library_version: 0.0.77
22
13
  ---
23
14
 
24
15
  **Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
@@ -1,18 +1,15 @@
1
1
  ---
2
2
  name: charts-and-metrics
3
3
  description: >-
4
- Aggregate Softr datasource records inside a server function: ds.chart with
5
- groupBy, metrics, where and orderBy for chart rows, ds.metric for a single
6
- headline number, date bucket intervals, which aggregations each kind of field
7
- offers, the two-group-column ceiling, and distinct values for filter controls.
8
- Use when drawing a chart, a stat tile or any counted, summed or averaged
9
- number, when filling a dropdown from a real column, or when a chart comes back
10
- scrambled or with the wrong rows. Aggregation only: fetching individual
11
- records is record-queries, and changing them is record-mutations.
4
+ Aggregate records in a server function: ds.chart with groupBy, metrics, where,
5
+ orderBy and date buckets for chart rows, ds.metric for one number, and
6
+ distinct values for filter controls. Use for a chart, a stat tile or any
7
+ counted, summed or averaged number, or when a chart comes back wrong. Single
8
+ records are record-queries.
12
9
  metadata:
13
10
  type: sub-skill
14
11
  library: softr-sdk
15
- library_version: 0.0.75
12
+ library_version: 0.0.77
16
13
  ---
17
14
 
18
15
  **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.
@@ -1,17 +1,14 @@
1
1
  ---
2
2
  name: comments
3
3
  description: >-
4
- Comment threads on records: defineComments from @softr/sdk declares which
5
- datasource has comments; comments.list, create, update, delete, react,
6
- subscription, setSubscription and searchMentions from @softr/sdk/server read
7
- and write one record's thread inside a server function; useCommentComposer
8
- and useFileUpload from @softr/sdk/client keep a textarea's text, its
9
- @mentions and its attachments in step. Bodies come back as sanitised
10
- bodyHtml. A null thread means render nothing.
4
+ Comment threads on records: defineComments, the comments calls from
5
+ @softr/sdk/server to list, write, react to and mention in one record's thread,
6
+ and useCommentComposer and useFileUpload from @softr/sdk/client for the
7
+ composer. Bodies come back as sanitised bodyHtml.
11
8
  metadata:
12
9
  type: sub-skill
13
10
  library: softr-sdk
14
- library_version: 0.0.75
11
+ library_version: 0.0.77
15
12
  ---
16
13
 
17
14
  **Boundary.** `comments` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler. Importing it into a component fails the build. `useCommentComposer` lives on `@softr/sdk/client` and runs in components. `defineComments` comes 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.75
12
+ library_version: 0.0.77
13
13
  ---
14
14
 
15
15
  # The current user
@@ -1,16 +1,13 @@
1
1
  ---
2
2
  name: file-uploads
3
3
  description: >-
4
- Upload files from a form or edit view: useFileUpload(action) from @softr/sdk
5
- does everything — XHR upload with real progress, per-upload cancel, and
6
- isUploading to block form submission. No route registration needed; the SDK
7
- ships its own server function. For server-side uploads or plain forms, use
8
- upload(actionId, file) from @softr/sdk/server. For edit forms pass
9
- initialAttachments to pre-load existing attachments.
4
+ Upload files from a form or edit view: useFileUpload from @softr/sdk, with
5
+ progress, cancel and isUploading to hold the submit, and initialAttachments
6
+ for an edit form; upload from @softr/sdk/server for a server-side upload.
10
7
  metadata:
11
8
  type: sub-skill
12
9
  library: softr-sdk
13
- library_version: 0.0.75
10
+ library_version: 0.0.77
14
11
  ---
15
12
 
16
13
  **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.
@@ -2,15 +2,13 @@
2
2
  name: formatting
3
3
  description: >-
4
4
  Show dates, times and numbers in the app's format without hydration
5
- mismatches: FormattedDate in JSX, useFormatting with formatDate,
6
- formatDateTime and formatNumber in code, FormattingScope and the timeZone
7
- option for a zone or locale the user names, parts for a partial date, and why
8
- toLocaleDateString, toLocaleString and Intl formatters with the runtime's
9
- defaults must never run in render.
5
+ mismatches: FormattedDate, useFormatting with formatDate, formatDateTime and
6
+ formatNumber, FormattingScope and timeZone, and why toLocaleDateString and
7
+ Intl defaults must never run in render.
10
8
  metadata:
11
9
  type: sub-skill
12
10
  library: softr-sdk
13
- library_version: 0.0.75
11
+ library_version: 0.0.77
14
12
  ---
15
13
 
16
14
  # Dates, times and numbers
@@ -1,16 +1,13 @@
1
1
  ---
2
2
  name: integrations
3
3
  description: >-
4
- Call a third-party service connected to the app as an integration:
5
- defineIntegration from @softr/sdk declares the action with its integrationId,
6
- the one HTTP method and endpoint url it allows, and permissions;
7
- integration.createFetcher from @softr/sdk/server binds it and returns a
8
- fetcher function you call with optional params, query, headers, and body.
9
- Check res.ok before res.json().
4
+ Call a third-party service connected as an integration: defineIntegration from
5
+ @softr/sdk declares the one method and endpoint it allows, and
6
+ integration.createFetcher from @softr/sdk/server calls it.
10
7
  metadata:
11
8
  type: sub-skill
12
9
  library: softr-sdk
13
- library_version: 0.0.75
10
+ library_version: 0.0.77
14
11
  ---
15
12
 
16
13
  **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.75
12
+ library_version: 0.0.77
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.75
12
+ library_version: 0.0.77
13
13
  ---
14
14
 
15
15
  # Installing the app
@@ -1,17 +1,14 @@
1
1
  ---
2
2
  name: record-mutations
3
3
  description: >-
4
- Create, update and delete records in a Softr datasource with defineMutation,
5
- ds.create, ds.update and ds.delete: one declaration per operation, the data
6
- payload keyed by your own field names, what each field type accepts on a
7
- write, and the record each call returns. Use when submitting a form, saving an
8
- edit or deleting a record, or when a write throws, writes the wrong field or
9
- silently changes nothing. Writes only: fetching records is record-queries, and
10
- aggregating them is charts-and-metrics.
4
+ Create, update and delete records with defineMutation, ds.create, ds.update
5
+ and ds.delete: the payload keyed by your own field names and what each field
6
+ type accepts. Use when submitting a form, saving an edit or deleting a record,
7
+ or when a write fails or changes nothing. Reads are record-queries.
11
8
  metadata:
12
9
  type: sub-skill
13
10
  library: softr-sdk
14
- library_version: 0.0.75
11
+ library_version: 0.0.77
15
12
  ---
16
13
 
17
14
  **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 +152,7 @@ A form select for a `LINKED_RECORD` field needs its options from the linked tabl
155
152
 
156
153
  Each key of `data` accepts exactly what its field takes (see [record-queries](../record-queries/SKILL.md#typing)):
157
154
 
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.
155
+ - 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)).
159
156
  - CHECKBOX takes a boolean. Dates take ISO strings, never `Date` instances.
160
157
  - ATTACHMENT takes `{ url }`, optionally with `filename`, `id` and `type`.
161
158
  - A multi-entry field takes an array of the value.
@@ -1,19 +1,15 @@
1
1
  ---
2
2
  name: record-queries
3
3
  description: >-
4
- Fetch records from a Softr datasource with defineQuery, ds.list and ds.get:
5
- the where and orderBy callbacks, field names of your own choosing, the
6
- 20-record default and 100-record cap, nextOffset pagination, and scoping a
7
- read to the signed-in visitor. Use when putting datasource records on a page,
8
- in a table, a list or a detail view, when filtering or sorting them, when
9
- filling a filter or form dropdown with all options from a linked table, or
10
- when a read comes back empty, unsorted or missing a field. Reads only:
11
- creating, updating and deleting records is record-mutations, and aggregating
12
- them into totals or chart rows is charts-and-metrics.
4
+ Fetch records with defineQuery, ds.list and ds.get: where and orderBy, field
5
+ names of your own choosing, pagination and its limits, and scoping a read to
6
+ the signed-in visitor. Use to put records on a page or fill a dropdown, or
7
+ when a read comes back empty, unsorted or missing a field. Writes are
8
+ record-mutations; totals and chart rows are charts-and-metrics.
13
9
  metadata:
14
10
  type: sub-skill
15
11
  library: softr-sdk
16
- library_version: 0.0.75
12
+ library_version: 0.0.77
17
13
  ---
18
14
 
19
15
  **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,6 +196,13 @@ const loadProductStatuses = createServerFn()
200
196
 
201
197
  Skip this request when the options are predefined and fixed — hardcode them instead.
202
198
 
199
+ 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:
200
+
201
+ ```ts
202
+ // `products` declares `category`, a SELECT field
203
+ const loadCategories = createServerFn().handler(() => ds.choices(products, 'category'))
204
+ ```
205
+
203
206
  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.
204
207
 
205
208
  ## 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 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.
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.
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.75
12
+ library_version: 0.0.77
13
13
  ---
14
14
 
15
15
  **Boundary.** `Audience` and `defineVisibility` come from `@softr/sdk` and work anywhere.
@@ -1,19 +1,14 @@
1
1
  ---
2
2
  name: routes-and-visibility
3
3
  description: >-
4
- Add a route file to a Softr app and declare who may see it: defineVisibility
5
- with a userGroupTypes set naming LOGGED_IN_USERS and/or NON_LOGGED_IN_USERS,
6
- customGroups, and the ways to apply a rule: guardRoute and assertRouteAccess
7
- in a route, isAuthorized and checkAccess anywhere. Covers per-reason
8
- overrides, the redirect and throw strategies, rendering an access error with
9
- isAccessError, isUnauthenticatedError, isForbiddenError and
10
- isAlreadyAuthenticatedError, guarding a layout, and the lint rule that
11
- requires a declaration alongside the rules that reject a duplicate, computed
12
- or root-level one.
4
+ Add a route file and declare who may see it: defineVisibility with
5
+ LOGGED_IN_USERS, NON_LOGGED_IN_USERS and customGroups, guardRoute and
6
+ assertRouteAccess in a route, isAuthorized and checkAccess anywhere, rendering
7
+ an access error, and the lint rule that requires the declaration.
13
8
  metadata:
14
9
  type: sub-skill
15
10
  library: softr-sdk
16
- library_version: 0.0.75
11
+ library_version: 0.0.77
17
12
  ---
18
13
 
19
14
  **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.75
12
+ library_version: 0.0.77
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.