@softr/sdk 0.0.68-props.0 → 0.0.69-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/auth/current-user-row.d.ts +20 -0
  2. package/dist/auth/current-user-row.js +22 -0
  3. package/dist/auth/methods/profile.d.ts +26 -0
  4. package/dist/auth/methods/profile.js +52 -0
  5. package/dist/auth/methods/utils.d.ts +1 -1
  6. package/dist/auth/platform.d.ts +1 -0
  7. package/dist/auth/platform.js +20 -33
  8. package/dist/auth/server.d.ts +4 -0
  9. package/dist/auth/server.js +5 -1
  10. package/dist/auth/session.js +12 -2
  11. package/dist/auth/user.d.ts +5 -8
  12. package/dist/auth/user.js +2 -1
  13. package/dist/cli.js +18 -26
  14. package/dist/codegen/datasource-docs.js +1 -93
  15. package/dist/codegen/datasource-types.js +1 -16
  16. package/dist/codegen/fetch-metadata.js +17 -44
  17. package/dist/codegen/softr-env.js +1 -2
  18. package/dist/codegen/softr-fields.js +41 -0
  19. package/dist/cookies.js +2 -10
  20. package/dist/core/options.d.ts +36 -0
  21. package/dist/core/options.js +39 -0
  22. package/dist/core/use-overrides.d.ts +17 -0
  23. package/dist/core/use-overrides.js +30 -0
  24. package/dist/datasource/datasource.d.ts +2 -0
  25. package/dist/datasource/datasource.js +3 -1
  26. package/dist/datasource/fields.js +3 -2
  27. package/dist/datasource/record.d.ts +2 -1
  28. package/dist/default-error-overlay.d.ts +2 -2
  29. package/dist/fields/format.d.ts +45 -0
  30. package/dist/fields/format.js +140 -0
  31. package/dist/formatting.d.ts +4 -4
  32. package/dist/formatting.js +1 -1
  33. package/dist/index.d.ts +4 -2
  34. package/dist/index.js +3 -1
  35. package/dist/mcp.d.ts +2 -2
  36. package/dist/mcp.js +2 -2
  37. package/dist/package.js +1 -1
  38. package/dist/pwa.d.ts +2 -2
  39. package/dist/pwa.js +1 -1
  40. package/dist/server.d.ts +2 -1
  41. package/dist/softr-config.schema.js +24 -45
  42. package/dist/visibility/route.js +1 -1
  43. package/dist/vite.js +19 -1
  44. package/package.json +10 -33
  45. package/schema/softr.schema.json +0 -13
  46. package/skills/softr-sdk/SKILL.md +2 -2
  47. package/skills/softr-sdk/auth/SKILL.md +29 -10
  48. package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
  49. package/skills/softr-sdk/current-user/SKILL.md +5 -24
  50. package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
  51. package/skills/softr-sdk/formatting/SKILL.md +19 -1
  52. package/skills/softr-sdk/integrations/SKILL.md +1 -1
  53. package/skills/softr-sdk/mcp/SKILL.md +1 -1
  54. package/skills/softr-sdk/pwa/SKILL.md +1 -1
  55. package/skills/softr-sdk/record-mutations/SKILL.md +13 -1
  56. package/skills/softr-sdk/record-queries/SKILL.md +21 -1
  57. package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
  58. package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
  59. package/skills/softr-sdk/workflows/SKILL.md +1 -1
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.68-props.0";
2
+ var version = "0.0.69-alpha.0";
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 react5 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(): react5.FunctionComponentElement<react5.FragmentProps>;
27
27
  type PwaPlatform = "ios" | "android" | "desktop";
