@softr/sdk 0.0.38 → 0.0.40
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/cli.js +3 -2
- package/dist/core/file-upload.js +4 -7
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/package.js +1 -1
- package/dist/routes/visibility.d.ts +4 -4
- package/dist/routes/visibility.js +8 -18
- package/dist/softr-config.js +19 -0
- package/dist/vite.d.ts +0 -1
- package/dist/vite.js +37 -2
- package/package.json +2 -2
- package/schema/softr.schema.json +58 -0
- package/skills/softr-sdk/SKILL.md +1 -1
- package/skills/softr-sdk/auth/SKILL.md +2 -2
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/file-uploads/SKILL.md +4 -5
- package/skills/softr-sdk/integrations/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
- package/skills/softr-sdk/record-queries/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +27 -12
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -8,8 +8,9 @@ import path from "node:path";
|
|
|
8
8
|
//#region src/cli.ts
|
|
9
9
|
const OUTPUT = "softr-env.d.ts";
|
|
10
10
|
async function generate() {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const input = await fetchSoftrEnvInput(metadataSourceFromEnv());
|
|
12
|
+
writeFileSync(path.resolve(process.cwd(), OUTPUT), renderSoftrEnv(input, version), "utf8");
|
|
13
|
+
console.log(`[softr] wrote ${OUTPUT} (${input.datasources.length} datasources)`);
|
|
13
14
|
}
|
|
14
15
|
const [command] = process.argv.slice(2);
|
|
15
16
|
if (command === "generate") await generate();
|
package/dist/core/file-upload.js
CHANGED
|
@@ -19,14 +19,11 @@ async function upload(action, file) {
|
|
|
19
19
|
body: formData
|
|
20
20
|
});
|
|
21
21
|
if (!res.ok) throw new Error(`File upload failed: ${res.status}`);
|
|
22
|
-
const
|
|
23
|
-
const parsed = new URL(rawUrl);
|
|
24
|
-
const fileId = parsed.searchParams.get("fileId") || randomUUID();
|
|
25
|
-
parsed.searchParams.delete("fileId");
|
|
22
|
+
const resultJSON = await res.json();
|
|
26
23
|
return {
|
|
27
|
-
url:
|
|
28
|
-
filename:
|
|
29
|
-
id:
|
|
24
|
+
url: resultJSON.url,
|
|
25
|
+
filename: resultJSON.filename || "Untitled file",
|
|
26
|
+
id: resultJSON.id || randomUUID()
|
|
30
27
|
};
|
|
31
28
|
}
|
|
32
29
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
|
|
2
2
|
import { ActionPermissions, SoftrDatasources, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
3
3
|
import { FileAttachment } from "./core/file-upload.js";
|
|
4
|
+
import { FileItem, useFileUpload } from "./core/use-file-upload.js";
|
|
4
5
|
import { SoftrContext } from "./auth/context.js";
|
|
5
|
-
import { defineVisibility, isAccessError,
|
|
6
|
+
import { defineVisibility, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
|
|
6
7
|
import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions } from "./auth/isomorphic.js";
|
|
7
8
|
import { useUserTimezone } from "./timezone.js";
|
|
8
9
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
9
|
-
export { type ActionPermissions, DefaultErrorOverlay, type FileAttachment, type SoftrContext, type SoftrDatasources, type SoftrUserGroupId, type SoftrUserGroups, type User, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, getSoftrContext, initialSoftrContext, isAccessError,
|
|
10
|
+
export { type ActionPermissions, DefaultErrorOverlay, type FileAttachment, type FileItem, type SoftrContext, type SoftrDatasources, type SoftrUserGroupId, type SoftrUserGroups, type User, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, getSoftrContext, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, useCurrentUser, useFileUpload, usePermissions, useUserTimezone };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { useFileUpload } from "./core/use-file-upload.js";
|
|
1
2
|
import { defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
2
|
-
import { defineVisibility, isAccessError,
|
|
3
|
+
import { defineVisibility, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
|
|
3
4
|
import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions } from "./auth/isomorphic.js";
|
|
4
5
|
import { useUserTimezone } from "./timezone.js";
|
|
5
6
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
6
7
|
|
|
7
|
-
export { DefaultErrorOverlay, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, getSoftrContext, initialSoftrContext, isAccessError,
|
|
8
|
+
export { DefaultErrorOverlay, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, getSoftrContext, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, useCurrentUser, useFileUpload, usePermissions, useUserTimezone };
|
package/dist/package.js
CHANGED
|
@@ -24,9 +24,9 @@ type RouteVisibility = {
|
|
|
24
24
|
*
|
|
25
25
|
* - `unauthenticated` — the route needs a signed-in visitor and there is none.
|
|
26
26
|
* - `forbidden` — signed in, but in none of the groups the route names.
|
|
27
|
-
* - `
|
|
27
|
+
* - `alreadyAuthenticated` — the route is for signed-out visitors only.
|
|
28
28
|
*/
|
|
29
|
-
declare const ACCESS_REASONS: readonly ["unauthenticated", "forbidden", "
|
|
29
|
+
declare const ACCESS_REASONS: readonly ["unauthenticated", "forbidden", "alreadyAuthenticated"];
|
|
30
30
|
type AccessReason = (typeof ACCESS_REASONS)[number];
|
|
31
31
|
/**
|
|
32
32
|
* What a guard does when it refuses a visitor: redirect to a path, or throw an
|
|
@@ -78,7 +78,7 @@ declare function isUnauthenticatedError(error: unknown): error is AccessError;
|
|
|
78
78
|
/** True for an access error thrown because the visitor was in none of the groups. */
|
|
79
79
|
declare function isForbiddenError(error: unknown): error is AccessError;
|
|
80
80
|
/** True for an access error thrown because a signed-out-only route saw a user. */
|
|
81
|
-
declare function
|
|
81
|
+
declare function isAlreadyAuthenticatedError(error: unknown): error is AccessError;
|
|
82
82
|
type Narrowed<R extends RouteVisibility> = R extends {
|
|
83
83
|
userGroup: "LOGGED_IN_USERS";
|
|
84
84
|
} ? AccessContext & {
|
|
@@ -139,4 +139,4 @@ type VisibilityHandle<R extends RouteVisibility = RouteVisibility> = {
|
|
|
139
139
|
*/
|
|
140
140
|
declare function defineVisibility<const R extends RouteVisibility>(rule: R): VisibilityHandle<R>;
|
|
141
141
|
//#endregion
|
|
142
|
-
export { defineVisibility, isAccessError,
|
|
142
|
+
export { defineVisibility, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { panic } from "../assertions.js";
|
|
1
2
|
import { redirect } from "@tanstack/react-router";
|
|
2
3
|
|
|
3
4
|
//#region src/routes/visibility.ts
|
|
@@ -6,12 +7,12 @@ import { redirect } from "@tanstack/react-router";
|
|
|
6
7
|
*
|
|
7
8
|
* - `unauthenticated` — the route needs a signed-in visitor and there is none.
|
|
8
9
|
* - `forbidden` — signed in, but in none of the groups the route names.
|
|
9
|
-
* - `
|
|
10
|
+
* - `alreadyAuthenticated` — the route is for signed-out visitors only.
|
|
10
11
|
*/
|
|
11
12
|
const ACCESS_REASONS = [
|
|
12
13
|
"unauthenticated",
|
|
13
14
|
"forbidden",
|
|
14
|
-
"
|
|
15
|
+
"alreadyAuthenticated"
|
|
15
16
|
];
|
|
16
17
|
/** Decides whether a visitor passes a visibility rule, without throwing. */
|
|
17
18
|
function checkAccess(rule, context) {
|
|
@@ -20,7 +21,7 @@ function checkAccess(rule, context) {
|
|
|
20
21
|
case "ALL_USERS": return { ok: true };
|
|
21
22
|
case "NON_LOGGED_IN_USERS": return user === null ? { ok: true } : {
|
|
22
23
|
ok: false,
|
|
23
|
-
reason: "
|
|
24
|
+
reason: "alreadyAuthenticated"
|
|
24
25
|
};
|
|
25
26
|
case "LOGGED_IN_USERS": {
|
|
26
27
|
if (user === null) return {
|
|
@@ -63,23 +64,12 @@ function isForbiddenError(error) {
|
|
|
63
64
|
return isReason(error, "forbidden");
|
|
64
65
|
}
|
|
65
66
|
/** True for an access error thrown because a signed-out-only route saw a user. */
|
|
66
|
-
function
|
|
67
|
-
return isReason(error, "
|
|
67
|
+
function isAlreadyAuthenticatedError(error) {
|
|
68
|
+
return isReason(error, "alreadyAuthenticated");
|
|
68
69
|
}
|
|
69
|
-
const DEFAULT_ACCESS_BEHAVIOUR = {
|
|
70
|
-
unauthenticated: {
|
|
71
|
-
strategy: "redirect",
|
|
72
|
-
to: "/login"
|
|
73
|
-
},
|
|
74
|
-
forbidden: { strategy: "throw" },
|
|
75
|
-
alreadySignedIn: {
|
|
76
|
-
strategy: "redirect",
|
|
77
|
-
to: "/"
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
70
|
/** What a guard throws for a reason: a redirect, or an {@link AccessError}. */
|
|
81
71
|
function thrownFor(reason, overrides) {
|
|
82
|
-
const behaviour = overrides?.[reason] ??
|
|
72
|
+
const behaviour = overrides?.[reason] ?? (typeof __SOFTR_CONFIG__ === "undefined" ? panic("softr.json is missing, so no guard behaviour is configured") : __SOFTR_CONFIG__.auth.guard[reason]);
|
|
83
73
|
const resolved = typeof behaviour === "string" ? {
|
|
84
74
|
strategy: "redirect",
|
|
85
75
|
to: behaviour
|
|
@@ -122,4 +112,4 @@ function defineVisibility(rule) {
|
|
|
122
112
|
}
|
|
123
113
|
|
|
124
114
|
//#endregion
|
|
125
|
-
export { defineVisibility, isAccessError,
|
|
115
|
+
export { defineVisibility, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/softr-config.ts
|
|
4
|
+
const AccessBehaviour = z.union([
|
|
5
|
+
z.string(),
|
|
6
|
+
z.object({
|
|
7
|
+
strategy: z.literal("redirect"),
|
|
8
|
+
to: z.string()
|
|
9
|
+
}),
|
|
10
|
+
z.object({ strategy: z.literal("throw") })
|
|
11
|
+
], { error: "must be a path like \"/login\", { \"strategy\": \"redirect\", \"to\": \"/login\" } or { \"strategy\": \"throw\" }" });
|
|
12
|
+
const SoftrConfigSchema = z.object({ auth: z.strictObject({ guard: z.strictObject({
|
|
13
|
+
unauthenticated: AccessBehaviour,
|
|
14
|
+
forbidden: AccessBehaviour,
|
|
15
|
+
alreadyAuthenticated: AccessBehaviour
|
|
16
|
+
}) }) });
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
export { SoftrConfigSchema };
|
package/dist/vite.d.ts
CHANGED
package/dist/vite.js
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
|
+
import { assert, panic } from "./assertions.js";
|
|
2
|
+
import { SoftrConfigSchema } from "./softr-config.js";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
1
4
|
import path from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
2
6
|
import { pathToFileURL } from "node:url";
|
|
3
7
|
|
|
4
8
|
//#region src/vite.ts
|
|
5
|
-
|
|
9
|
+
const SOFTR_CONFIG_FILE = "softr.json";
|
|
6
10
|
function softr() {
|
|
7
|
-
return [editEngine()];
|
|
11
|
+
return [softrConfig(), editEngine()];
|
|
12
|
+
}
|
|
13
|
+
function softrConfig() {
|
|
14
|
+
let root;
|
|
15
|
+
return {
|
|
16
|
+
name: "softr:config",
|
|
17
|
+
config(userConfig) {
|
|
18
|
+
root = path.resolve(userConfig.root ?? process.cwd());
|
|
19
|
+
return { define: { __SOFTR_CONFIG__: JSON.stringify(loadSoftrConfig(root)) } };
|
|
20
|
+
},
|
|
21
|
+
configureServer(server) {
|
|
22
|
+
const file = path.join(root, SOFTR_CONFIG_FILE);
|
|
23
|
+
server.watcher.add(file);
|
|
24
|
+
server.watcher.on("change", (changed) => {
|
|
25
|
+
if (changed === file) server.restart();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
};
|
|
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;
|
|
8
43
|
}
|
|
9
44
|
/**
|
|
10
45
|
* The edit engine is provided by the pod runtime rather than an app dependency: it is never in the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softr/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.40",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"./vite": { "types": "./dist/vite.d.ts", "import": "./dist/vite.js" }
|
|
10
10
|
},
|
|
11
11
|
"bin": { "softr-sdk": "./dist/cli.js" },
|
|
12
|
-
"files": ["dist", "skills", "!skills/_artifacts"],
|
|
12
|
+
"files": ["dist", "schema", "skills", "!skills/_artifacts"],
|
|
13
13
|
"publishConfig": { "access": "public" },
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsdown",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "softr.json",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"auth": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"description": "Authentication. Written in this repo and read by the app's build.",
|
|
9
|
+
"properties": {
|
|
10
|
+
"guard": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"description": "What a route guard does when it denies a visitor. A route's own overrides win over these.",
|
|
13
|
+
"properties": {
|
|
14
|
+
"unauthenticated": {
|
|
15
|
+
"$ref": "#/$defs/accessBehaviour",
|
|
16
|
+
"description": "The route needs a signed-in visitor and there is none. Defaults to a redirect to /login."
|
|
17
|
+
},
|
|
18
|
+
"alreadyAuthenticated": {
|
|
19
|
+
"$ref": "#/$defs/accessBehaviour",
|
|
20
|
+
"description": "The route is for signed-out visitors only. Defaults to a redirect to /."
|
|
21
|
+
},
|
|
22
|
+
"forbidden": {
|
|
23
|
+
"$ref": "#/$defs/accessBehaviour",
|
|
24
|
+
"description": "Signed in, but in none of the groups the route names. Defaults to throwing, for the route's errorComponent to render."
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"required": ["unauthenticated", "alreadyAuthenticated", "forbidden"],
|
|
28
|
+
"additionalProperties": false
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"required": ["guard"],
|
|
32
|
+
"additionalProperties": false
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"required": ["auth"],
|
|
36
|
+
"$defs": {
|
|
37
|
+
"accessBehaviour": {
|
|
38
|
+
"oneOf": [
|
|
39
|
+
{ "type": "string", "description": "A path to redirect to." },
|
|
40
|
+
{
|
|
41
|
+
"type": "object",
|
|
42
|
+
"properties": {
|
|
43
|
+
"strategy": { "const": "redirect" },
|
|
44
|
+
"to": { "type": "string" }
|
|
45
|
+
},
|
|
46
|
+
"required": ["strategy", "to"],
|
|
47
|
+
"additionalProperties": false
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": { "strategy": { "const": "throw" } },
|
|
52
|
+
"required": ["strategy"],
|
|
53
|
+
"additionalProperties": false
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -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.40
|
|
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. `defineVisibility`, the hooks and every type come from `@softr/sdk`. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -353,5 +353,5 @@ After a reset, enrol the new app with `startAuthenticatorSetup` and `confirmAuth
|
|
|
353
353
|
## References
|
|
354
354
|
|
|
355
355
|
- [current-user/SKILL.md](../current-user/SKILL.md): reading the visitor these calls establish
|
|
356
|
-
- [routes-and-visibility/SKILL.md](../routes-and-visibility/SKILL.md): `NON_LOGGED_IN_USERS` and the `
|
|
356
|
+
- [routes-and-visibility/SKILL.md](../routes-and-visibility/SKILL.md): `NON_LOGGED_IN_USERS` and the `alreadyAuthenticated` redirect that keeps a signed-in visitor off a login page
|
|
357
357
|
- `src/routes/_auth/login.tsx` and `signup.tsx` in your own app: the implementations it was scaffolded with
|
|
@@ -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.40
|
|
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. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -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.40
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**Boundary.** `useCurrentUser`, `usePermissions` and every type come from `@softr/sdk` and work anywhere; `ds` and `auth` live on `@softr/sdk/server` and run only inside a `createServerFn` handler. The app is already wired — `src/start.ts` registers `softrAuthMiddleware`, `src/routes/__root.tsx` publishes the visitor as route context — never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: file-uploads
|
|
3
3
|
description: >-
|
|
4
|
-
Upload files from a form or edit view: useFileUpload(action) from @softr/sdk
|
|
4
|
+
Upload files from a form or edit view: useFileUpload(action) from @softr/sdk does
|
|
5
5
|
everything — XHR upload with real progress, per-upload cancel, and isUploading to block
|
|
6
6
|
form submission. No route registration needed; the SDK ships its own server function.
|
|
7
7
|
For server-side uploads or plain forms, use upload(actionId, file) from @softr/sdk/server.
|
|
@@ -9,10 +9,10 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.40
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
**Boundary.** `upload` lives on `@softr/sdk/server` and runs inside server functions. `useFileUpload` and `FileItem` live on `@softr/sdk
|
|
15
|
+
**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.
|
|
16
16
|
|
|
17
17
|
# File uploads
|
|
18
18
|
|
|
@@ -21,9 +21,8 @@ metadata:
|
|
|
21
21
|
## Setup
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
|
-
import { defineMutation, type FileAttachment } from '@softr/sdk'
|
|
24
|
+
import { defineMutation, type FileAttachment, useFileUpload, type FileItem } from '@softr/sdk'
|
|
25
25
|
import { ds } from '@softr/sdk/server'
|
|
26
|
-
import { useFileUpload, type FileItem } from '@softr/sdk/client'
|
|
27
26
|
```
|
|
28
27
|
|
|
29
28
|
## Core pattern
|
|
@@ -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.40
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**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. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -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.40
|
|
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. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -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.40
|
|
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. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -6,13 +6,13 @@ description: >-
|
|
|
6
6
|
four ways to enforce a rule (guard, assert, isAuthorized, check). Covers
|
|
7
7
|
per-reason overrides, the redirect and throw strategies, rendering an access
|
|
8
8
|
error with isAccessError, isUnauthenticatedError, isForbiddenError and
|
|
9
|
-
|
|
9
|
+
isAlreadyAuthenticatedError, guarding a layout, and the lint rule that requires a
|
|
10
10
|
declaration alongside the rules that reject a duplicate, computed or
|
|
11
11
|
root-level one.
|
|
12
12
|
metadata:
|
|
13
13
|
type: sub-skill
|
|
14
14
|
library: softr-sdk
|
|
15
|
-
library_version: 0.0.
|
|
15
|
+
library_version: 0.0.40
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
**Boundary.** `defineVisibility`, the error predicates and every type come from `@softr/sdk` and work anywhere; `ds` and `auth` live on `@softr/sdk/server` and run only inside a `createServerFn` handler. The app is already wired — `src/start.ts` registers `softrAuthMiddleware`, `src/routes/__root.tsx` publishes the visitor as route context — never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -82,7 +82,7 @@ export const Route = createFileRoute('/_app')({
|
|
|
82
82
|
|
|
83
83
|
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. Routes below a guarded layout still declare their own rule, because the declaration is what Studio reads.
|
|
84
84
|
|
|
85
|
-
A thrown error renders in place of the guarded route's component.
|
|
85
|
+
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 `userGroupIds` 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.
|
|
86
86
|
|
|
87
87
|
### Change what a denied visitor gets
|
|
88
88
|
|
|
@@ -99,15 +99,30 @@ beforeLoad: visibility.guard({
|
|
|
99
99
|
|
|
100
100
|
The long redirect form and the bare string do the same thing.
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
Every app declares where a denied visitor goes in `softr.json`, at the app root, and every app has one:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"$schema": "./node_modules/@softr/sdk/schema/softr.schema.json",
|
|
107
|
+
"auth": {
|
|
108
|
+
"guard": {
|
|
109
|
+
"unauthenticated": "/login",
|
|
110
|
+
"alreadyAuthenticated": "/",
|
|
111
|
+
"forbidden": { "strategy": "throw" }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
All three reasons are required, because nothing fills in a reason the file leaves out:
|
|
103
118
|
|
|
104
|
-
| Reason | Condition |
|
|
105
|
-
| --- | --- |
|
|
106
|
-
| `unauthenticated` | Route needs a visitor, none present |
|
|
107
|
-
| `forbidden` | Signed in, in none of the named groups |
|
|
108
|
-
| `
|
|
119
|
+
| Reason | Condition |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `unauthenticated` | Route needs a visitor, none present |
|
|
122
|
+
| `forbidden` | Signed in, in none of the named groups |
|
|
123
|
+
| `alreadyAuthenticated` | Signed-in visitor on a signed-out-only route |
|
|
109
124
|
|
|
110
|
-
`ALL_USERS` admits everyone, so it never redirects and never throws. Only `strategy: 'throw'` reaches an `errorComponent`. A redirect navigates away and the route never renders.
|
|
125
|
+
That file covers every route at once; a route's own `guard()` overrides win over it. `ALL_USERS` admits everyone, so it never redirects and never throws. Only `strategy: 'throw'` reaches an `errorComponent`. A redirect navigates away and the route never renders.
|
|
111
126
|
|
|
112
127
|
### Render the error in place
|
|
113
128
|
|
|
@@ -133,11 +148,11 @@ export const Route = createFileRoute('/_app/billing')({
|
|
|
133
148
|
|
|
134
149
|
The error carries a `type` discriminant and its `reason`, nothing else. It never names the groups the route wanted, which would hand that list to the one visitor who was just turned away.
|
|
135
150
|
|
|
136
|
-
Use the predicates: `isAccessError` for any of the three, and `isUnauthenticatedError`, `isForbiddenError`, `
|
|
151
|
+
Use the predicates: `isAccessError` for any of the three, and `isUnauthenticatedError`, `isForbiddenError`, `isAlreadyAuthenticatedError` for one each. The error is plain data rather than an `Error` subclass, so `instanceof` never matches and there is no `message` to read. That is also why the parameter is annotated `error: unknown`. The router types it as `Error`, and the wider annotation is what lets the predicates narrow it. Leave it as it is.
|
|
137
152
|
|
|
138
153
|
### Run the check and then keep going
|
|
139
154
|
|
|
140
|
-
`guard()` covers almost everything. When a route needs its own logic afterwards, `assert()` applies the same rule and throws the same access error, using
|
|
155
|
+
`guard()` covers almost everything. When a route needs its own logic afterwards, `assert()` applies the same rule and throws the same access error, using `softr.json` for each reason. It takes no overrides.
|
|
141
156
|
|
|
142
157
|
`assert()` guarantees the visitor at runtime but does not narrow `context.user` for the compiler. `isAuthorized()` is the one that narrows. It is a type predicate, so inside its `true` branch `context.user` is `User` under `LOGGED_IN_USERS` and `null` under `NON_LOGGED_IN_USERS`.
|
|
143
158
|
|
|
@@ -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.40
|
|
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. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|