@softr/sdk 0.0.50 → 0.0.51
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/action.d.ts +9 -10
- package/dist/codegen/datasource-docs.js +2 -2
- package/dist/package.js +1 -1
- package/dist/visibility/audience.d.ts +1 -1
- package/dist/visibility/audience.js +1 -1
- package/dist/visibility/visibility.d.ts +15 -20
- package/dist/visibility/visibility.js +16 -20
- package/package.json +1 -1
- package/skills/softr-sdk/SKILL.md +1 -1
- package/skills/softr-sdk/auth/SKILL.md +1 -1
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +2 -2
- package/skills/softr-sdk/current-user/SKILL.md +2 -2
- package/skills/softr-sdk/file-uploads/SKILL.md +2 -2
- package/skills/softr-sdk/integrations/SKILL.md +4 -4
- package/skills/softr-sdk/mcp/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +6 -6
- package/skills/softr-sdk/record-queries/SKILL.md +7 -7
- package/skills/softr-sdk/region-visibility/SKILL.md +3 -3
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +30 -23
- package/skills/softr-sdk/workflows/SKILL.md +2 -2
package/dist/action.d.ts
CHANGED
|
@@ -3,12 +3,11 @@ import { SoftrUserGroupId } from "./auth/user.js";
|
|
|
3
3
|
//#region src/action.d.ts
|
|
4
4
|
type ActionOperation = "read" | "create" | "update" | "delete";
|
|
5
5
|
type ActionPermissions = {
|
|
6
|
-
|
|
6
|
+
userGroupTypes: readonly ["NON_LOGGED_IN_USERS"];
|
|
7
|
+
customGroups?: never;
|
|
7
8
|
} | {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
userGroup: "LOGGED_IN_USERS";
|
|
11
|
-
userGroupIds?: Array<SoftrUserGroupId>;
|
|
9
|
+
userGroupTypes: readonly ["LOGGED_IN_USERS"] | readonly ["NON_LOGGED_IN_USERS", "LOGGED_IN_USERS"] | readonly ["LOGGED_IN_USERS", "NON_LOGGED_IN_USERS"];
|
|
10
|
+
customGroups?: ReadonlyArray<SoftrUserGroupId>;
|
|
12
11
|
};
|
|
13
12
|
type Action<TResource> = {
|
|
14
13
|
id: string;
|
|
@@ -109,7 +108,7 @@ type DeleteDeclaration<TId extends string, TDatasource$1> = {
|
|
|
109
108
|
* id: "publishedPosts",
|
|
110
109
|
* datasourceId: "8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77",
|
|
111
110
|
* fields: { title: "V1StGXR8", author: "Z7pQm4Kd", publishedAt: "Xp2dLh6v" },
|
|
112
|
-
* permissions: {
|
|
111
|
+
* permissions: { userGroupTypes: ["NON_LOGGED_IN_USERS", "LOGGED_IN_USERS"] },
|
|
113
112
|
* })
|
|
114
113
|
*
|
|
115
114
|
* const { records } = await ds
|
|
@@ -135,7 +134,7 @@ declare function defineQuery<const TId extends string, const TDatasource$1 exten
|
|
|
135
134
|
* datasourceId: "8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77",
|
|
136
135
|
* operation: "update",
|
|
137
136
|
* fields: { status: "V1StGXR8", closedAt: "Z7pQm4Kd" },
|
|
138
|
-
* permissions: {
|
|
137
|
+
* permissions: { userGroupTypes: ["LOGGED_IN_USERS"], customGroups: ["grp_staff"] },
|
|
139
138
|
* })
|
|
140
139
|
*
|
|
141
140
|
* await ds.update(closeOrder, { id, data: { status: "closed" } })
|
|
@@ -145,7 +144,7 @@ declare function defineQuery<const TId extends string, const TDatasource$1 exten
|
|
|
145
144
|
* id: "removeOrder",
|
|
146
145
|
* datasourceId: "8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77",
|
|
147
146
|
* operation: "delete",
|
|
148
|
-
* permissions: {
|
|
147
|
+
* permissions: { userGroupTypes: ["LOGGED_IN_USERS"], customGroups: ["grp_staff"] },
|
|
149
148
|
* })
|
|
150
149
|
*
|
|
151
150
|
* await ds.delete(removeOrder, { id })
|
|
@@ -191,7 +190,7 @@ type WorkflowDefinition<TId extends string> = {
|
|
|
191
190
|
* id: "notifyTeam",
|
|
192
191
|
* workflowId: "wf_abc123",
|
|
193
192
|
* triggerId: "tr_xyz789",
|
|
194
|
-
* permissions: {
|
|
193
|
+
* permissions: { userGroupTypes: ["LOGGED_IN_USERS"] },
|
|
195
194
|
* body: { requestedBy: "{LOGGED_IN_USER:EMAIL}", at: "{CURRENT_DATE_TIME}" },
|
|
196
195
|
* });
|
|
197
196
|
*
|
|
@@ -228,7 +227,7 @@ type IntegrationDefinition<TId extends string> = {
|
|
|
228
227
|
* integrationId: "25d60213-ef4f-4fa0-b094-a46931b2d120",
|
|
229
228
|
* method: "GET",
|
|
230
229
|
* url: "https://www.googleapis.com/calendar/v3/calendars/primary/events",
|
|
231
|
-
* permissions: {
|
|
230
|
+
* permissions: { userGroupTypes: ["LOGGED_IN_USERS"] },
|
|
232
231
|
* })
|
|
233
232
|
*
|
|
234
233
|
* const listEvents = integration.createFetcher(listCalendarEvents)
|
|
@@ -194,7 +194,7 @@ function renderExamples(datasource, datasources) {
|
|
|
194
194
|
" fields: {",
|
|
195
195
|
...sampled.map((field) => ` ${keyOf(field)}: ${quoted(field.id)}, // ${fieldTypeLiteral(field).slice(1, -1)}`),
|
|
196
196
|
" },",
|
|
197
|
-
" permissions: {
|
|
197
|
+
" permissions: { userGroupTypes: ['NON_LOGGED_IN_USERS', 'LOGGED_IN_USERS'] },",
|
|
198
198
|
"})",
|
|
199
199
|
"",
|
|
200
200
|
`const list${name} = createServerFn().handler(async () => {`,
|
|
@@ -226,7 +226,7 @@ function renderExamples(datasource, datasources) {
|
|
|
226
226
|
" fields: {",
|
|
227
227
|
...writable.map((field) => ` ${keyOf(field)}: ${quoted(field.id)},`),
|
|
228
228
|
" },",
|
|
229
|
-
" permissions: {
|
|
229
|
+
" permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },",
|
|
230
230
|
"})",
|
|
231
231
|
"",
|
|
232
232
|
`const add${name} = createServerFn({ method: 'POST' }).handler(async () =>`,
|
package/dist/package.js
CHANGED
|
@@ -16,7 +16,7 @@ type Props = {
|
|
|
16
16
|
* Requires the root route to resolve {@link getSoftrContext}.
|
|
17
17
|
*
|
|
18
18
|
* @example
|
|
19
|
-
* const membersOnly = defineVisibility({
|
|
19
|
+
* const membersOnly = defineVisibility({ userGroupTypes: ["LOGGED_IN_USERS"] })
|
|
20
20
|
*
|
|
21
21
|
* <Audience visibility={membersOnly} fallback={<Upgrade />}>
|
|
22
22
|
* <DangerZone />
|
|
@@ -11,7 +11,7 @@ import { useCurrentUser } from "../auth/isomorphic.js";
|
|
|
11
11
|
* Requires the root route to resolve {@link getSoftrContext}.
|
|
12
12
|
*
|
|
13
13
|
* @example
|
|
14
|
-
* const membersOnly = defineVisibility({
|
|
14
|
+
* const membersOnly = defineVisibility({ userGroupTypes: ["LOGGED_IN_USERS"] })
|
|
15
15
|
*
|
|
16
16
|
* <Audience visibility={membersOnly} fallback={<Upgrade />}>
|
|
17
17
|
* <DangerZone />
|
|
@@ -2,22 +2,20 @@ import { SoftrUserGroupId, User } from "../auth/user.js";
|
|
|
2
2
|
import { SoftrContext } from "../auth/context.js";
|
|
3
3
|
|
|
4
4
|
//#region src/visibility/visibility.d.ts
|
|
5
|
-
|
|
5
|
+
type AudienceSet = readonly ["NON_LOGGED_IN_USERS"] | readonly ["LOGGED_IN_USERS"] | readonly ["NON_LOGGED_IN_USERS", "LOGGED_IN_USERS"] | readonly ["LOGGED_IN_USERS", "NON_LOGGED_IN_USERS"];
|
|
6
6
|
/**
|
|
7
|
-
* Who may see a route.
|
|
7
|
+
* Who may see a route or region. `customGroups` narrows only the
|
|
8
|
+
* `LOGGED_IN_USERS` half.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* @example
|
|
11
|
+
* { userGroupTypes: ["NON_LOGGED_IN_USERS", "LOGGED_IN_USERS"] }
|
|
12
|
+
* { userGroupTypes: ["NON_LOGGED_IN_USERS"] }
|
|
13
|
+
* { userGroupTypes: ["LOGGED_IN_USERS"], customGroups: ["…"] }
|
|
14
|
+
* { userGroupTypes: ["NON_LOGGED_IN_USERS", "LOGGED_IN_USERS"], customGroups: ["…"] }
|
|
13
15
|
*/
|
|
14
16
|
type VisibilityRule = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
userGroup: "NON_LOGGED_IN_USERS";
|
|
18
|
-
} | {
|
|
19
|
-
userGroup: "LOGGED_IN_USERS";
|
|
20
|
-
userGroupIds?: Array<SoftrUserGroupId>;
|
|
17
|
+
userGroupTypes: AudienceSet;
|
|
18
|
+
customGroups?: ReadonlyArray<SoftrUserGroupId>;
|
|
21
19
|
};
|
|
22
20
|
declare const visibilityBrand: unique symbol;
|
|
23
21
|
/** A rule {@link defineVisibility} has produced. */
|
|
@@ -43,15 +41,12 @@ type AccessCheck = {
|
|
|
43
41
|
type AccessContext = Pick<SoftrContext, "user">;
|
|
44
42
|
/** The decision and, when a visitor is denied, the reason. Throws nothing. */
|
|
45
43
|
declare function checkAccess(visibility: Visibility, context: AccessContext): AccessCheck;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
} ? AccessContext & {
|
|
44
|
+
/** Narrows `context.user` only for a rule naming exactly one audience; naming both narrows to neither. */
|
|
45
|
+
type Narrowed<R extends VisibilityRule> = R["userGroupTypes"] extends ReadonlyArray<infer T> ? [T] extends ["LOGGED_IN_USERS"] ? AccessContext & {
|
|
49
46
|
user: User;
|
|
50
|
-
} :
|
|
51
|
-
userGroup: "NON_LOGGED_IN_USERS";
|
|
52
|
-
} ? AccessContext & {
|
|
47
|
+
} : [T] extends ["NON_LOGGED_IN_USERS"] ? AccessContext & {
|
|
53
48
|
user: null;
|
|
54
|
-
} : AccessContext;
|
|
49
|
+
} : AccessContext : AccessContext;
|
|
55
50
|
/**
|
|
56
51
|
* Declares who may see a route or a region of the page.
|
|
57
52
|
*
|
|
@@ -59,7 +54,7 @@ type Narrowed<R extends VisibilityRule> = R extends {
|
|
|
59
54
|
* declarations out of the source to publish the app's route visibility.
|
|
60
55
|
*
|
|
61
56
|
* @example
|
|
62
|
-
* const visibility = defineVisibility({
|
|
57
|
+
* const visibility = defineVisibility({ userGroupTypes: ["LOGGED_IN_USERS"] })
|
|
63
58
|
*
|
|
64
59
|
* export const Route = createFileRoute("/about")({
|
|
65
60
|
* beforeLoad: guardRoute(visibility),
|
|
@@ -14,25 +14,21 @@ const ACCESS_REASONS = [
|
|
|
14
14
|
/** The decision and, when a visitor is denied, the reason. Throws nothing. */
|
|
15
15
|
function checkAccess(visibility, context) {
|
|
16
16
|
const { user } = context;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
reason: "forbidden"
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
17
|
+
const audiences = visibility.userGroupTypes;
|
|
18
|
+
if (user === null) return audiences.includes("NON_LOGGED_IN_USERS") ? { ok: true } : {
|
|
19
|
+
ok: false,
|
|
20
|
+
reason: "unauthenticated"
|
|
21
|
+
};
|
|
22
|
+
if (!audiences.includes("LOGGED_IN_USERS")) return {
|
|
23
|
+
ok: false,
|
|
24
|
+
reason: "alreadyAuthenticated"
|
|
25
|
+
};
|
|
26
|
+
const required = visibility.customGroups ?? [];
|
|
27
|
+
if (required.length === 0) return { ok: true };
|
|
28
|
+
return required.some((id) => user.userGroupIds.includes(id)) ? { ok: true } : {
|
|
29
|
+
ok: false,
|
|
30
|
+
reason: "forbidden"
|
|
31
|
+
};
|
|
36
32
|
}
|
|
37
33
|
/**
|
|
38
34
|
* Declares who may see a route or a region of the page.
|
|
@@ -41,7 +37,7 @@ function checkAccess(visibility, context) {
|
|
|
41
37
|
* declarations out of the source to publish the app's route visibility.
|
|
42
38
|
*
|
|
43
39
|
* @example
|
|
44
|
-
* const visibility = defineVisibility({
|
|
40
|
+
* const visibility = defineVisibility({ userGroupTypes: ["LOGGED_IN_USERS"] })
|
|
45
41
|
*
|
|
46
42
|
* export const Route = createFileRoute("/about")({
|
|
47
43
|
* beforeLoad: guardRoute(visibility),
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@ description: >-
|
|
|
16
16
|
metadata:
|
|
17
17
|
type: sub-skill
|
|
18
18
|
library: softr-sdk
|
|
19
|
-
library_version: 0.0.
|
|
19
|
+
library_version: 0.0.51
|
|
20
20
|
---
|
|
21
21
|
|
|
22
22
|
**Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
|
|
@@ -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.51
|
|
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.
|
|
@@ -38,7 +38,7 @@ const sales = defineQuery({
|
|
|
38
38
|
closedAt: 'Xp2dLh6vKz9Fa3', // DATETIME
|
|
39
39
|
rep: 'Gq5wNt8rJy3Hb1', // LINKED_RECORD
|
|
40
40
|
},
|
|
41
|
-
permissions: {
|
|
41
|
+
permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
|
|
42
42
|
})
|
|
43
43
|
```
|
|
44
44
|
|
|
@@ -8,7 +8,7 @@ description: >-
|
|
|
8
8
|
metadata:
|
|
9
9
|
type: sub-skill
|
|
10
10
|
library: softr-sdk
|
|
11
|
-
library_version: 0.0.
|
|
11
|
+
library_version: 0.0.51
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# The current user
|
|
@@ -71,7 +71,7 @@ const myInvoices = defineQuery({
|
|
|
71
71
|
// The datasource id from the table's registration on the application — not the table id.
|
|
72
72
|
datasourceId: '00000000-0000-0000-0000-000000000001',
|
|
73
73
|
fields: { owner: 'Gq5wNt8rJy3Hb1', amount: 'Vk9cXm2pSz6Ld4' },
|
|
74
|
-
permissions: {
|
|
74
|
+
permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
|
|
75
75
|
})
|
|
76
76
|
|
|
77
77
|
const getMyInvoices = createServerFn().handler(async ({ context }) => {
|
|
@@ -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.51
|
|
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.
|
|
@@ -34,7 +34,7 @@ const createApplication = defineMutation({
|
|
|
34
34
|
datasourceId: '8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77',
|
|
35
35
|
operation: 'create',
|
|
36
36
|
fields: { name: 'V1StGXR8', resume: 'Z7pQm4Kd' },
|
|
37
|
-
permissions: {
|
|
37
|
+
permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
|
|
38
38
|
})
|
|
39
39
|
|
|
40
40
|
const submitApplication = createServerFn({ method: 'POST' })
|
|
@@ -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.51
|
|
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.
|
|
@@ -48,7 +48,7 @@ const listCalendarEvents = defineIntegration({
|
|
|
48
48
|
integrationId: '25d60213-ef4f-4fa0-b094-a46931b2d120',
|
|
49
49
|
method: 'GET',
|
|
50
50
|
url: 'https://www.googleapis.com/calendar/v3/calendars/primary/events',
|
|
51
|
-
permissions: {
|
|
51
|
+
permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
|
|
52
52
|
})
|
|
53
53
|
```
|
|
54
54
|
|
|
@@ -88,7 +88,7 @@ const getStripeCustomer = defineIntegration({
|
|
|
88
88
|
integrationId: 'wsint-stripe',
|
|
89
89
|
method: 'GET',
|
|
90
90
|
url: 'https://api.stripe.com/v1/customers/{id}',
|
|
91
|
-
permissions: {
|
|
91
|
+
permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
|
|
92
92
|
})
|
|
93
93
|
|
|
94
94
|
const fetchCustomer = createServerFn({ method: 'GET' })
|
|
@@ -109,7 +109,7 @@ const postSlackMessage = defineIntegration({
|
|
|
109
109
|
integrationId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
|
110
110
|
method: 'POST',
|
|
111
111
|
url: 'https://slack.com/api/chat.postMessage',
|
|
112
|
-
permissions: {
|
|
112
|
+
permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
|
|
113
113
|
})
|
|
114
114
|
|
|
115
115
|
const sendSlackMessage = createServerFn({ method: 'POST' })
|
|
@@ -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.51
|
|
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.51
|
|
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.
|
|
@@ -45,7 +45,7 @@ const createProject = defineMutation({
|
|
|
45
45
|
status: 'Ru7bVx2cNq0Ed9', // SELECT, choice ids 'draft' | 'active' | 'archived'
|
|
46
46
|
dueDate: 'Xp2dLh6vKz9Fa3', // DATETIME
|
|
47
47
|
},
|
|
48
|
-
permissions: {
|
|
48
|
+
permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
|
|
49
49
|
})
|
|
50
50
|
```
|
|
51
51
|
|
|
@@ -53,7 +53,7 @@ The declaration follows the same rules as a read, spelled out in [record-queries
|
|
|
53
53
|
|
|
54
54
|
The mutation's `fields` bound the write: `data` can only name them, and the returned record only carries them. A delete mutation declares no `fields`, and giving it some is a type error.
|
|
55
55
|
|
|
56
|
-
An update or a delete must not be open to visitors: its `permissions` must be `{
|
|
56
|
+
An update or a delete must not be open to visitors: its `permissions` must be `{ userGroupTypes: ['LOGGED_IN_USERS'] }`. A visitor who is not logged in must never change or remove a record, so naming `NON_LOGGED_IN_USERS` is rejected for these two operations. A create may stay open to visitors, for a sign-up or a contact form.
|
|
57
57
|
|
|
58
58
|
## Core Patterns
|
|
59
59
|
|
|
@@ -81,7 +81,7 @@ const updateProject = defineMutation({
|
|
|
81
81
|
datasourceId: '00000000-0000-0000-0000-000000000000',
|
|
82
82
|
operation: 'update',
|
|
83
83
|
fields: { status: 'Ru7bVx2cNq0Ed9', dueDate: 'Xp2dLh6vKz9Fa3' },
|
|
84
|
-
permissions: {
|
|
84
|
+
permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
|
|
85
85
|
})
|
|
86
86
|
|
|
87
87
|
const archiveProject = createServerFn({ method: 'POST' })
|
|
@@ -102,7 +102,7 @@ const deleteProject = defineMutation({
|
|
|
102
102
|
id: 'deleteProject',
|
|
103
103
|
datasourceId: '00000000-0000-0000-0000-000000000000',
|
|
104
104
|
operation: 'delete',
|
|
105
|
-
permissions: {
|
|
105
|
+
permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
|
|
106
106
|
})
|
|
107
107
|
|
|
108
108
|
const removeProject = createServerFn({ method: 'POST' })
|
|
@@ -123,7 +123,7 @@ const projectDetail = defineQuery({
|
|
|
123
123
|
id: 'projectDetail',
|
|
124
124
|
datasourceId: '00000000-0000-0000-0000-000000000000',
|
|
125
125
|
fields: { status: 'Ru7bVx2cNq0Ed9', dueDate: 'Xp2dLh6vKz9Fa3' },
|
|
126
|
-
permissions: {
|
|
126
|
+
permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
|
|
127
127
|
})
|
|
128
128
|
|
|
129
129
|
const project = await ds.get(projectDetail, { id })
|
|
@@ -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.51
|
|
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. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -46,7 +46,7 @@ const publishedPosts = defineQuery({
|
|
|
46
46
|
publishedAt: 'Xp2dLh6vKz9Fa3',// DATETIME
|
|
47
47
|
views: 'mB4kQ8yTn1sWc7', // NUMBER
|
|
48
48
|
},
|
|
49
|
-
permissions: {
|
|
49
|
+
permissions: { userGroupTypes: ['NON_LOGGED_IN_USERS', 'LOGGED_IN_USERS'] },
|
|
50
50
|
})
|
|
51
51
|
```
|
|
52
52
|
|
|
@@ -54,9 +54,9 @@ const publishedPosts = defineQuery({
|
|
|
54
54
|
- `datasourceId` is the opaque UUID from the table's registration on the application (Softr Studio's Data tab, or the `application_register_data_source` tool when an agent does it). It is not the table id and is never derived from the table's name.
|
|
55
55
|
- The keys of `fields` are names you choose. The values are field ids, opaque strings the table's schema gives you: `database_get_table` returns them for a Softr table, `integration_list_table_fields` for a table on a connected database. Everything downstream speaks your names: the returned records, `where`, `orderBy`.
|
|
56
56
|
- `permissions` declares who may run the query, and the SDK enforces it:
|
|
57
|
-
- `{
|
|
58
|
-
- `{
|
|
59
|
-
- `{
|
|
57
|
+
- `{ userGroupTypes: ['NON_LOGGED_IN_USERS', 'LOGGED_IN_USERS'] }`: everyone, signed out included. `customGroups` narrows only the signed-in half.
|
|
58
|
+
- `{ userGroupTypes: ['NON_LOGGED_IN_USERS'] }`: signed-out visitors only.
|
|
59
|
+
- `{ userGroupTypes: ['LOGGED_IN_USERS'] }`: any signed-in visitor, narrowed to particular groups with `customGroups: ['00000000-0000-0000-0000-00000000000a']`.
|
|
60
60
|
- Write the object inline in the `defineQuery` call. Passing the declaration or its `fields` through a variable, or spreading one in, widens the field ids to `string` and the types stop checking.
|
|
61
61
|
|
|
62
62
|
A query fixes the table and the columns, not the rows: filters, sorting and paging stay at the call site, because a list screen changes them as the user types. Declare as many queries over one datasource as the app needs, each carrying the fields its own screen touches.
|
|
@@ -135,7 +135,7 @@ const myInvoices = defineQuery({
|
|
|
135
135
|
id: 'myInvoices',
|
|
136
136
|
datasourceId: '00000000-0000-0000-0000-000000000001',
|
|
137
137
|
fields: { owner: 'Gq5wNt8rJy3Hb1', amount: 'Vk9cXm2pSz6Ld4' },
|
|
138
|
-
permissions: {
|
|
138
|
+
permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
|
|
139
139
|
})
|
|
140
140
|
|
|
141
141
|
const listMyInvoices = createServerFn().handler(async ({ context }) => {
|
|
@@ -157,7 +157,7 @@ const productStatuses = defineQuery({
|
|
|
157
157
|
id: 'productStatuses',
|
|
158
158
|
datasourceId: '00000000-0000-0000-0000-000000000001', // the linked table's datasource id
|
|
159
159
|
fields: { name: 'fZk3Qm9pLw2Xa1' },
|
|
160
|
-
permissions: {
|
|
160
|
+
permissions: { userGroupTypes: ['NON_LOGGED_IN_USERS', 'LOGGED_IN_USERS'] },
|
|
161
161
|
})
|
|
162
162
|
|
|
163
163
|
const listProductStatuses = createServerFn().handler(async () => {
|
|
@@ -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.51
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `Audience` and `defineVisibility` come from `@softr/sdk` and work anywhere.
|
|
@@ -26,8 +26,8 @@ Wrap the region in `Audience` and give it a rule:
|
|
|
26
26
|
import { Audience, defineVisibility } from '@softr/sdk'
|
|
27
27
|
|
|
28
28
|
const staffOnly = defineVisibility({
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
userGroupTypes: ['LOGGED_IN_USERS'],
|
|
30
|
+
customGroups: ['00000000-0000-0000-0000-00000000000a'],
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
function InvoiceActions({ invoice }: { invoice: Invoice }) {
|
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
name: routes-and-visibility
|
|
3
3
|
description: >-
|
|
4
4
|
Add a route file to a Softr app and declare who may see it: defineVisibility
|
|
5
|
-
with
|
|
6
|
-
ways to apply a rule: guardRoute and assertRouteAccess
|
|
7
|
-
isAuthorized and checkAccess anywhere. Covers per-reason
|
|
8
|
-
redirect and throw strategies, rendering an access error with
|
|
9
|
-
isUnauthenticatedError, isForbiddenError and
|
|
10
|
-
guarding a layout, and the lint rule that
|
|
11
|
-
rules that reject a duplicate, computed
|
|
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.
|
|
12
13
|
metadata:
|
|
13
14
|
type: sub-skill
|
|
14
15
|
library: softr-sdk
|
|
15
|
-
library_version: 0.0.
|
|
16
|
+
library_version: 0.0.51
|
|
16
17
|
---
|
|
17
18
|
|
|
18
19
|
**Boundary.** `defineVisibility`, the access functions and the error predicates come from `@softr/sdk` and work anywhere.
|
|
@@ -29,7 +30,7 @@ Every route file, without exception, looks like this:
|
|
|
29
30
|
import { createFileRoute } from '@tanstack/react-router'
|
|
30
31
|
import { defineVisibility, guardRoute } from '@softr/sdk'
|
|
31
32
|
|
|
32
|
-
const visibility = defineVisibility({
|
|
33
|
+
const visibility = defineVisibility({ userGroupTypes: ['LOGGED_IN_USERS'] })
|
|
33
34
|
|
|
34
35
|
export const Route = createFileRoute('/_app/billing')({
|
|
35
36
|
beforeLoad: guardRoute(visibility),
|
|
@@ -50,19 +51,25 @@ Four constraints on that shape, because Softr reads the file rather than running
|
|
|
50
51
|
- The route guards itself in `beforeLoad`, naming a rule declared in the same file. A rule imported from elsewhere is rejected.
|
|
51
52
|
- Never declare on `__root.tsx`. The root cannot carry an audience, so a declaration there would gate nothing while looking like it does. Softr rejects it. Put the rule on a layout below the root instead.
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
`userGroupTypes` is a set, so a rule names one audience or several:
|
|
54
55
|
|
|
55
56
|
```ts
|
|
56
|
-
defineVisibility({
|
|
57
|
-
defineVisibility({
|
|
58
|
-
defineVisibility({
|
|
59
|
-
defineVisibility({
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
defineVisibility({ userGroupTypes: ['NON_LOGGED_IN_USERS', 'LOGGED_IN_USERS'] }) // everyone, signed in or not
|
|
58
|
+
defineVisibility({ userGroupTypes: ['NON_LOGGED_IN_USERS'] }) // signed-out only: login, signup
|
|
59
|
+
defineVisibility({ userGroupTypes: ['LOGGED_IN_USERS'] }) // any signed-in visitor
|
|
60
|
+
defineVisibility({ // those groups only
|
|
61
|
+
userGroupTypes: ['LOGGED_IN_USERS'],
|
|
62
|
+
customGroups: ['00000000-0000-0000-0000-00000000000a'],
|
|
63
|
+
})
|
|
64
|
+
defineVisibility({ // signed-out visitors AND those groups
|
|
65
|
+
userGroupTypes: ['NON_LOGGED_IN_USERS', 'LOGGED_IN_USERS'],
|
|
66
|
+
customGroups: ['00000000-0000-0000-0000-00000000000a'],
|
|
62
67
|
})
|
|
63
68
|
```
|
|
64
69
|
|
|
65
|
-
|
|
70
|
+
Everyone is both audiences, as in the first line above. A set names each audience at most once and
|
|
71
|
+
is never empty — the type and the build both reject one that does not. `customGroups` narrows only the `LOGGED_IN_USERS` half of the set; absent or empty it
|
|
72
|
+
means **any** signed-in visitor, not "nobody".
|
|
66
73
|
|
|
67
74
|
## Core Patterns
|
|
68
75
|
|
|
@@ -73,7 +80,7 @@ Absent or empty `userGroupIds` means **any** signed-in visitor, not "nobody".
|
|
|
73
80
|
import { Outlet, createFileRoute } from '@tanstack/react-router'
|
|
74
81
|
import { defineVisibility, guardRoute } from '@softr/sdk'
|
|
75
82
|
|
|
76
|
-
const visibility = defineVisibility({
|
|
83
|
+
const visibility = defineVisibility({ userGroupTypes: ['LOGGED_IN_USERS'] })
|
|
77
84
|
|
|
78
85
|
export const Route = createFileRoute('/_app')({
|
|
79
86
|
beforeLoad: guardRoute(visibility),
|
|
@@ -83,7 +90,7 @@ export const Route = createFileRoute('/_app')({
|
|
|
83
90
|
|
|
84
91
|
The whole match chain runs, so a layout guards everything beneath it. Sibling routes do not guard each other. `/admin/index.tsx` does nothing for `/admin/users.tsx` unless an `admin.tsx` layout exists. A route below a guarded layout declares and guards its own rule too.
|
|
85
92
|
|
|
86
|
-
A thrown error renders in place of the guarded route's component. A shell layout guarded with `LOGGED_IN_USERS`, where `unauthenticated` redirects, sends a signed-out visitor away before anything renders there. A rule that can *throw* — a `
|
|
93
|
+
A thrown error renders in place of the guarded route's component. A shell layout guarded with `LOGGED_IN_USERS`, where `unauthenticated` redirects, sends a signed-out visitor away before anything renders there. A rule that can *throw* — a `customGroups` rule with `forbidden` set to throw — should not sit on the shell itself, or the error replaces the nav and chrome. Put it on a layout inside the shell, or on the restricted routes, so the error renders inside the chrome.
|
|
87
94
|
|
|
88
95
|
### Change what a denied visitor gets
|
|
89
96
|
|
|
@@ -123,7 +130,7 @@ All three reasons are required, because nothing fills in a reason the file leave
|
|
|
123
130
|
| `forbidden` | Signed in, in none of the named groups |
|
|
124
131
|
| `alreadyAuthenticated` | Signed-in visitor on a signed-out-only route |
|
|
125
132
|
|
|
126
|
-
That file covers every route at once; a route's own overrides win over it. `
|
|
133
|
+
That file covers every route at once; a route's own overrides win over it. A rule naming both audiences with no `customGroups` admits everyone, so it never redirects and never throws. Only `strategy: 'throw'` reaches an `errorComponent`. A redirect navigates away and the route never renders.
|
|
127
134
|
|
|
128
135
|
### Render the error in place
|
|
129
136
|
|
|
@@ -132,8 +139,8 @@ import { createFileRoute } from '@tanstack/react-router'
|
|
|
132
139
|
import { defineVisibility, guardRoute, isAccessError, isForbiddenError } from '@softr/sdk'
|
|
133
140
|
|
|
134
141
|
const visibility = defineVisibility({
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
userGroupTypes: ['LOGGED_IN_USERS'],
|
|
143
|
+
customGroups: ['00000000-0000-0000-0000-00000000000a'],
|
|
137
144
|
})
|
|
138
145
|
|
|
139
146
|
export const Route = createFileRoute('/_app/billing')({
|
|
@@ -155,7 +162,7 @@ Use the predicates: `isAccessError` for any of the three, and `isUnauthenticated
|
|
|
155
162
|
|
|
156
163
|
`guardRoute` covers almost everything. When a route needs its own logic afterwards, `assertRouteAccess(visibility, context)` applies the same rule and throws the same access error, using the default behaviour for each reason. It takes no overrides.
|
|
157
164
|
|
|
158
|
-
It also narrows `context.user` for the rest of the handler: `User` under `LOGGED_IN_USERS`, `null` under `NON_LOGGED_IN_USERS`. `guardRoute` cannot, because it hands `beforeLoad` a function rather than running inside one.
|
|
165
|
+
It also narrows `context.user` for the rest of the handler, where the set names one audience and no other: `User` under `['LOGGED_IN_USERS']`, `null` under `['NON_LOGGED_IN_USERS']`. A set naming both admits either kind of visitor, so it narrows to neither. `guardRoute` cannot, because it hands `beforeLoad` a function rather than running inside one.
|
|
159
166
|
|
|
160
167
|
```ts
|
|
161
168
|
import { redirect } from '@tanstack/react-router'
|
|
@@ -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.51
|
|
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.
|
|
@@ -35,7 +35,7 @@ const sendWelcomeEmail = defineWorkflow({
|
|
|
35
35
|
id: 'sendWelcomeEmail',
|
|
36
36
|
workflowId: 'c9d8e7f6-a5b4-4c3d-8e2f-1a0b9c8d7e6f',
|
|
37
37
|
triggerId: '926da496-749f-4006-999c-197d8a556122',
|
|
38
|
-
permissions: {
|
|
38
|
+
permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
|
|
39
39
|
body: {
|
|
40
40
|
email: '{LOGGED_IN_USER:EMAIL}', // resolved server-side from the signed-in user
|
|
41
41
|
name: '{LOGGED_IN_USER:NAME}',
|