28
28
  type PwaInstall = {
29
29
  available: false;
package/dist/pwa.js CHANGED
@@ -2,9 +2,9 @@ import { logger } from "./logger.js";
2
2
  import { fetch } from "./fetch.js";
3
3
  import { sessionConfig } from "./auth/config.js";
4
4
  import { platformApiUrl } from "./auth/platform.js";
5
- import { z } from "zod";
6
5
  import { Fragment, createElement, useEffect, useState } from "react";
7
6
  import { createMiddleware } from "@tanstack/react-start";
7
+ import { z } from "zod";
8
8
 
9
9
  //#region src/pwa.ts
10
10
  const MANIFEST_PATH = "/manifest.webmanifest";
package/dist/server.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { upload } from "./core/file-upload.js";
2
2
  import { auth } from "./auth/server.js";
3
+ import { RecordOption, RecordOptions } from "./core/options.js";
3
4
  import { ds } from "./datasource/datasource.js";
4
5
  import { workflow } from "./core/workflow.js";
5
6
  import { integration } from "./core/integration.js";
6
7
  import "@tanstack/react-start/server-only";
7
- export { auth, ds, integration, upload, workflow };
8
+ export { type RecordOption, type RecordOptions, auth, ds, integration, upload, workflow };
@@ -1,10 +1,6 @@
1
- import { assert, panic } from "./assertions.js";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import path from "node:path";
4
1
  import { z } from "zod";
5
2
 
6
3
  //#region src/softr-config.schema.ts
7
- const SOFTR_CONFIG_FILE = "softr.json";
8
4
  const AccessBehaviour = z.union([
9
5
  z.string(),
10
6
  z.object({
@@ -13,49 +9,32 @@ const AccessBehaviour = z.union([
13
9
  }),
14
10
  z.object({ strategy: z.literal("throw") })
15
11
  ], { error: "must be a path like \"/login\", { \"strategy\": \"redirect\", \"to\": \"/login\" } or { \"strategy\": \"throw\" }" });
16
- const AppPath = z.string().refine((path$1) => path$1.startsWith("/") && !path$1.includes("?") && !path$1.includes("#") && new URL(path$1, "https://app.invalid").origin === "https://app.invalid", { error: "must be a path on this app like \"/login\", with no \"?\" or \"#\"" });
17
- const SoftrConfigSchema = z.object({
18
- auth: z.strictObject({
19
- routes: z.strictObject({
20
- login: z.strictObject({
21
- path: AppPath,
22
- redirectParam: z.string()
23
- }),
24
- resetPassword: z.strictObject({
25
- path: AppPath,
26
- tokenParam: z.string()
27
- }),
28
- acceptInvitation: z.strictObject({
29
- path: AppPath,
30
- tokenParam: z.string()
31
- }),
32
- magicLink: z.strictObject({
33
- path: AppPath,
34
- tokenParam: z.string()
35
- })
12
+ const AppPath = z.string().refine((path) => path.startsWith("/") && !path.includes("?") && !path.includes("#") && new URL(path, "https://app.invalid").origin === "https://app.invalid", { error: "must be a path on this app like \"/login\", with no \"?\" or \"#\"" });
13
+ const SoftrConfigSchema = z.object({ auth: z.strictObject({
14
+ routes: z.strictObject({
15
+ login: z.strictObject({
16
+ path: AppPath,
17
+ redirectParam: z.string()
36
18
  }),
37
- guard: z.strictObject({
38
- unauthenticated: AccessBehaviour,
39
- forbidden: AccessBehaviour,
40
- alreadyAuthenticated: AccessBehaviour
19
+ resetPassword: z.strictObject({
20
+ path: AppPath,
21
+ tokenParam: z.string()
22
+ }),
23
+ acceptInvitation: z.strictObject({
24
+ path: AppPath,
25
+ tokenParam: z.string()
26
+ }),
27
+ magicLink: z.strictObject({
28
+ path: AppPath,
29
+ tokenParam: z.string()
41
30
  })
42
31
  }),
43
- user: z.strictObject({ properties: z.record(z.string(), z.string()) }).optional()
44
- });
45
- function loadSoftrConfig(root) {
46
- const file = path.join(root, SOFTR_CONFIG_FILE);
47
- assert(existsSync(file), `${SOFTR_CONFIG_FILE} is missing from ${root}`);
48
- const source = readFileSync(file, "utf8");
49
- let parsed;
50
- try {
51
- parsed = JSON.parse(source);
52
- } catch (error) {
53
- panic(`${SOFTR_CONFIG_FILE} is not valid JSON: ${error instanceof Error ? error.message : error}`);
54
- }
55
- const result = SoftrConfigSchema.safeParse(parsed);
56
- if (!result.success) panic(`${SOFTR_CONFIG_FILE} does not match the schema:\n\n${z.prettifyError(result.error)}`);
57
- return result.data;
58
- }
32
+ guard: z.strictObject({
33
+ unauthenticated: AccessBehaviour,
34
+ forbidden: AccessBehaviour,
35
+ alreadyAuthenticated: AccessBehaviour
36
+ })
37
+ }) });
59
38
 
60
39
  //#endregion
61
- export { SOFTR_CONFIG_FILE, loadSoftrConfig };
40
+ export { SoftrConfigSchema };
@@ -1,5 +1,5 @@
1
- import { softrConfig } from "../softr-config.js";
2
1
  import { ACCESS_REASONS, checkAccess } from "./visibility.js";
2
+ import { softrConfig } from "../softr-config.js";
3
3
  import { redirect } from "@tanstack/react-router";
4
4
 
5
5
  //#region src/visibility/route.ts
package/dist/vite.js CHANGED
@@ -1,8 +1,12 @@
1
- import { SOFTR_CONFIG_FILE, loadSoftrConfig } from "./softr-config.schema.js";
1
+ import { assert, panic } from "./assertions.js";
2
+ import { SoftrConfigSchema } from "./softr-config.schema.js";
3
+ import { existsSync, readFileSync } from "node:fs";
2
4
  import path from "node:path";
5
+ import { z } from "zod";
3
6
  import { pathToFileURL } from "node:url";
4
7
 
5
8
  //#region src/vite.ts
9
+ const SOFTR_CONFIG_FILE = "softr.json";
6
10
  function softr() {
7
11
  return [softrConfig(), editEngine()];
8
12
  }
@@ -23,6 +27,20 @@ function softrConfig() {
23
27
  }
24
28
  };
25
29
  }
30
+ function loadSoftrConfig(root) {
31
+ const file = path.join(root, SOFTR_CONFIG_FILE);
32
+ assert(existsSync(file), `${SOFTR_CONFIG_FILE} is missing from ${root}`);
33
+ const source = readFileSync(file, "utf8");
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(source);
37
+ } catch (error) {
38
+ panic(`${SOFTR_CONFIG_FILE} is not valid JSON: ${error instanceof Error ? error.message : error}`);
39
+ }
40
+ const result = SoftrConfigSchema.safeParse(parsed);
41
+ if (!result.success) panic(`${SOFTR_CONFIG_FILE} does not match the schema:\n\n${z.prettifyError(result.error)}`);
42
+ return result.data;
43
+ }
26
44
  /**
27
45
  * The edit engine is provided by the pod runtime rather than an app dependency: it is never in the
28
46
  * app's `package.json` and never in its `node_modules`. The pod points `SOFTR_EDIT_ENGINE_DIR` at
package/package.json CHANGED
@@ -1,37 +1,16 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.68-props.0",
3
+ "version": "0.0.69-alpha.0",
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",
@@ -54,19 +54,6 @@
54
54
  },
55
55
  "required": ["routes", "guard"],
56
56
  "additionalProperties": false
57
- },
58
- "user": {
59
- "type": "object",
60
- "description": "The signed-in user.",
61
- "properties": {
62
- "properties": {
63
- "type": "object",
64
- "description": "Fields of the users table the app reads as user.properties, keyed by the name the app uses. Each value is a field id. No other field leaves the server.",
65
- "additionalProperties": { "type": "string" }
66
- }
67
- },
68
- "required": ["properties"],
69
- "additionalProperties": false
70
57
  }
71
58
  },
72
59
  "required": ["auth"],
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: core
14
14
  library: softr-sdk
15
- library_version: 0.0.68-props.0
15
+ library_version: 0.0.69-alpha.0
16
16
  ---
17
17
 
18
18
  # Softr SDK core
@@ -30,7 +30,7 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
30
30
 
31
31
  | Import | Runs | Holds |
32
32
  | --- | --- | --- |
33
- | `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `FormattedDate`, `FormattingScope`, `useFormatting`, `formatDate`, `formatDateTime`, `formatNumber`, `SoftrPwaHead`, `usePwaInstall`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
33
+ | `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `FormattedDate`, `FormattingScope`, `useFormatting`, `formatDate`, `formatDateTime`, `formatNumber`, `formatValue`, `formatField`, `fieldShape`, `registerSoftrFields`, `useOverrides`, `SoftrPwaHead`, `usePwaInstall`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
34
34
  | `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration` |
35
35
  | `@softr/sdk/client` | Browser only | Nothing yet, reserved for browser-only utilities |
36
36
  | `@softr/sdk/vite` | Build | `softr()`, already in `vite.config.ts` |
@@ -7,19 +7,18 @@ description: >-
7
7
  sign-in (verifyAuthenticator, startAuthenticatorSetup,
8
8
  confirmAuthenticatorSetup, skipAuthenticatorSetup) and the ones that change an
9
9
  authenticator from a settings page (sendAuthenticatorResetCode,
10
- resetAuthenticator, removeAuthenticator), the emailed password links
11
- (requestPasswordReset, checkPasswordLink, setPasswordWithLink), and
12
- getAuthSettings, AuthSettingsProvider and useAuthSettings for which sign-in
13
- and sign-up methods the app has enabled. Covers the Result shape they answer
14
- with and its codes invalidCredentials, tooManyAttempts, emailTaken,
15
- invalidDetails, invalidCode, notAuthenticated, emailDomainNotAllowed and
16
- linkExpired, the two-step sign-up, why a successful sign-in does not always
17
- mean the visitor is signed in, and toReturnPath for where to land after
18
- signing in.
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.
19
18
  metadata:
20
19
  type: sub-skill
21
20
  library: softr-sdk
22
- library_version: 0.0.68-props.0
21
+ library_version: 0.0.69-alpha.0
23
22
  ---
24
23
 
25
24
  **Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
@@ -82,6 +81,12 @@ Changing an authenticator from a settings page:
82
81
  | `auth.resetAuthenticator({ code })` | Unpairs the app, so a new one can be enrolled |
83
82
  | `auth.removeAuthenticator({ code })` | Turns the authenticator off |
84
83
 
84
+ Changing the visitor's own profile:
85
+
86
+ | Call | Does |
87
+ | --- | --- |
88
+ | `auth.updateProfile({ fullName?, avatar? })` | Changes their name, their photo, or both. `avatar: null` removes the photo |
89
+
85
90
  ## Setup
86
91
 
87
92
  `auth` is server-only, so every call sits inside a `createServerFn` handler:
@@ -451,6 +456,20 @@ All three need a signed-in visitor and answer `notAuthenticated` when there is n
451
456
 
452
457
  After a reset, enrol the new app with `startAuthenticatorSetup` and `confirmAuthenticatorSetup`, which is also where the new backup codes come from. After a removal, an app that requires an authenticator asks the visitor to set one up again on their next sign-in.
453
458
 
459
+ ### Changing the visitor's name and photo
460
+
461
+ ```ts
462
+ const result = await auth.updateProfile({ fullName: 'Ann Lee' })
463
+ 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
+ ```
466
+
467
+ 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`.
468
+
469
+ Only some apps can store a photo. Show the photo controls only when `user.avatarEditable` is true; elsewhere a photo answers `avatarNotSupported`.
470
+
471
+ The page keeps showing the old name and photo until its route context loads again, so call `router.invalidate()` after a success.
472
+
454
473
  ## References
455
474
 
456
475
  - [current-user/SKILL.md](../current-user/SKILL.md): reading the visitor these calls establish
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.68-props.0
15
+ library_version: 0.0.69-alpha.0
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.
@@ -4,13 +4,12 @@ description: >-
4
4
  Read the signed-in Softr visitor: useCurrentUser and the User type,
5
5
  context.user in a route's beforeLoad, and context.user inside a server
6
6
  function handler, where Start types it from the app's registered request
7
- middleware. Covers the null guest, the generated SoftrUserGroups that narrows
8
- group ids to the app's own, and user.properties, the fields of the visitor's
9
- row that softr.json declares.
7
+ middleware. Covers the null guest and the generated SoftrUserGroups that
8
+ narrows group ids to the app's own.
10
9
  metadata:
11
10
  type: sub-skill
12
11
  library: softr-sdk
13
- library_version: 0.0.68-props.0
12
+ library_version: 0.0.69-alpha.0
14
13
  ---
15
14
 
16
15
  # The current user
@@ -32,7 +31,8 @@ type User = {
32
31
  email: string
33
32
  fullName: string
34
33
  userGroupIds: Array<SoftrUserGroupId> // the app's own group ids
35
- properties: SoftrUserProperties // the aliases softr.json declares
34
+ avatarUrl: string | null // their photo; show their initials when null
35
+ avatarEditable: boolean // whether auth.updateProfile can change the photo
36
36
  }
37
37
  ```
38
38
 
@@ -92,25 +92,6 @@ For every group's name and id, read `docs/user-groups.md`. It lists every group
92
92
 
93
93
  `user.userGroupIds` and every `customGroups` the SDK accepts take only those ids, so an id the app does not have stops compiling. A new group appears in the list once it exists on the app.
94
94
 
95
- ### Fields of the visitor's row: `user.properties`
96
-
97
- To read more of the visitor's row in the users table, give each field an alias in `softr.json`, under `user.properties`. The value is the field's id:
98
-
99
- ```json
100
- {
101
- "user": {
102
- "properties": {
103
- "plan": "OeIz1",
104
- "seats": "1iHKE"
105
- }
106
- }
107
- }
108
- ```
109
-
110
- `user.properties` then holds exactly those aliases. To read them, see `docs/user-properties.md`.
111
-
112
- Attachment and long text fields, and linked record and lookup fields with multiple entries, cannot be properties. Read those with `defineQuery` on the users table. See [record-queries](../record-queries/SKILL.md).
113
-
114
95
  ## References
115
96
 
116
97
  - [routes-and-visibility/SKILL.md](../routes-and-visibility/SKILL.md): `isAuthorized` narrows `context.user` while applying the route's rule
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.68-props.0
13
+ library_version: 0.0.69-alpha.0
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.68-props.0
13
+ library_version: 0.0.69-alpha.0
14
14
  ---
15
15
 
16
16
  # Dates, times and numbers
@@ -116,6 +116,24 @@ The currency comes from the field or from the user, never from the locale. A Ger
116
116
 
117
117
  `formatDate` and `formatDateTime` show a timestamp in UTC while `formatting.timeZone` is `null`. In JSX, prefer `<FormattedDate />`, which waits instead.
118
118
 
119
+ ### A field's own text: `formatValue`
120
+
121
+ A value a query read already knows how it should look: money with its symbol and cents, a percent, a rating out of its scale, a duration in hours and minutes, a date in the app's format, a choice or a linked record by its label, a checkbox as Yes or No, several values joined by a comma, `''` for nothing. `formatValue` reads the field's type and options from the generated `src/softr-fields.ts` and does all of it, so a page never branches on a type itself.
122
+
123
+ ```tsx
124
+ import { formatValue, useFormatting } from '@softr/sdk'
125
+ import { dealsQuery } from '#/server/deals.ts'
126
+
127
+ function Amount({ deal }: { deal: Deal }) {
128
+ const formatting = useFormatting()
129
+ return <span>{formatValue(dealsQuery, 'amount', deal.fields.amount, formatting)}</span>
130
+ }
131
+ ```
132
+
133
+ The second argument is the alias the query holds the field under. `formatField(shape, value, formatting)` does the same for a shape in hand, and `fieldShape(query, alias)` reads one.
134
+
135
+ The SDK's codegen writes `src/softr-fields.ts` beside `softr-env.d.ts` after every schema change; it registers the shapes as a side effect of being imported, and the root route imports it once. `formatValue` throws when that import is missing.
136
+
119
137
  ### Outside render: a server function or `beforeLoad`
120
138
 
121
139
  Not every date ends up in a component. A CSV or spreadsheet export, a text the server sends, or a value a loader prepares are formatted before React runs. There, `context.formatting` holds the same `Formatting` that `useFormatting()` returns. See [current-user](../current-user/SKILL.md) for how `context` reaches a handler.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.68-props.0
13
+ library_version: 0.0.69-alpha.0
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.68-props.0
12
+ library_version: 0.0.69-alpha.0
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.68-props.0
12
+ library_version: 0.0.69-alpha.0
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.68-props.0
14
+ library_version: 0.0.69-alpha.0
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.
@@ -135,6 +135,18 @@ await ds.update(updateProject, {
135
135
 
136
136
  A read answers a SELECT as `{ id, label }` and a write takes `{ id }`, so pass the id through rather than the whole value.
137
137
 
138
+ ### Show the edit before the loader re-runs
139
+
140
+ A list whose rows a drawer or an inline control edits keeps the saved values over the loader's rows until a fresh read replaces them. `useOverrides` holds them by record id:
141
+
142
+ ```tsx
143
+ const overrides = useOverrides<Partial<Deal['fields']>>()
144
+ const rows = deals.map((deal) => ({ ...deal, fields: { ...deal.fields, ...overrides.of(deal.id) } }))
145
+ <DealsDrawer onSaved={overrides.onSaved} />
146
+ ```
147
+
148
+ `onSaved(recordId, patch)` is what a drawer calls before the write and again with the old value if the write fails; `clear(recordId)` drops a record's patch once the page has reloaded it.
149
+
138
150
  ### Form dropdowns for linked record fields
139
151
 
140
152
  A form select for a `LINKED_RECORD` field needs its options from the linked table, not from the record being edited. Query the linked table with its own `defineQuery` and `ds.list`; each result's `id` is what you pass to the write payload. See [record-queries](../record-queries/SKILL.md).
@@ -13,7 +13,7 @@ description: >-
13
13
  metadata:
14
14
  type: sub-skill
15
15
  library: softr-sdk
16
- library_version: 0.0.68-props.0
16
+ library_version: 0.0.69-alpha.0
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.
@@ -146,6 +146,18 @@ const listMyInvoices = createServerFn().handler(async ({ context }) => {
146
146
  })
147
147
  ```
148
148
 
149
+ When the owner is a link to the Users table, the row to match is the visitor's own row there, not `context.user.id`. `auth.currentUserRow(context.user, usersQuery, 'email')` finds it by email, or answers `null` when they are signed out or have no row:
150
+
151
+ ```ts
152
+ import { auth, ds } from '@softr/sdk/server'
153
+
154
+ const listMyDeals = createServerFn().handler(async ({ context }) => {
155
+ const me = await auth.currentUserRow(context.user, usersQuery, 'email')
156
+ if (me === null) return { records: [], nextOffset: null }
157
+ return await ds.list(deals).where((q) => q.owner.isOneOf([me.id]))
158
+ })
159
+ ```
160
+
149
161
  The SDK enforces `permissions`, so a visitor outside the declared audience never reaches the records. Which of those records belong to one visitor is a separate question, and the `where` above answers it: a group says nothing about who owns a row, so a per-visitor read filters on `context.user`. See [current-user](../current-user/SKILL.md) for how the visitor gets onto `context`.
150
162
 
151
163
  ### Populate a control from a linked table
@@ -178,6 +190,14 @@ Each returned record is one option: `record.id` is the value a `where` filter an
178
190
  ds.list(products).where((q) => q.status.is(selectedStatusId))
179
191
  ```
180
192
 
193
+ For a picker that searches and pages, `ds.options` answers the shape a linked-record picker's `load` takes, `{ options: [{ id, label }], nextOffset }`, searched on the label field when a term is given:
194
+
195
+ ```ts
196
+ const loadProductStatuses = createServerFn()
197
+ .validator(z.object({ term: z.string(), offset: z.number().nullable() }))
198
+ .handler(({ data }) => ds.options(productStatuses, { label: 'name', ...data }))
199
+ ```
200
+
181
201
  Skip this request when the options are predefined and fixed — hardcode them instead.
182
202
 
183
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.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.68-props.0
12
+ library_version: 0.0.69-alpha.0
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.68-props.0
16
+ library_version: 0.0.69-alpha.0
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.68-props.0
12
+ library_version: 0.0.69-alpha.0
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.