@softr/sdk 0.0.20 → 0.0.22-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/isomorphic.d.ts +2 -2
- package/dist/auth/isomorphic.js +3 -2
- package/dist/auth/methods/password.js +8 -14
- package/dist/auth/platform.js +5 -40
- package/dist/auth/session.d.ts +16 -0
- package/dist/auth/session.js +27 -14
- package/dist/auth/user.d.ts +4 -1
- package/dist/auth/user.js +12 -0
- package/dist/boot-error.js +252 -0
- package/dist/server.d.ts +2 -1
- package/dist/server.js +2 -1
- package/package.json +1 -1
- package/skills/softr-sdk/SKILL.md +3 -3
- package/skills/softr-sdk/auth/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +11 -6
- package/skills/softr-sdk/data-mutations/SKILL.md +14 -6
- package/skills/softr-sdk/reading-records/SKILL.md +27 -17
- package/skills/softr-sdk/reading-records/references/query-builder.md +3 -3
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
|
@@ -18,7 +18,7 @@ import * as _tanstack_react_start0 from "@tanstack/react-start";
|
|
|
18
18
|
* requestMiddleware: [softrAuthMiddleware()],
|
|
19
19
|
* }))
|
|
20
20
|
*/
|
|
21
|
-
declare function softrAuthMiddleware(): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, {
|
|
21
|
+
declare function softrAuthMiddleware(): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, readonly [_tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>], {
|
|
22
22
|
user: User | null;
|
|
23
23
|
permissions: Permissions;
|
|
24
24
|
}>;
|
|
@@ -48,7 +48,7 @@ declare function initialSoftrContext(): SoftrContext;
|
|
|
48
48
|
* @example
|
|
49
49
|
* function Greeting() {
|
|
50
50
|
* const user = useCurrentUser()
|
|
51
|
-
* return <p>{user ? `Hello, ${user.
|
|
51
|
+
* return <p>{user ? `Hello, ${user.fullName}` : "Please sign in"}</p>
|
|
52
52
|
* }
|
|
53
53
|
*/
|
|
54
54
|
declare function useCurrentUser(): User | null;
|
package/dist/auth/isomorphic.js
CHANGED
|
@@ -2,6 +2,7 @@ import { assert, panic, requireDefined } from "../assertions.js";
|
|
|
2
2
|
import { writeCookies } from "../cookies.js";
|
|
3
3
|
import { sessionConfig } from "./config.js";
|
|
4
4
|
import { makePlatform } from "./platform.js";
|
|
5
|
+
import { bootErrorMiddleware } from "../boot-error.js";
|
|
5
6
|
import { clearSession, makeSessionDataCookie, readSessionData, readSessionToken } from "./session.js";
|
|
6
7
|
import { rootRouteId, useRouteContext } from "@tanstack/react-router";
|
|
7
8
|
import { createMiddleware, createServerFn, getGlobalStartContext } from "@tanstack/react-start";
|
|
@@ -20,7 +21,7 @@ import { createMiddleware, createServerFn, getGlobalStartContext } from "@tansta
|
|
|
20
21
|
* }))
|
|
21
22
|
*/
|
|
22
23
|
function softrAuthMiddleware() {
|
|
23
|
-
return createMiddleware({ type: "request" }).server(async ({ next }) => {
|
|
24
|
+
return createMiddleware({ type: "request" }).middleware([bootErrorMiddleware()]).server(async ({ next }) => {
|
|
24
25
|
async function getSession() {
|
|
25
26
|
const token = readSessionToken();
|
|
26
27
|
const cached = readSessionData(token);
|
|
@@ -77,7 +78,7 @@ function initialSoftrContext() {
|
|
|
77
78
|
* @example
|
|
78
79
|
* function Greeting() {
|
|
79
80
|
* const user = useCurrentUser()
|
|
80
|
-
* return <p>{user ? `Hello, ${user.
|
|
81
|
+
* return <p>{user ? `Hello, ${user.fullName}` : "Please sign in"}</p>
|
|
81
82
|
* }
|
|
82
83
|
*/
|
|
83
84
|
function useCurrentUser() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { panic, softAssert
|
|
1
|
+
import { panic, softAssert } from "../../assertions.js";
|
|
2
2
|
import { writeCookies } from "../../cookies.js";
|
|
3
3
|
import { sessionConfig } from "../config.js";
|
|
4
4
|
import { makePlatform } from "../platform.js";
|
|
@@ -10,26 +10,20 @@ import { authError } from "./utils.js";
|
|
|
10
10
|
* Signs a visitor in with an email and password.
|
|
11
11
|
*/
|
|
12
12
|
async function signIn(credentials) {
|
|
13
|
-
const
|
|
14
|
-
const result = await platform.signIn(credentials);
|
|
13
|
+
const result = await makePlatform(sessionConfig().applicationId).tokens(credentials);
|
|
15
14
|
if (!result.ok) {
|
|
16
15
|
if (result.status === 401 || result.status === 429) return authError(result.status === 401 ? "invalidCredentials" : "tooManyAttempts", result.message);
|
|
17
16
|
if (result.status >= 500) panic(result.message);
|
|
18
|
-
if (result.status === 400) todo(result.message);
|
|
19
17
|
throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows sign-in.`);
|
|
20
18
|
}
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (!sessionResult.ok) panic(sessionResult.message);
|
|
27
|
-
const session = sessionResult.data;
|
|
28
|
-
if (session.user === null) panic("Session token issued, but no user came back with it.");
|
|
29
|
-
writeCookies([makeSessionTokenCookie(token), makeSessionDataCookie(session, token)]);
|
|
19
|
+
const { jwt, user, permissions } = result.data;
|
|
20
|
+
writeCookies([makeSessionTokenCookie(jwt), makeSessionDataCookie({
|
|
21
|
+
user,
|
|
22
|
+
permissions
|
|
23
|
+
}, jwt)]);
|
|
30
24
|
return {
|
|
31
25
|
success: true,
|
|
32
|
-
data:
|
|
26
|
+
data: user
|
|
33
27
|
};
|
|
34
28
|
}
|
|
35
29
|
/**
|
package/dist/auth/platform.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { panic } from "../assertions.js";
|
|
2
2
|
import { sessionConfig } from "./config.js";
|
|
3
|
+
import { userSchema } from "./user.js";
|
|
3
4
|
import { permissionsSchema } from "./permissions.js";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
|
|
@@ -11,22 +12,9 @@ function makePlatform(applicationId) {
|
|
|
11
12
|
method: "GET",
|
|
12
13
|
token
|
|
13
14
|
}, z.object({
|
|
14
|
-
user:
|
|
15
|
-
id: z.string(),
|
|
16
|
-
email: z.string().nullish(),
|
|
17
|
-
fullName: z.string().nullish(),
|
|
18
|
-
userGroupIds: z.array(z.string()).nullish()
|
|
19
|
-
}).nullish(),
|
|
15
|
+
user: userSchema.nullable(),
|
|
20
16
|
permissions: permissionsSchema
|
|
21
|
-
})
|
|
22
|
-
user: user ? {
|
|
23
|
-
id: user.id,
|
|
24
|
-
email: user.email ?? "",
|
|
25
|
-
name: user.fullName ?? "",
|
|
26
|
-
userGroupIds: user.userGroupIds ?? []
|
|
27
|
-
} : null,
|
|
28
|
-
permissions
|
|
29
|
-
})));
|
|
17
|
+
}));
|
|
30
18
|
if (result.status === 404) panic(`Application ${applicationId} not found. Check SOFTR_APPLICATION_ID.`);
|
|
31
19
|
return result;
|
|
32
20
|
},
|
|
@@ -35,32 +23,9 @@ function makePlatform(applicationId) {
|
|
|
35
23
|
json: credentials
|
|
36
24
|
}, z.object({
|
|
37
25
|
jwt: z.string(),
|
|
38
|
-
user:
|
|
39
|
-
id: z.string(),
|
|
40
|
-
email: z.string().nullish(),
|
|
41
|
-
fullName: z.string().nullish(),
|
|
42
|
-
userGroupIds: z.array(z.string()).nullish()
|
|
43
|
-
}).nullish(),
|
|
26
|
+
user: userSchema,
|
|
44
27
|
permissions: permissionsSchema
|
|
45
|
-
})
|
|
46
|
-
token: jwt,
|
|
47
|
-
session: {
|
|
48
|
-
user: user ? {
|
|
49
|
-
id: user.id,
|
|
50
|
-
email: user.email ?? "",
|
|
51
|
-
name: user.fullName ?? "",
|
|
52
|
-
userGroupIds: user.userGroupIds ?? []
|
|
53
|
-
} : null,
|
|
54
|
-
permissions
|
|
55
|
-
}
|
|
56
|
-
}))),
|
|
57
|
-
signIn: (credentials) => request(`/v1/applications/${applicationId}/users/sign-in`, {
|
|
58
|
-
method: "POST",
|
|
59
|
-
json: credentials
|
|
60
|
-
}, z.object({
|
|
61
|
-
jwt: z.string().nullish(),
|
|
62
|
-
authenticatorAppAction: z.string().nullish()
|
|
63
|
-
}).nullable()),
|
|
28
|
+
})),
|
|
64
29
|
signUp: (body) => request(`/v1/applications/${applicationId}/users/sign-up`, {
|
|
65
30
|
method: "POST",
|
|
66
31
|
json: body
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import "./context.js";
|
|
2
|
+
|
|
3
|
+
//#region src/auth/session.d.ts
|
|
4
|
+
|
|
5
|
+
declare function makeSessionTokenCookie(token: string, secure?: boolean): string;
|
|
6
|
+
/** The clearing pair, for the same out-of-context caller as `makeSessionTokenCookie`. */
|
|
7
|
+
declare function makeSessionClearingCookies(secure: boolean): Array<string>;
|
|
8
|
+
/**
|
|
9
|
+
* The name `readSessionToken` would look the token up under on a request of this
|
|
10
|
+
* scheme, for a caller that only needs to know whether one is present and cannot
|
|
11
|
+
* decode it. Same `secure` argument as `makeSessionTokenCookie`, so a caller that
|
|
12
|
+
* writes and a caller that probes cannot disagree about the name.
|
|
13
|
+
*/
|
|
14
|
+
declare function sessionTokenCookieName(secure: boolean): string;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { makeSessionClearingCookies, makeSessionTokenCookie, sessionTokenCookieName };
|
package/dist/auth/session.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readRequestCookies, serializeCookie, writeCookies } from "../cookies.js";
|
|
2
2
|
import { sessionConfig } from "./config.js";
|
|
3
|
+
import { userSchema } from "./user.js";
|
|
3
4
|
import { permissionsSchema } from "./permissions.js";
|
|
4
5
|
import { getRequestProtocol } from "@tanstack/react-start/server";
|
|
5
6
|
import { z } from "zod";
|
|
@@ -18,10 +19,13 @@ const HOST_PREFIX = "__Host-";
|
|
|
18
19
|
function requestIsSecure() {
|
|
19
20
|
return getRequestProtocol() === "https";
|
|
20
21
|
}
|
|
22
|
+
function sessionCookieName(base, secure) {
|
|
23
|
+
return secure ? HOST_PREFIX + base : base;
|
|
24
|
+
}
|
|
21
25
|
function sessionCookie(base) {
|
|
22
26
|
const secure = requestIsSecure();
|
|
23
27
|
return {
|
|
24
|
-
name:
|
|
28
|
+
name: sessionCookieName(base, secure),
|
|
25
29
|
secure
|
|
26
30
|
};
|
|
27
31
|
}
|
|
@@ -42,9 +46,8 @@ function readSessionData(sessionToken) {
|
|
|
42
46
|
permissions: payload.permissions
|
|
43
47
|
};
|
|
44
48
|
}
|
|
45
|
-
function makeSessionTokenCookie(token) {
|
|
46
|
-
|
|
47
|
-
return serializeCookie(name, sealEnvelope(token, SESSION_TOKEN), {
|
|
49
|
+
function makeSessionTokenCookie(token, secure = requestIsSecure()) {
|
|
50
|
+
return serializeCookie(sessionCookieName(SESSION_TOKEN, secure), sealEnvelope(token, SESSION_TOKEN), {
|
|
48
51
|
maxAge: sessionConfig().sessionMaxAge,
|
|
49
52
|
secure
|
|
50
53
|
});
|
|
@@ -68,13 +71,21 @@ function makeSessionDataCookie(session, sessionToken) {
|
|
|
68
71
|
function clearSession() {
|
|
69
72
|
clearCookies([SESSION_TOKEN, SESSION_DATA]);
|
|
70
73
|
}
|
|
74
|
+
/** The clearing pair, for the same out-of-context caller as `makeSessionTokenCookie`. */
|
|
75
|
+
function makeSessionClearingCookies(secure) {
|
|
76
|
+
return serializeClearingCookies([SESSION_TOKEN, SESSION_DATA], secure);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The name `readSessionToken` would look the token up under on a request of this
|
|
80
|
+
* scheme, for a caller that only needs to know whether one is present and cannot
|
|
81
|
+
* decode it. Same `secure` argument as `makeSessionTokenCookie`, so a caller that
|
|
82
|
+
* writes and a caller that probes cannot disagree about the name.
|
|
83
|
+
*/
|
|
84
|
+
function sessionTokenCookieName(secure) {
|
|
85
|
+
return sessionCookieName(SESSION_TOKEN, secure);
|
|
86
|
+
}
|
|
71
87
|
const sessionDataSchema = z.object({
|
|
72
|
-
user:
|
|
73
|
-
id: z.string(),
|
|
74
|
-
email: z.string(),
|
|
75
|
-
name: z.string(),
|
|
76
|
-
userGroupIds: z.array(z.string())
|
|
77
|
-
}).nullable(),
|
|
88
|
+
user: userSchema.nullable(),
|
|
78
89
|
permissions: permissionsSchema,
|
|
79
90
|
exp: z.number(),
|
|
80
91
|
tb: z.string()
|
|
@@ -84,11 +95,13 @@ const sessionDataSchema = z.object({
|
|
|
84
95
|
* schemes, and the one left behind would revive if the scheme changed back.
|
|
85
96
|
*/
|
|
86
97
|
function clearCookies(bases) {
|
|
87
|
-
|
|
88
|
-
|
|
98
|
+
writeCookies(serializeClearingCookies(bases, requestIsSecure()));
|
|
99
|
+
}
|
|
100
|
+
function serializeClearingCookies(bases, secure) {
|
|
101
|
+
return bases.flatMap((base) => secure ? [sessionCookieName(base, secure), base] : [base]).map((name) => serializeCookie(name, "", {
|
|
89
102
|
maxAge: 0,
|
|
90
103
|
secure
|
|
91
|
-
}))
|
|
104
|
+
}));
|
|
92
105
|
}
|
|
93
106
|
/**
|
|
94
107
|
* Over the plaintext token, never the cookie value: that is resealed with a
|
|
@@ -164,4 +177,4 @@ function openEnvelope(raw, purpose) {
|
|
|
164
177
|
}
|
|
165
178
|
|
|
166
179
|
//#endregion
|
|
167
|
-
export { clearSession, makeSessionDataCookie, makeSessionTokenCookie, readSessionData, readSessionToken };
|
|
180
|
+
export { clearSession, makeSessionClearingCookies, makeSessionDataCookie, makeSessionTokenCookie, readSessionData, readSessionToken, sessionTokenCookieName };
|
package/dist/auth/user.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
1
3
|
//#region src/auth/user.d.ts
|
|
4
|
+
|
|
2
5
|
/**
|
|
3
6
|
* Declare this interface in your app to narrow every group id the SDK takes and
|
|
4
7
|
* returns. Without a declaration, a group id is a plain `string`.
|
|
@@ -21,7 +24,7 @@ type SoftrUserGroupId = SoftrUserGroups extends {
|
|
|
21
24
|
type User = {
|
|
22
25
|
id: string;
|
|
23
26
|
email: string;
|
|
24
|
-
|
|
27
|
+
fullName: string;
|
|
25
28
|
userGroupIds: Array<SoftrUserGroupId>;
|
|
26
29
|
};
|
|
27
30
|
//#endregion
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { isNotFound, isRedirect } from "@tanstack/react-router";
|
|
2
|
+
import { createMiddleware } from "@tanstack/react-start";
|
|
3
|
+
|
|
4
|
+
//#region src/boot-error.ts
|
|
5
|
+
/**
|
|
6
|
+
* Catches errors thrown before the router renders, in request middleware or SSR
|
|
7
|
+
* setup, where a route's `errorComponent` cannot reach. Without it the browser
|
|
8
|
+
* gets a bare JSON 500 and the real message only reaches the terminal.
|
|
9
|
+
*
|
|
10
|
+
* Compose it as a dependency of the outermost request middleware, so that it
|
|
11
|
+
* wraps everything registered after it.
|
|
12
|
+
*/
|
|
13
|
+
function bootErrorMiddleware() {
|
|
14
|
+
return createMiddleware({ type: "request" }).server(async ({ next, handlerType }) => {
|
|
15
|
+
try {
|
|
16
|
+
return await next();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (handlerType === "serverFn" || error instanceof Response || isRedirect(error) || isNotFound(error)) throw error;
|
|
19
|
+
return new Response(errorPage(error), {
|
|
20
|
+
status: 500,
|
|
21
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function errorPage(error) {
|
|
27
|
+
if (process.env.NODE_ENV === "production") return page("Something went wrong", "This page couldn’t be shown. Try again in a moment, and if it keeps happening, reach out to the owner of this app.", { note: "If you are the owner, you can check what happened in Studio." });
|
|
28
|
+
const cause = error instanceof Error ? error.cause : void 0;
|
|
29
|
+
const root = cause instanceof Error ? cause : error;
|
|
30
|
+
const message = root instanceof Error ? root.message : String(root);
|
|
31
|
+
const stack = root instanceof Error ? root.stack : void 0;
|
|
32
|
+
return page("This app crashed", "The app stopped before it could start. The technical details below show where it failed and can be copied to the agent to help fix the issue.", {
|
|
33
|
+
note: "These details are only shown because the app is running in development mode.",
|
|
34
|
+
detail: `<pre class="message">${escapeHtml(message)}</pre>`,
|
|
35
|
+
actions: stack ? `<button type="button" id="toggle" aria-expanded="true" aria-controls="panel">
|
|
36
|
+
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
|
|
37
|
+
Technical details
|
|
38
|
+
</button>` : "",
|
|
39
|
+
panel: stack ? `<div class="stack" id="panel">
|
|
40
|
+
<pre id="stack">${escapeHtml(stack)}</pre>
|
|
41
|
+
<button type="button" id="copy" aria-label="Copy stack trace">
|
|
42
|
+
<svg class="copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
|
43
|
+
<svg class="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
<script>
|
|
47
|
+
const toggle = document.getElementById('toggle')
|
|
48
|
+
const panel = document.getElementById('panel')
|
|
49
|
+
const stack = document.getElementById('stack')
|
|
50
|
+
|
|
51
|
+
const setOpen = (open) => {
|
|
52
|
+
toggle.setAttribute('aria-expanded', String(open))
|
|
53
|
+
panel.hidden = !open
|
|
54
|
+
}
|
|
55
|
+
setOpen(false)
|
|
56
|
+
toggle.addEventListener('click', () => {
|
|
57
|
+
setOpen(toggle.getAttribute('aria-expanded') === 'false')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
document.getElementById('copy').addEventListener('click', async (event) => {
|
|
61
|
+
const button = event.currentTarget
|
|
62
|
+
try {
|
|
63
|
+
await navigator.clipboard.writeText(stack.textContent)
|
|
64
|
+
button.dataset.copied = ''
|
|
65
|
+
setTimeout(() => delete button.dataset.copied, 1500)
|
|
66
|
+
} catch {
|
|
67
|
+
// Denied clipboard permission. Select it so Ctrl/Cmd+C works.
|
|
68
|
+
getSelection().selectAllChildren(stack)
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
<\/script>` : ""
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function page(title, description, { note = "", detail = "", actions = "", panel = "" } = {}) {
|
|
75
|
+
return `<!doctype html>
|
|
76
|
+
<html lang="en">
|
|
77
|
+
<head>
|
|
78
|
+
<meta charset="utf-8" />
|
|
79
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
80
|
+
<title>${escapeHtml(title)}</title>
|
|
81
|
+
<style>
|
|
82
|
+
/* Mirrors the shadcn tokens the templates ship. Theme follows the OS
|
|
83
|
+
here, because a stored choice needs the app to be running. */
|
|
84
|
+
:root {
|
|
85
|
+
color-scheme: light dark;
|
|
86
|
+
--background: oklch(1 0 0);
|
|
87
|
+
--foreground: oklch(0.145 0 0);
|
|
88
|
+
--card: oklch(1 0 0);
|
|
89
|
+
--muted: oklch(0.97 0 0);
|
|
90
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
91
|
+
--border: oklch(0.922 0 0);
|
|
92
|
+
--input: oklch(0.922 0 0);
|
|
93
|
+
--input-fill: oklch(1 0 0);
|
|
94
|
+
--input-fill-hover: oklch(0.97 0 0);
|
|
95
|
+
--primary: oklch(0.205 0 0);
|
|
96
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
97
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
98
|
+
--radius: 0.625rem;
|
|
99
|
+
}
|
|
100
|
+
@media (prefers-color-scheme: dark) {
|
|
101
|
+
:root {
|
|
102
|
+
--background: oklch(0.145 0 0);
|
|
103
|
+
--foreground: oklch(0.985 0 0);
|
|
104
|
+
--card: oklch(0.205 0 0);
|
|
105
|
+
--muted: oklch(0.269 0 0);
|
|
106
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
107
|
+
--border: oklch(1 0 0 / 10%);
|
|
108
|
+
--input: oklch(1 0 0 / 15%);
|
|
109
|
+
--input-fill: oklch(1 0 0 / 4.5%);
|
|
110
|
+
--input-fill-hover: oklch(1 0 0 / 7.5%);
|
|
111
|
+
--primary: oklch(0.922 0 0);
|
|
112
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
113
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
* { box-sizing: border-box }
|
|
117
|
+
body {
|
|
118
|
+
margin: 0;
|
|
119
|
+
min-height: 100dvh;
|
|
120
|
+
display: grid;
|
|
121
|
+
place-items: center;
|
|
122
|
+
/* Without an explicit minmax the column sizes to main's max-width and
|
|
123
|
+
overflows narrow viewports. */
|
|
124
|
+
grid-template-columns: minmax(0, 1fr);
|
|
125
|
+
padding: 1.5rem;
|
|
126
|
+
background: var(--background);
|
|
127
|
+
color: var(--foreground);
|
|
128
|
+
font: 0.875rem/1.5 Inter, ui-sans-serif, system-ui, sans-serif;
|
|
129
|
+
overflow-wrap: anywhere;
|
|
130
|
+
}
|
|
131
|
+
main {
|
|
132
|
+
width: 100%;
|
|
133
|
+
max-width: 36rem;
|
|
134
|
+
display: flex;
|
|
135
|
+
flex-direction: column;
|
|
136
|
+
gap: 1.25rem;
|
|
137
|
+
padding: 1.5rem;
|
|
138
|
+
border-radius: calc(var(--radius) + 4px);
|
|
139
|
+
background: var(--card);
|
|
140
|
+
box-shadow: 0 0 0 1px color-mix(in oklch, var(--foreground) 10%, transparent);
|
|
141
|
+
}
|
|
142
|
+
.alert {
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
justify-content: center;
|
|
146
|
+
width: 2.5rem;
|
|
147
|
+
height: 2.5rem;
|
|
148
|
+
border-radius: var(--radius);
|
|
149
|
+
background: color-mix(in oklch, var(--destructive) 12%, transparent);
|
|
150
|
+
color: var(--destructive);
|
|
151
|
+
}
|
|
152
|
+
.alert svg { width: 1.25rem; height: 1.25rem }
|
|
153
|
+
h1 { margin: 0; font-size: 1rem; font-weight: 500; line-height: 1.375 }
|
|
154
|
+
p { margin: 0.375rem 0 0; color: var(--muted-foreground) }
|
|
155
|
+
p.note { margin-top: 0.75rem; opacity: 0.65 }
|
|
156
|
+
pre {
|
|
157
|
+
margin: 0;
|
|
158
|
+
padding: 0.75rem;
|
|
159
|
+
border-radius: var(--radius);
|
|
160
|
+
background: var(--muted);
|
|
161
|
+
font: 0.75rem/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
162
|
+
max-height: 16rem;
|
|
163
|
+
overflow: auto;
|
|
164
|
+
}
|
|
165
|
+
pre.message { font-size: 0.8125rem; white-space: pre-wrap }
|
|
166
|
+
.actions { display: flex; flex-wrap: wrap; gap: 0.5rem }
|
|
167
|
+
.actions button {
|
|
168
|
+
display: inline-flex;
|
|
169
|
+
align-items: center;
|
|
170
|
+
gap: 0.375rem;
|
|
171
|
+
height: 2rem;
|
|
172
|
+
padding: 0 0.75rem;
|
|
173
|
+
border-radius: var(--radius);
|
|
174
|
+
font: inherit;
|
|
175
|
+
font-weight: 500;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
user-select: none;
|
|
178
|
+
}
|
|
179
|
+
.actions button svg { width: 0.875rem; height: 0.875rem }
|
|
180
|
+
#toggle {
|
|
181
|
+
padding-left: 0.5rem;
|
|
182
|
+
border: 1px solid var(--input);
|
|
183
|
+
background: var(--input-fill);
|
|
184
|
+
color: inherit;
|
|
185
|
+
}
|
|
186
|
+
#toggle:hover { background: var(--input-fill-hover) }
|
|
187
|
+
.chevron { transition: transform 150ms }
|
|
188
|
+
#toggle[aria-expanded='true'] .chevron { transform: rotate(90deg) }
|
|
189
|
+
.stack { position: relative }
|
|
190
|
+
.stack pre { padding-right: 2.75rem }
|
|
191
|
+
#copy {
|
|
192
|
+
position: absolute;
|
|
193
|
+
top: 0.375rem;
|
|
194
|
+
right: 0.375rem;
|
|
195
|
+
display: inline-flex;
|
|
196
|
+
align-items: center;
|
|
197
|
+
justify-content: center;
|
|
198
|
+
width: 1.75rem;
|
|
199
|
+
height: 1.75rem;
|
|
200
|
+
padding: 0;
|
|
201
|
+
/* Opaque, so the stack trace underneath never shows through the icon. */
|
|
202
|
+
border: 1px solid var(--input);
|
|
203
|
+
border-radius: calc(var(--radius) - 2px);
|
|
204
|
+
background: var(--card);
|
|
205
|
+
color: var(--muted-foreground);
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
}
|
|
208
|
+
#copy:hover {
|
|
209
|
+
background: color-mix(in oklch, var(--card), var(--foreground) 6%);
|
|
210
|
+
color: var(--foreground);
|
|
211
|
+
}
|
|
212
|
+
#copy svg { width: 0.875rem; height: 0.875rem }
|
|
213
|
+
#copy .check, #copy[data-copied] .copy { display: none }
|
|
214
|
+
#copy[data-copied] .check { display: block }
|
|
215
|
+
.retry {
|
|
216
|
+
border: 1px solid transparent;
|
|
217
|
+
background: var(--primary);
|
|
218
|
+
color: var(--primary-foreground);
|
|
219
|
+
}
|
|
220
|
+
/* Mixed with the card rather than faded, so it stays opaque. */
|
|
221
|
+
.retry:hover { background: color-mix(in oklch, var(--primary) 80%, var(--card)) }
|
|
222
|
+
</style>
|
|
223
|
+
</head>
|
|
224
|
+
<body>
|
|
225
|
+
<main>
|
|
226
|
+
<div class="alert">
|
|
227
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
|
|
228
|
+
</div>
|
|
229
|
+
<div>
|
|
230
|
+
<h1>${escapeHtml(title)}</h1>
|
|
231
|
+
<p>${escapeHtml(description)}</p>
|
|
232
|
+
${note ? `<p class="note">${escapeHtml(note)}</p>` : ""}
|
|
233
|
+
</div>
|
|
234
|
+
${detail}
|
|
235
|
+
<div class="actions">
|
|
236
|
+
<button type="button" class="retry" onclick="location.reload()">
|
|
237
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>
|
|
238
|
+
Try again
|
|
239
|
+
</button>
|
|
240
|
+
${actions}
|
|
241
|
+
</div>
|
|
242
|
+
${panel}
|
|
243
|
+
</main>
|
|
244
|
+
</body>
|
|
245
|
+
</html>`;
|
|
246
|
+
}
|
|
247
|
+
function escapeHtml(value) {
|
|
248
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
//#endregion
|
|
252
|
+
export { bootErrorMiddleware };
|
package/dist/server.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ import { auth } from "./auth/server.js";
|
|
|
2
2
|
import { ds } from "./core/datasource.js";
|
|
3
3
|
import { workflow } from "./core/workflow.js";
|
|
4
4
|
import { integration } from "./core/integration.js";
|
|
5
|
+
import { makeSessionClearingCookies, makeSessionTokenCookie, sessionTokenCookieName } from "./auth/session.js";
|
|
5
6
|
import "@tanstack/react-start/server-only";
|
|
6
|
-
export { auth, ds, integration, workflow };
|
|
7
|
+
export { auth, ds, integration, makeSessionClearingCookies, makeSessionTokenCookie, sessionTokenCookieName, workflow };
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { makeSessionClearingCookies, makeSessionTokenCookie, sessionTokenCookieName } from "./auth/session.js";
|
|
1
2
|
import { auth } from "./auth/server.js";
|
|
2
3
|
import { ds } from "./core/datasource.js";
|
|
3
4
|
import { workflow } from "./core/workflow.js";
|
|
4
5
|
import { integration } from "./core/integration.js";
|
|
5
6
|
import "@tanstack/react-start/server-only";
|
|
6
7
|
|
|
7
|
-
export { auth, ds, integration, workflow };
|
|
8
|
+
export { auth, ds, integration, makeSessionClearingCookies, makeSessionTokenCookie, sessionTokenCookieName, workflow };
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
metadata:
|
|
11
11
|
type: core
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
13
|
+
library_version: 0.0.21
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
# Softr SDK core
|
|
@@ -116,11 +116,11 @@ type SoftrContext = {
|
|
|
116
116
|
type User = {
|
|
117
117
|
id: string
|
|
118
118
|
email: string
|
|
119
|
-
|
|
119
|
+
fullName: string
|
|
120
120
|
userGroupIds: Array<SoftrUserGroupId> // plain string unless you declare your ids
|
|
121
121
|
}
|
|
122
122
|
```
|
|
123
123
|
|
|
124
124
|
## Version
|
|
125
125
|
|
|
126
|
-
Targets `@softr/sdk` v0.0.
|
|
126
|
+
Targets `@softr/sdk` v0.0.21.
|
|
@@ -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.21
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
This skill builds on softr-sdk. Read it first for the entrypoints and the wiring that puts a visitor on route context.
|
|
@@ -46,7 +46,7 @@ import { useCurrentUser } from '@softr/sdk'
|
|
|
46
46
|
|
|
47
47
|
function Header() {
|
|
48
48
|
const user = useCurrentUser()
|
|
49
|
-
return <p>{user ? `Hello, ${user.
|
|
49
|
+
return <p>{user ? `Hello, ${user.fullName}` : 'Please sign in'}</p>
|
|
50
50
|
}
|
|
51
51
|
```
|
|
52
52
|
|
|
@@ -68,11 +68,14 @@ A `guard()` in `beforeLoad` guarantees a visitor at runtime, but it does not cha
|
|
|
68
68
|
```ts
|
|
69
69
|
import { createServerFn } from '@tanstack/react-start'
|
|
70
70
|
|
|
71
|
+
// The datasource id from the table's registration on the application — not the table id.
|
|
72
|
+
const INVOICES = 'e4b7c2a1-9d3f-4e68-b05a-7f1c6d2e8a93'
|
|
73
|
+
|
|
71
74
|
const getMyInvoices = createServerFn().handler(async ({ context }) => {
|
|
72
75
|
if (!context.user) throw new Error('Not signed in')
|
|
73
76
|
|
|
74
|
-
return await ds.list(
|
|
75
|
-
select: q.select({ owner: '
|
|
77
|
+
return await ds.list(INVOICES, {
|
|
78
|
+
select: q.select({ owner: 'Gq5wNt8rJy3Hb1', amount: 'Vk9cXm2pSz6Ld4' }),
|
|
76
79
|
where: q.text('owner').is(context.user.id),
|
|
77
80
|
})
|
|
78
81
|
})
|
|
@@ -85,13 +88,15 @@ const getMyInvoices = createServerFn().handler(async ({ context }) => {
|
|
|
85
88
|
```tsx
|
|
86
89
|
import { usePermissions } from '@softr/sdk'
|
|
87
90
|
|
|
91
|
+
const INVOICES = 'e4b7c2a1-9d3f-4e68-b05a-7f1c6d2e8a93' // the registered datasource's id
|
|
92
|
+
|
|
88
93
|
function NewInvoiceButton() {
|
|
89
|
-
const { create } = usePermissions(
|
|
94
|
+
const { create } = usePermissions(INVOICES)
|
|
90
95
|
return create ? <button>New invoice</button> : null
|
|
91
96
|
}
|
|
92
97
|
```
|
|
93
98
|
|
|
94
|
-
`usePermissions(datasourceId)` returns `{ read, create, update, delete }` for one datasource. It hides a control. It does not protect what the control does.
|
|
99
|
+
`usePermissions(datasourceId)` returns `{ read, create, update, delete }` for one datasource. The id is the opaque UUID from the table's registration on the application, the same one `ds.*` takes — not the table id, and never built from the table's name. It hides a control. It does not protect what the control does.
|
|
95
100
|
|
|
96
101
|
Pass an id the visitor's session has no entry for and it throws while rendering, so a typo takes the page down rather than returning four falses.
|
|
97
102
|
|
|
@@ -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.21
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
This skill builds on softr-sdk. Read it first for the entrypoints and the client/server boundary.
|
|
@@ -24,10 +24,16 @@ import { createServerFn } from '@tanstack/react-start'
|
|
|
24
24
|
import { z } from 'zod'
|
|
25
25
|
import { ds } from '@softr/sdk/server'
|
|
26
26
|
import { q } from '@softr/sdk'
|
|
27
|
+
|
|
28
|
+
// The datasource id Softr assigned when the table was registered on the application.
|
|
29
|
+
// Copy it from the registration (datasource.datasourceId); it is not the table id.
|
|
30
|
+
const PROJECTS = '8ffb4814-b028-4496-b679-7face2167ebc'
|
|
27
31
|
```
|
|
28
32
|
|
|
29
33
|
Pass a zod schema straight to `.validator`. Start accepts it and types `data` from it.
|
|
30
34
|
|
|
35
|
+
Every `ds` call names its datasource by that id: an opaque UUID from the table's registration on the application (Softr Studio's Data tab, or the `register_application_data_source` tool when an agent does it). It is never derived from the table's name. Field ids are opaque too (`fZk3Qm9pLw2Xa1`, not `fld_<name>`) and come from the table's schema.
|
|
36
|
+
|
|
31
37
|
## Core Patterns
|
|
32
38
|
|
|
33
39
|
### Create a record
|
|
@@ -38,9 +44,9 @@ const NewProject = z.object({ name: z.string(), status: z.string() })
|
|
|
38
44
|
const createProject = createServerFn({ method: 'POST' })
|
|
39
45
|
.validator(NewProject)
|
|
40
46
|
.handler(async ({ data }) =>
|
|
41
|
-
ds.create(
|
|
47
|
+
ds.create(PROJECTS, {
|
|
42
48
|
data,
|
|
43
|
-
select: q.select({ name: '
|
|
49
|
+
select: q.select({ name: 'fZk3Qm9pLw2Xa1', status: 'Ru7bVx2cNq0Ed9' }),
|
|
44
50
|
}),
|
|
45
51
|
)
|
|
46
52
|
```
|
|
@@ -59,10 +65,10 @@ const Rename = z.object({ id: z.string(), name: z.string() })
|
|
|
59
65
|
const renameProject = createServerFn({ method: 'POST' })
|
|
60
66
|
.validator(Rename)
|
|
61
67
|
.handler(async ({ data }) =>
|
|
62
|
-
ds.update(
|
|
68
|
+
ds.update(PROJECTS, {
|
|
63
69
|
id: data.id,
|
|
64
70
|
data: { name: data.name },
|
|
65
|
-
select: q.select({ name: '
|
|
71
|
+
select: q.select({ name: 'fZk3Qm9pLw2Xa1' }),
|
|
66
72
|
}),
|
|
67
73
|
)
|
|
68
74
|
```
|
|
@@ -78,7 +84,7 @@ const RecordId = z.object({ id: z.string() })
|
|
|
78
84
|
|
|
79
85
|
const deleteProject = createServerFn({ method: 'POST' })
|
|
80
86
|
.validator(RecordId)
|
|
81
|
-
.handler(async ({ data }) => ds.delete(
|
|
87
|
+
.handler(async ({ data }) => ds.delete(PROJECTS, data.id))
|
|
82
88
|
```
|
|
83
89
|
|
|
84
90
|
**`delete` takes the record id as a positional argument**, unlike `create` and `update`, which take an options object. It resolves to nothing.
|
|
@@ -95,6 +101,8 @@ Stamping a record with who created it, or scoping an update to the person making
|
|
|
95
101
|
|
|
96
102
|
## Failure Modes
|
|
97
103
|
|
|
104
|
+
**A datasource id built from a name.** `'ds_projects'`, the table id, or the table's name glued to a uuid are not datasource ids. The id is the opaque UUID assigned when the table was registered on the application; copy it from there. Anything else fails at request time as not found or forbidden.
|
|
105
|
+
|
|
98
106
|
**A write that sends `fields`.** The option is `data`. Only typecheck catches this, so it survives any run that does not typecheck.
|
|
99
107
|
|
|
100
108
|
**A key missing from `select`.** The key is not translated, so the field it was meant for is not the field written. Check the map covers every key rather than trusting the call to complain.
|
|
@@ -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.21
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
This skill builds on softr-sdk. Read it first for the entrypoints and the client/server boundary.
|
|
@@ -25,10 +25,16 @@ This skill builds on softr-sdk. Read it first for the entrypoints and the client
|
|
|
25
25
|
import { createServerFn } from '@tanstack/react-start'
|
|
26
26
|
import { ds } from '@softr/sdk/server'
|
|
27
27
|
import { q } from '@softr/sdk'
|
|
28
|
+
|
|
29
|
+
// The datasource id Softr assigned when the table was registered on the application.
|
|
30
|
+
// Copy it from the registration (datasource.datasourceId); it is not the table id.
|
|
31
|
+
const POSTS = '8ffb4814-b028-4496-b679-7face2167ebc'
|
|
28
32
|
```
|
|
29
33
|
|
|
30
34
|
`ds` is server-only and lives on `@softr/sdk/server`. `q` builds queries and nothing more, so it ships on the root entrypoint and can be imported anywhere.
|
|
31
35
|
|
|
36
|
+
Every `ds` call names its datasource by that id: an opaque UUID from the table's registration on the application (Softr Studio's Data tab, or the `register_application_data_source` tool when an agent does it). It is not the table id, and it is never derived from the table's name — `'ds_posts'` or a name glued to a uuid fails at request time as not found or forbidden.
|
|
37
|
+
|
|
32
38
|
A record comes back as `{ id: string; fields: Record<string, unknown> }`. When you write a TypeScript type for the fields, the shape of each value depends on the Softr field type — see [references/field-types.md](./references/field-types.md) for the full mapping.
|
|
33
39
|
|
|
34
40
|
## Typing record fields
|
|
@@ -53,8 +59,10 @@ Cast at the `ds.list` call using the `ListResult` generic so the type flows thro
|
|
|
53
59
|
```ts
|
|
54
60
|
import type { ListResult, SoftrRecord } from '@softr/sdk'
|
|
55
61
|
|
|
62
|
+
const PRODUCTS = '3c1e8a52-6f0d-4b7e-9a21-5d84c0f7b2e6' // from the table's registration
|
|
63
|
+
|
|
56
64
|
const fetchProducts = createServerFn().handler(() =>
|
|
57
|
-
ds.list(
|
|
65
|
+
ds.list(PRODUCTS, { select, limit: 100 }) as Promise<ListResult<Product>>
|
|
58
66
|
)
|
|
59
67
|
```
|
|
60
68
|
|
|
@@ -66,12 +74,12 @@ const fetchProducts = createServerFn().handler(() =>
|
|
|
66
74
|
|
|
67
75
|
```ts
|
|
68
76
|
const listPublishedPosts = createServerFn().handler(async () => {
|
|
69
|
-
return await ds.list(
|
|
77
|
+
return await ds.list(POSTS, {
|
|
70
78
|
select: q.select({
|
|
71
|
-
title: '
|
|
72
|
-
author: '
|
|
73
|
-
published: '
|
|
74
|
-
publishedAt: '
|
|
79
|
+
title: 'fZk3Qm9pLw2Xa1',
|
|
80
|
+
author: 'mB4kQ8yTn1sWc7',
|
|
81
|
+
published: 'Ru7bVx2cNq0Ed9',
|
|
82
|
+
publishedAt: 'Xp2dLh6vKz9Fa3',
|
|
75
83
|
}),
|
|
76
84
|
where: q.boolean('published').is(true),
|
|
77
85
|
orderBy: q.desc('publishedAt'),
|
|
@@ -80,20 +88,20 @@ const listPublishedPosts = createServerFn().handler(async () => {
|
|
|
80
88
|
})
|
|
81
89
|
```
|
|
82
90
|
|
|
83
|
-
Returns `{ records, total, nextOffset }`. `ds.list(
|
|
91
|
+
Returns `{ records, total, nextOffset }`. `ds.list(POSTS)` on its own returns the first 20 records with their raw field ids. `limit` defaults to 20 and is clamped to 100. A larger number is silently reduced, so more than a hundred records means paginating.
|
|
84
92
|
|
|
85
93
|
**Filtering or sorting requires a `select`, and it has to cover every field you name.** `where` and `orderBy` speak aliases, so without a map to translate through they throw at request time. That is why `published` is in the map above even though the table never displays it.
|
|
86
94
|
|
|
87
95
|
### Name the fields with `select`
|
|
88
96
|
|
|
89
|
-
Softr field ids are opaque (`
|
|
97
|
+
Softr field ids are opaque strings (`fZk3Qm9pLw2Xa1`), copied from the table's schema. `q.select` maps your own names onto them, and those names are what the rest of the call speaks:
|
|
90
98
|
|
|
91
99
|
```ts
|
|
92
|
-
const select = q.select({ title: '
|
|
100
|
+
const select = q.select({ title: 'fZk3Qm9pLw2Xa1', views: 'Ru7bVx2cNq0Ed9' })
|
|
93
101
|
|
|
94
|
-
await ds.list(
|
|
102
|
+
await ds.list(POSTS, {
|
|
95
103
|
select,
|
|
96
|
-
where: q.number('views').gt(100), // alias, not
|
|
104
|
+
where: q.number('views').gt(100), // alias, not Ru7bVx2cNq0Ed9
|
|
97
105
|
orderBy: q.asc('title'), // alias here too
|
|
98
106
|
})
|
|
99
107
|
// records[0].fields is { title: ..., views: ... }
|
|
@@ -104,7 +112,7 @@ Once `select` is present it is the whole vocabulary. The returned record carries
|
|
|
104
112
|
### Get one record
|
|
105
113
|
|
|
106
114
|
```ts
|
|
107
|
-
const post = await ds.get(
|
|
115
|
+
const post = await ds.get(POSTS, { id: postId })
|
|
108
116
|
if (post === null) throw notFound()
|
|
109
117
|
```
|
|
110
118
|
|
|
@@ -130,9 +138,9 @@ Five field types, `q.text`, `q.number`, `q.boolean`, `q.date` and `q.array`, com
|
|
|
130
138
|
### Paginate
|
|
131
139
|
|
|
132
140
|
```ts
|
|
133
|
-
const first = await ds.list(
|
|
141
|
+
const first = await ds.list(POSTS, { limit: 50 })
|
|
134
142
|
if (first.nextOffset !== null) {
|
|
135
|
-
const second = await ds.list(
|
|
143
|
+
const second = await ds.list(POSTS, { limit: 50, offset: first.nextOffset })
|
|
136
144
|
}
|
|
137
145
|
```
|
|
138
146
|
|
|
@@ -141,11 +149,13 @@ if (first.nextOffset !== null) {
|
|
|
141
149
|
### Scope the read to the visitor
|
|
142
150
|
|
|
143
151
|
```ts
|
|
152
|
+
const INVOICES = 'e4b7c2a1-9d3f-4e68-b05a-7f1c6d2e8a93' // from the table's registration
|
|
153
|
+
|
|
144
154
|
const listMyInvoices = createServerFn().handler(async ({ context }) => {
|
|
145
155
|
if (!context.user) throw new Error('Not signed in')
|
|
146
156
|
|
|
147
|
-
return await ds.list(
|
|
148
|
-
select: q.select({ owner: '
|
|
157
|
+
return await ds.list(INVOICES, {
|
|
158
|
+
select: q.select({ owner: 'Gq5wNt8rJy3Hb1', amount: 'Vk9cXm2pSz6Ld4' }),
|
|
149
159
|
where: q.text('owner').is(context.user.id),
|
|
150
160
|
})
|
|
151
161
|
})
|
|
@@ -10,7 +10,7 @@ import { q } from '@softr/sdk'
|
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
12
|
await ds.list(datasourceId, {
|
|
13
|
-
select: q.select({ alias: '
|
|
13
|
+
select: q.select({ alias: '<field id>', ... }), // optional field map
|
|
14
14
|
where: <one filter, or q.and/q.or of filters>,
|
|
15
15
|
orderBy: q.asc('alias') | [q.desc('a'), q.asc('b')],
|
|
16
16
|
limit: number, // default 20, clamped down to 100
|
|
@@ -123,10 +123,10 @@ Sorts are applied in array order.
|
|
|
123
123
|
## Field maps
|
|
124
124
|
|
|
125
125
|
```ts
|
|
126
|
-
const select = q.select({ title: '
|
|
126
|
+
const select = q.select({ title: 'fZk3Qm9pLw2Xa1', views: 'Ru7bVx2cNq0Ed9' })
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
`q.select` maps names you choose onto Softr field ids. Two rules follow from that:
|
|
129
|
+
`q.select` maps names you choose onto Softr field ids — opaque strings copied from the table's schema. Two rules follow from that:
|
|
130
130
|
|
|
131
131
|
1. The returned record carries exactly the keys in the map. A field left out is not returned, and no error is raised.
|
|
132
132
|
2. `where` and `orderBy` name aliases, and every one of them must be in the map. A name that is not throws `Could not find an alias for subject "..."` at request time, and so does a `where` or `orderBy` passed with no `select` at all.
|