@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.
- package/dist/auth/current-user-row.d.ts +20 -0
- package/dist/auth/current-user-row.js +22 -0
- package/dist/auth/methods/profile.d.ts +26 -0
- package/dist/auth/methods/profile.js +52 -0
- package/dist/auth/methods/utils.d.ts +1 -1
- package/dist/auth/platform.d.ts +1 -0
- package/dist/auth/platform.js +20 -33
- package/dist/auth/server.d.ts +4 -0
- package/dist/auth/server.js +5 -1
- package/dist/auth/session.js +12 -2
- package/dist/auth/user.d.ts +5 -8
- package/dist/auth/user.js +2 -1
- package/dist/cli.js +18 -26
- package/dist/codegen/datasource-docs.js +1 -93
- package/dist/codegen/datasource-types.js +1 -16
- package/dist/codegen/fetch-metadata.js +17 -44
- package/dist/codegen/softr-env.js +1 -2
- package/dist/codegen/softr-fields.js +41 -0
- package/dist/cookies.js +2 -10
- package/dist/core/options.d.ts +36 -0
- package/dist/core/options.js +39 -0
- package/dist/core/use-overrides.d.ts +17 -0
- package/dist/core/use-overrides.js +30 -0
- package/dist/datasource/datasource.d.ts +2 -0
- package/dist/datasource/datasource.js +3 -1
- package/dist/datasource/fields.js +3 -2
- package/dist/datasource/record.d.ts +2 -1
- package/dist/default-error-overlay.d.ts +2 -2
- package/dist/fields/format.d.ts +45 -0
- package/dist/fields/format.js +140 -0
- package/dist/formatting.d.ts +4 -4
- package/dist/formatting.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/package.js +1 -1
- package/dist/pwa.d.ts +2 -2
- package/dist/pwa.js +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/softr-config.schema.js +24 -45
- package/dist/visibility/route.js +1 -1
- package/dist/vite.js +19 -1
- package/package.json +10 -33
- package/schema/softr.schema.json +0 -13
- package/skills/softr-sdk/SKILL.md +2 -2
- package/skills/softr-sdk/auth/SKILL.md +29 -10
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +5 -24
- package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
- package/skills/softr-sdk/formatting/SKILL.md +19 -1
- package/skills/softr-sdk/integrations/SKILL.md +1 -1
- package/skills/softr-sdk/mcp/SKILL.md +1 -1
- package/skills/softr-sdk/pwa/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +13 -1
- package/skills/softr-sdk/record-queries/SKILL.md +21 -1
- package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
package/dist/package.js
CHANGED
package/dist/pwa.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
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():
|
|
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
|
|
17
|
-
const SoftrConfigSchema = z.object({
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 {
|
|
40
|
+
export { SoftrConfigSchema };
|
package/dist/visibility/route.js
CHANGED
package/dist/vite.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
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.
|
|
3
|
+
"version": "0.0.69-alpha.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
|
-
".": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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",
|
package/schema/softr.schema.json
CHANGED
|
@@ -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.
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
and
|
|
14
|
-
with and its
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
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.
|
|
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
|
|
8
|
-
group ids to the app's own
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
metadata:
|
|
12
12
|
type: sub-skill
|
|
13
13
|
library: softr-sdk
|
|
14
|
-
library_version: 0.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.
|
|
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.
|
|
@@ -13,7 +13,7 @@ description: >-
|
|
|
13
13
|
metadata:
|
|
14
14
|
type: sub-skill
|
|
15
15
|
library: softr-sdk
|
|
16
|
-
library_version: 0.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.
|
|
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.
|