@softr/sdk 0.0.68-alpha.0 → 0.0.68-props.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/platform.d.ts +0 -1
- package/dist/auth/platform.js +33 -13
- package/dist/auth/session.js +1 -1
- package/dist/auth/user.d.ts +8 -1
- package/dist/auth/user.js +2 -1
- package/dist/cli.js +26 -11
- package/dist/codegen/datasource-docs.js +93 -1
- package/dist/codegen/datasource-types.js +16 -1
- package/dist/codegen/fetch-metadata.js +44 -17
- package/dist/codegen/softr-env.js +2 -1
- package/dist/cookies.js +10 -2
- package/dist/core/integration.d.ts +0 -7
- package/dist/core/integration.js +3 -10
- package/dist/formatting.d.ts +4 -4
- package/dist/formatting.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/package.js +1 -1
- package/dist/pwa.d.ts +2 -2
- package/dist/pwa.js +1 -1
- package/dist/softr-config.schema.js +45 -24
- package/dist/visibility/route.js +1 -1
- package/dist/vite.js +1 -19
- package/package.json +33 -10
- package/schema/softr.schema.json +13 -0
- 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 +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +24 -3
- package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
- package/skills/softr-sdk/formatting/SKILL.md +1 -1
- package/skills/softr-sdk/integrations/SKILL.md +3 -40
- package/skills/softr-sdk/mcp/SKILL.md +1 -1
- package/skills/softr-sdk/pwa/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
- package/skills/softr-sdk/record-queries/SKILL.md +1 -1
- package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
package/dist/auth/platform.d.ts
CHANGED
package/dist/auth/platform.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { panic } from "../assertions.js";
|
|
1
|
+
import { assert, panic } from "../assertions.js";
|
|
2
2
|
import { fetch } from "../fetch.js";
|
|
3
3
|
import { permissionsSchema } from "./permissions.js";
|
|
4
4
|
import { sessionConfig } from "./config.js";
|
|
5
|
-
import {
|
|
5
|
+
import { softrConfig } from "../softr-config.js";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
|
|
8
8
|
//#region src/auth/platform.ts
|
|
@@ -33,10 +33,30 @@ const authSettingsSchema = z.object({
|
|
|
33
33
|
sso: z.object({ type: z.enum(["SAML", "OPEN_ID"]).nullable() }).nullable()
|
|
34
34
|
});
|
|
35
35
|
function makePlatform(applicationId) {
|
|
36
|
+
const aliases = Object.entries(softrConfig().user?.properties ?? {});
|
|
37
|
+
const withAttributes = aliases.length > 0 ? "?includeAttributes=true" : "";
|
|
38
|
+
const userSchema = z.object({
|
|
39
|
+
id: z.string(),
|
|
40
|
+
email: z.string(),
|
|
41
|
+
fullName: z.string(),
|
|
42
|
+
userGroupIds: z.array(z.string()),
|
|
43
|
+
attributes: z.record(z.string(), z.unknown()).optional()
|
|
44
|
+
}).transform(({ attributes,...identity }) => {
|
|
45
|
+
const properties = {};
|
|
46
|
+
for (const [alias, field] of aliases) {
|
|
47
|
+
assert(attributes !== void 0, "The signed-in user came back without their properties.");
|
|
48
|
+
assert(Object.hasOwn(attributes, field), `softr.json user.properties.${alias} names the field "${field}", which the users table does not have.`);
|
|
49
|
+
properties[alias] = attributes[field];
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
...identity,
|
|
53
|
+
properties
|
|
54
|
+
};
|
|
55
|
+
});
|
|
36
56
|
return {
|
|
37
57
|
authSettings: () => request(`/v1/vibe/applications/${applicationId}/users/auth-settings`, { method: "GET" }, authSettingsSchema),
|
|
38
58
|
session: async (token) => {
|
|
39
|
-
const result = await request(`/v1/vibe/applications/${applicationId}/users/session`, {
|
|
59
|
+
const result = await request(`/v1/vibe/applications/${applicationId}/users/session${withAttributes}`, {
|
|
40
60
|
method: "GET",
|
|
41
61
|
token
|
|
42
62
|
}, z.object({
|
|
@@ -46,7 +66,7 @@ function makePlatform(applicationId) {
|
|
|
46
66
|
if (result.status === 404) panic(`Application ${applicationId} not found. Check SOFTR_APPLICATION_ID.`);
|
|
47
67
|
return result;
|
|
48
68
|
},
|
|
49
|
-
tokens: (credentials) => request(`/v1/vibe/applications/${applicationId}/users/tokens`, {
|
|
69
|
+
tokens: (credentials) => request(`/v1/vibe/applications/${applicationId}/users/tokens${withAttributes}`, {
|
|
50
70
|
method: "POST",
|
|
51
71
|
json: credentials
|
|
52
72
|
}, z.object({
|
|
@@ -69,7 +89,7 @@ function makePlatform(applicationId) {
|
|
|
69
89
|
method: "POST",
|
|
70
90
|
json: { email }
|
|
71
91
|
}, z.null()),
|
|
72
|
-
signInWithCode: (credentials) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-code`, {
|
|
92
|
+
signInWithCode: (credentials) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-code${withAttributes}`, {
|
|
73
93
|
method: "POST",
|
|
74
94
|
json: credentials
|
|
75
95
|
}, z.object({
|
|
@@ -88,7 +108,7 @@ function makePlatform(applicationId) {
|
|
|
88
108
|
challengeToken: z.string()
|
|
89
109
|
}).nullish()
|
|
90
110
|
})),
|
|
91
|
-
signInWithGoogle: (body) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-google`, {
|
|
111
|
+
signInWithGoogle: (body) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-google${withAttributes}`, {
|
|
92
112
|
method: "POST",
|
|
93
113
|
json: body
|
|
94
114
|
}, z.object({
|
|
@@ -107,7 +127,7 @@ function makePlatform(applicationId) {
|
|
|
107
127
|
challengeToken: z.string()
|
|
108
128
|
}).nullish()
|
|
109
129
|
})),
|
|
110
|
-
signInWithMagicLink: (body) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-magic-link`, {
|
|
130
|
+
signInWithMagicLink: (body) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-magic-link${withAttributes}`, {
|
|
111
131
|
method: "POST",
|
|
112
132
|
json: body
|
|
113
133
|
}, z.object({
|
|
@@ -134,7 +154,7 @@ function makePlatform(applicationId) {
|
|
|
134
154
|
method: "POST",
|
|
135
155
|
json: body
|
|
136
156
|
}, z.object({ email: z.string() })),
|
|
137
|
-
setPassword: (body) => request(`/v1/vibe/applications/${applicationId}/users/set-password`, {
|
|
157
|
+
setPassword: (body) => request(`/v1/vibe/applications/${applicationId}/users/set-password${withAttributes}`, {
|
|
138
158
|
method: "POST",
|
|
139
159
|
json: body
|
|
140
160
|
}, z.object({
|
|
@@ -157,7 +177,7 @@ function makePlatform(applicationId) {
|
|
|
157
177
|
method: "POST",
|
|
158
178
|
json: body
|
|
159
179
|
}, z.null()),
|
|
160
|
-
signUpWithCode: (body) => request(`/v1/vibe/applications/${applicationId}/users/sign-up/with-code`, {
|
|
180
|
+
signUpWithCode: (body) => request(`/v1/vibe/applications/${applicationId}/users/sign-up/with-code${withAttributes}`, {
|
|
161
181
|
method: "POST",
|
|
162
182
|
json: body
|
|
163
183
|
}, z.object({
|
|
@@ -176,7 +196,7 @@ function makePlatform(applicationId) {
|
|
|
176
196
|
challengeToken: z.string()
|
|
177
197
|
}).nullish()
|
|
178
198
|
})),
|
|
179
|
-
signUpWithPassword: (body) => request(`/v1/vibe/applications/${applicationId}/users/sign-up/with-password`, {
|
|
199
|
+
signUpWithPassword: (body) => request(`/v1/vibe/applications/${applicationId}/users/sign-up/with-password${withAttributes}`, {
|
|
180
200
|
method: "POST",
|
|
181
201
|
json: body
|
|
182
202
|
}, z.object({
|
|
@@ -202,7 +222,7 @@ function makePlatform(applicationId) {
|
|
|
202
222
|
otpauthUri: z.string(),
|
|
203
223
|
qrPngBase64: z.string()
|
|
204
224
|
})),
|
|
205
|
-
authenticatorSetupComplete: (token, code) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/setup/complete`, {
|
|
225
|
+
authenticatorSetupComplete: (token, code) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/setup/complete${withAttributes}`, {
|
|
206
226
|
method: "POST",
|
|
207
227
|
token,
|
|
208
228
|
json: { code }
|
|
@@ -214,7 +234,7 @@ function makePlatform(applicationId) {
|
|
|
214
234
|
}),
|
|
215
235
|
backupCodes: z.array(z.string())
|
|
216
236
|
})),
|
|
217
|
-
authenticatorSetupDismiss: (token) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/setup/dismiss`, {
|
|
237
|
+
authenticatorSetupDismiss: (token) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/setup/dismiss${withAttributes}`, {
|
|
218
238
|
method: "POST",
|
|
219
239
|
token
|
|
220
240
|
}, z.object({
|
|
@@ -233,7 +253,7 @@ function makePlatform(applicationId) {
|
|
|
233
253
|
challengeToken: z.string()
|
|
234
254
|
}).nullish()
|
|
235
255
|
})),
|
|
236
|
-
authenticatorVerify: (token, body) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/totp/verify`, {
|
|
256
|
+
authenticatorVerify: (token, body) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/totp/verify${withAttributes}`, {
|
|
237
257
|
method: "POST",
|
|
238
258
|
token,
|
|
239
259
|
json: body
|
package/dist/auth/session.js
CHANGED
|
@@ -2,10 +2,10 @@ import { readRequestCookies, serializeCookie, writeCookies } from "../cookies.js
|
|
|
2
2
|
import { permissionsSchema } from "./permissions.js";
|
|
3
3
|
import { sessionConfig } from "./config.js";
|
|
4
4
|
import { userSchema } from "./user.js";
|
|
5
|
+
import { z } from "zod";
|
|
5
6
|
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
6
7
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
8
|
import { getRequestProtocol } from "@tanstack/react-start/server";
|
|
8
|
-
import { z } from "zod";
|
|
9
9
|
|
|
10
10
|
//#region src/auth/session.ts
|
|
11
11
|
const SESSION_TOKEN = "session_token";
|
package/dist/auth/user.d.ts
CHANGED
|
@@ -13,12 +13,19 @@ interface SoftrUserGroups {}
|
|
|
13
13
|
type SoftrUserGroupId = SoftrUserGroups extends {
|
|
14
14
|
ids: infer Id extends string;
|
|
15
15
|
} ? Id : string;
|
|
16
|
+
/**
|
|
17
|
+
* The fields of the visitor's row that softr.json declares under
|
|
18
|
+
* `user.properties`, keyed by their aliases. The app's generated types declare
|
|
19
|
+
* it.
|
|
20
|
+
*/
|
|
21
|
+
interface SoftrUserProperties {}
|
|
16
22
|
/** The signed-in visitor. */
|
|
17
23
|
type User = {
|
|
18
24
|
id: string;
|
|
19
25
|
email: string;
|
|
20
26
|
fullName: string;
|
|
21
27
|
userGroupIds: Array<SoftrUserGroupId>;
|
|
28
|
+
properties: SoftrUserProperties;
|
|
22
29
|
};
|
|
23
30
|
//#endregion
|
|
24
|
-
export { SoftrUserGroupId, SoftrUserGroups, User };
|
|
31
|
+
export { SoftrUserGroupId, SoftrUserGroups, SoftrUserProperties, User };
|
package/dist/auth/user.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import { version } from "./package.js";
|
|
3
3
|
import { requireDefined } from "./assertions.js";
|
|
4
4
|
import { fetchSoftrEnvInput, metadataSourceFromEnv } from "./codegen/fetch-metadata.js";
|
|
5
|
+
import { renderUserPropertiesDoc } from "./codegen/datasource-docs.js";
|
|
5
6
|
import { renderUserGroupsDoc } from "./codegen/user-groups.js";
|
|
6
7
|
import { renderSoftrEnv } from "./codegen/softr-env.js";
|
|
7
8
|
import { writeIfChanged } from "./codegen/write-if-changed.js";
|
|
8
9
|
import { writeDatasourceDocs } from "./codegen/write-datasource-docs.js";
|
|
9
|
-
import {
|
|
10
|
+
import { loadSoftrConfig } from "./softr-config.schema.js";
|
|
11
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
10
12
|
import path from "node:path";
|
|
11
13
|
import { parseArgs } from "node:util";
|
|
12
14
|
|
|
@@ -14,9 +16,10 @@ import { parseArgs } from "node:util";
|
|
|
14
16
|
const OUTPUT = "softr-env.d.ts";
|
|
15
17
|
const DOCS_DIR = "docs/datasources";
|
|
16
18
|
const USER_GROUPS_DOC = "docs/user-groups.md";
|
|
19
|
+
const USER_PROPERTIES_DOC = "docs/user-properties.md";
|
|
17
20
|
async function generate({ json }) {
|
|
18
21
|
if (json) console.info = console.error;
|
|
19
|
-
const input = await fetchSoftrEnvInput(metadataSourceFromEnv());
|
|
22
|
+
const input = await fetchSoftrEnvInput(metadataSourceFromEnv(), loadSoftrConfig(process.cwd()).user?.properties ?? {});
|
|
20
23
|
writeFileSync(path.resolve(process.cwd(), OUTPUT), renderSoftrEnv(input, version), "utf8");
|
|
21
24
|
const report = writeDocs(input);
|
|
22
25
|
if (json) {
|
|
@@ -26,18 +29,30 @@ async function generate({ json }) {
|
|
|
26
29
|
printReport(report);
|
|
27
30
|
}
|
|
28
31
|
function writeDocs(input) {
|
|
32
|
+
if (input.users === null) rmSync(path.resolve(process.cwd(), USER_PROPERTIES_DOC), { force: true });
|
|
29
33
|
const datasourceDocs = writeDatasourceDocs(input.datasources, path.resolve(process.cwd(), DOCS_DIR));
|
|
30
34
|
return {
|
|
31
35
|
types: OUTPUT,
|
|
32
|
-
docs: [
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
docs: [
|
|
37
|
+
{
|
|
38
|
+
subject: "datasources",
|
|
39
|
+
path: `${DOCS_DIR}/README.md`,
|
|
40
|
+
changed: datasourceDocs.changed
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
subject: "user groups",
|
|
44
|
+
path: USER_GROUPS_DOC,
|
|
45
|
+
changed: writeIfChanged(path.resolve(process.cwd(), USER_GROUPS_DOC), renderUserGroupsDoc(input.userGroups))
|
|
46
|
+
},
|
|
47
|
+
...input.users === null ? [] : [{
|
|
48
|
+
subject: "user properties",
|
|
49
|
+
path: USER_PROPERTIES_DOC,
|
|
50
|
+
changed: writeIfChanged(path.resolve(process.cwd(), USER_PROPERTIES_DOC), renderUserPropertiesDoc(input.users, {
|
|
51
|
+
datasources: new Map(input.datasources.map((d) => [d.id, d])),
|
|
52
|
+
fileNames: datasourceDocs.fileNames
|
|
53
|
+
}))
|
|
54
|
+
}]
|
|
55
|
+
],
|
|
41
56
|
datasources: input.datasources.map(({ id, name }) => ({
|
|
42
57
|
id,
|
|
43
58
|
name,
|
|
@@ -18,6 +18,98 @@ function renderDatasourceDoc(datasource, context) {
|
|
|
18
18
|
...renderExamples(datasource, context.datasources)
|
|
19
19
|
].join("\n");
|
|
20
20
|
}
|
|
21
|
+
function renderUserPropertiesDoc(users, context) {
|
|
22
|
+
const aliases = Object.entries(users.properties);
|
|
23
|
+
assert(aliases.length > 0, "Expected user properties to document, but got none");
|
|
24
|
+
const [first] = requireDefined(aliases[0]);
|
|
25
|
+
const registered = linkedDatasource(users.tableId, context.datasources);
|
|
26
|
+
const types = /* @__PURE__ */ new Set();
|
|
27
|
+
const sampled = aliases.filter(([, field]) => {
|
|
28
|
+
const type = fieldTypeLiteral(field);
|
|
29
|
+
if (types.has(type)) return false;
|
|
30
|
+
types.add(type);
|
|
31
|
+
return true;
|
|
32
|
+
});
|
|
33
|
+
return [
|
|
34
|
+
"# User properties",
|
|
35
|
+
"",
|
|
36
|
+
`Users datasource: ${displayName(users.name) || users.tableId} `,
|
|
37
|
+
`Datasource ID: ${users.datasourceId}` + (registered === void 0 ? "" : ` \nRegistered as: [${datasourceLabel(registered)}](./datasources/${requireDefined(context.fileNames.get(registered.id))}) (${registered.id})`),
|
|
38
|
+
"",
|
|
39
|
+
"> This document is generated automatically when softr.json's `user.properties` or the users table changes.",
|
|
40
|
+
"",
|
|
41
|
+
"Each alias below is declared in softr.json under `user.properties`, and read as `user.properties.<alias>`.",
|
|
42
|
+
"",
|
|
43
|
+
"## Field reference",
|
|
44
|
+
"",
|
|
45
|
+
...aliases.flatMap(([alias, field]) => {
|
|
46
|
+
const linkedId = option(field.options, "linkedTableId");
|
|
47
|
+
const linked = typeof linkedId === "string" ? linkedDatasource(linkedId, context.datasources) : void 0;
|
|
48
|
+
return [
|
|
49
|
+
`### ${alias}`,
|
|
50
|
+
"",
|
|
51
|
+
`- Field: ${displayName(field.name)}`,
|
|
52
|
+
`- ID: ${field.id}`,
|
|
53
|
+
`- Type: ${fieldTypeLiteral(field).slice(1, -1)}`,
|
|
54
|
+
...optionLines(field),
|
|
55
|
+
...choiceLines(field),
|
|
56
|
+
...typeof linkedId !== "string" ? [] : linked === void 0 ? [`- Links to: ${linkedId}, not registered`] : [`- Links to: [${datasourceLabel(linked)}](./datasources/${requireDefined(context.fileNames.get(linked.id))}) (${linked.id})`],
|
|
57
|
+
""
|
|
58
|
+
];
|
|
59
|
+
}),
|
|
60
|
+
"## Read shape",
|
|
61
|
+
"",
|
|
62
|
+
"In a component:",
|
|
63
|
+
"",
|
|
64
|
+
"```tsx",
|
|
65
|
+
"import { useCurrentUser } from '@softr/sdk'",
|
|
66
|
+
"",
|
|
67
|
+
"const user = useCurrentUser()",
|
|
68
|
+
"if (user === null) return null",
|
|
69
|
+
"",
|
|
70
|
+
...sampled.map(([alias, field]) => `user.properties.${alias} // ${fieldTypeLiteral(field).slice(1, -1)}`),
|
|
71
|
+
"```",
|
|
72
|
+
"",
|
|
73
|
+
"In a route's `beforeLoad`, to send a visitor elsewhere. It guards navigation only, not data:",
|
|
74
|
+
"",
|
|
75
|
+
"```tsx",
|
|
76
|
+
"import { createFileRoute, redirect } from '@tanstack/react-router'",
|
|
77
|
+
"import { assertRouteAccess, defineVisibility } from '@softr/sdk'",
|
|
78
|
+
"",
|
|
79
|
+
"const visibility = defineVisibility({ userGroupTypes: ['LOGGED_IN_USERS'] })",
|
|
80
|
+
"",
|
|
81
|
+
"export const Route = createFileRoute('/example')({",
|
|
82
|
+
" beforeLoad: (opts) => {",
|
|
83
|
+
" assertRouteAccess(visibility, opts)",
|
|
84
|
+
` if (opts.context.user.properties.${first} == null) throw redirect({ to: '/' })`,
|
|
85
|
+
" },",
|
|
86
|
+
"})",
|
|
87
|
+
"```",
|
|
88
|
+
"",
|
|
89
|
+
"In a server function, called from a route's `loader`. The handler checks the visitor itself:",
|
|
90
|
+
"",
|
|
91
|
+
"```tsx",
|
|
92
|
+
"import { createFileRoute } from '@tanstack/react-router'",
|
|
93
|
+
"import { createServerFn } from '@tanstack/react-start'",
|
|
94
|
+
"",
|
|
95
|
+
`const get${pascalCase(first)} = createServerFn().handler(async ({ context }) => {`,
|
|
96
|
+
" if (!context.user) throw new Error('Not signed in')",
|
|
97
|
+
` return context.user.properties.${first}`,
|
|
98
|
+
"})",
|
|
99
|
+
"",
|
|
100
|
+
"export const Route = createFileRoute('/example')({",
|
|
101
|
+
` loader: () => get${pascalCase(first)}(),`,
|
|
102
|
+
" component: Example,",
|
|
103
|
+
"})",
|
|
104
|
+
"",
|
|
105
|
+
"function Example() {",
|
|
106
|
+
` const ${first} = Route.useLoaderData()`,
|
|
107
|
+
" // ...",
|
|
108
|
+
"}",
|
|
109
|
+
"```",
|
|
110
|
+
""
|
|
111
|
+
].join("\n");
|
|
112
|
+
}
|
|
21
113
|
/** Two trailing spaces are a markdown hard break, which keeps the block one paragraph. */
|
|
22
114
|
function summary(datasource, context) {
|
|
23
115
|
const lines = [`Datasource ID: ${datasource.id}`];
|
|
@@ -315,4 +407,4 @@ function words(value) {
|
|
|
315
407
|
}
|
|
316
408
|
|
|
317
409
|
//#endregion
|
|
318
|
-
export { renderDatasourceDoc, renderDatasourceIndex };
|
|
410
|
+
export { renderDatasourceDoc, renderDatasourceIndex, renderUserPropertiesDoc };
|
|
@@ -251,6 +251,21 @@ function linkedRecord(options, context) {
|
|
|
251
251
|
if (linked === void 0 || linked.primaryFieldId === "") return "LinkedRecord";
|
|
252
252
|
return `LinkedRecord<NonNullable<${`${nameOf(linked.id, context)}Read[${quote(linked.primaryFieldId)}]`}>>`;
|
|
253
253
|
}
|
|
254
|
+
/** `SoftrUserProperties`, each alias typed as its field reads. */
|
|
255
|
+
function renderUserProperties(properties, datasources) {
|
|
256
|
+
const context = {
|
|
257
|
+
datasources: byId(datasources),
|
|
258
|
+
names: typeNames(datasources)
|
|
259
|
+
};
|
|
260
|
+
return [
|
|
261
|
+
"interface SoftrUserProperties {",
|
|
262
|
+
...Object.entries(properties).map(([alias, field]) => indent(readMember({
|
|
263
|
+
...field,
|
|
264
|
+
id: alias
|
|
265
|
+
}, context))),
|
|
266
|
+
"}"
|
|
267
|
+
].join("\n");
|
|
268
|
+
}
|
|
254
269
|
function renderRegistry(datasources, context) {
|
|
255
270
|
return [
|
|
256
271
|
"interface SoftrDatasources {",
|
|
@@ -494,4 +509,4 @@ function indent(block) {
|
|
|
494
509
|
}
|
|
495
510
|
|
|
496
511
|
//#endregion
|
|
497
|
-
export { choiceEntries, fieldTypeLiteral, isWritable, linkedDatasource, option, readExample, renderDatasourceTypes, resolvedShape, scalarExample, underlyingType };
|
|
512
|
+
export { choiceEntries, fieldTypeLiteral, isWritable, linkedDatasource, option, readExample, renderDatasourceTypes, renderUserProperties, resolvedShape, scalarExample, underlyingType };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { panic } from "../assertions.js";
|
|
1
|
+
import { assert, panic } from "../assertions.js";
|
|
2
2
|
import { fetch } from "../fetch.js";
|
|
3
3
|
|
|
4
4
|
//#region src/codegen/fetch-metadata.ts
|
|
@@ -17,27 +17,54 @@ function metadataSourceFromEnv(env = process.env) {
|
|
|
17
17
|
token: env.SOFTR_STUDIO_TOKEN
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
-
async function fetchSoftrEnvInput(source) {
|
|
20
|
+
async function fetchSoftrEnvInput(source, aliases) {
|
|
21
21
|
const { studioApiUrl, datasourceApiUrl, applicationId, token } = source;
|
|
22
22
|
const [registered, userGroups] = await Promise.all([apiFetch(`${studioApiUrl}/v1/applications/${applicationId}/datasources`, token), apiFetch(`${studioApiUrl}/v1/applications/${applicationId}/access-control/user-groups`, token)]);
|
|
23
|
+
const datasources = await Promise.all(registered.map(async ({ name, datasource }) => {
|
|
24
|
+
const metadata = await apiFetch(`${datasourceApiUrl}/v1/datasource/vibe/studio/applications/${applicationId}/datasources/${datasource.id}/metadata`, token);
|
|
25
|
+
return {
|
|
26
|
+
...metadata,
|
|
27
|
+
id: datasource.id,
|
|
28
|
+
tableId: metadata.id,
|
|
29
|
+
name
|
|
30
|
+
};
|
|
31
|
+
}));
|
|
32
|
+
let users = null;
|
|
33
|
+
if (Object.keys(aliases).length > 0) {
|
|
34
|
+
const { payload } = await apiFetch(`${studioApiUrl}/v1/applications/${applicationId}`, token);
|
|
35
|
+
const connection = payload.user_connection?.datasource ?? panic("softr.json declares user.properties, but this app has no users table.");
|
|
36
|
+
const metadata = await apiFetch(`${datasourceApiUrl}/v1/datasource/${applicationId}/${connection.integration_id}/metadata`, token, {
|
|
37
|
+
datasourceType: connection.datasource_integration_type,
|
|
38
|
+
resourceIdentifier: { keys: connection.resource_identifier.keys }
|
|
39
|
+
});
|
|
40
|
+
users = {
|
|
41
|
+
datasourceId: connection.id,
|
|
42
|
+
tableId: metadata.id,
|
|
43
|
+
name: metadata.name,
|
|
44
|
+
properties: {}
|
|
45
|
+
};
|
|
46
|
+
for (const [alias, fieldId] of Object.entries(aliases)) {
|
|
47
|
+
const field = metadata.fields.find(({ id }) => id === fieldId) ?? panic(`softr.json user.properties.${alias} names "${fieldId}", which is not a field of the users table.`);
|
|
48
|
+
assert(field.type !== "ATTACHMENT" && field.type !== "LONG_TEXT" && !(field.allowMultipleEntries && (field.type === "LINKED_RECORD" || field.type === "LOOKUP")), `softr.json user.properties.${alias} names "${field.name}", a ${field.allowMultipleEntries ? "multiple " : ""}${field.type} field, which can be too big to read with the signed-in user. Remove it, and read it with defineQuery on the users table instead.`);
|
|
49
|
+
users.properties[alias] = field;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
23
52
|
return {
|
|
24
|
-
datasources
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
...metadata,
|
|
28
|
-
id: datasource.id,
|
|
29
|
-
tableId: metadata.id,
|
|
30
|
-
name
|
|
31
|
-
};
|
|
32
|
-
})),
|
|
33
|
-
userGroups
|
|
53
|
+
datasources,
|
|
54
|
+
userGroups,
|
|
55
|
+
users
|
|
34
56
|
};
|
|
35
57
|
}
|
|
36
|
-
async function apiFetch(url, token) {
|
|
37
|
-
const response = await fetch(url, {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
58
|
+
async function apiFetch(url, token, body) {
|
|
59
|
+
const response = await fetch(url, {
|
|
60
|
+
method: body === void 0 ? "GET" : "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
accept: "application/json",
|
|
63
|
+
...body === void 0 ? {} : { "content-type": "application/json" },
|
|
64
|
+
Authorization: `Bearer ${token}`
|
|
65
|
+
},
|
|
66
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
67
|
+
}).catch(() => panic(`Could not reach ${url}`));
|
|
41
68
|
if (!response.ok) panic(`${url} answered ${response.status}`);
|
|
42
69
|
return await response.json();
|
|
43
70
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderDatasourceTypes } from "./datasource-types.js";
|
|
1
|
+
import { renderDatasourceTypes, renderUserProperties } from "./datasource-types.js";
|
|
2
2
|
import { renderUserGroups } from "./user-groups.js";
|
|
3
3
|
|
|
4
4
|
//#region src/codegen/softr-env.ts
|
|
@@ -15,6 +15,7 @@ function renderSoftrEnv(input, sdkVersion) {
|
|
|
15
15
|
indent(interfaces),
|
|
16
16
|
"",
|
|
17
17
|
indent(renderUserGroups(input.userGroups)),
|
|
18
|
+
...input.users === null ? [] : ["", indent(renderUserProperties(input.users.properties, input.datasources))],
|
|
18
19
|
"}",
|
|
19
20
|
""
|
|
20
21
|
].join("\n");
|
package/dist/cookies.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { softAssert } from "./assertions.js";
|
|
1
2
|
import { getRequestHeader, getResponseHeaders, setResponseHeader } from "@tanstack/react-start/server";
|
|
2
3
|
|
|
3
4
|
//#region src/cookies.ts
|
|
@@ -38,9 +39,16 @@ const USER_TIMEZONE_COOKIE = "user-timezone";
|
|
|
38
39
|
function readUserTimezone() {
|
|
39
40
|
return readRequestCookies()[USER_TIMEZONE_COOKIE] ?? null;
|
|
40
41
|
}
|
|
42
|
+
/** What browsers keep of one cookie's name and value. They drop a larger one silently. */
|
|
43
|
+
const COOKIE_MAX_BYTES = 4096;
|
|
41
44
|
function writeCookies(cookies) {
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
const written = cookies.filter((cookie) => {
|
|
46
|
+
const size = cookie.split(";")[0].length;
|
|
47
|
+
softAssert(size <= COOKIE_MAX_BYTES, () => `The ${cookie.slice(0, cookie.indexOf("="))} cookie is ${size} bytes, over the ${COOKIE_MAX_BYTES} a browser keeps. It was not written.`);
|
|
48
|
+
return size <= COOKIE_MAX_BYTES;
|
|
49
|
+
});
|
|
50
|
+
if (written.length === 0) return;
|
|
51
|
+
setResponseHeader("Set-Cookie", [...getResponseHeaders().getSetCookie(), ...written]);
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
//#endregion
|
|
@@ -7,13 +7,6 @@ interface IntegrationFetchInit {
|
|
|
7
7
|
headers?: Record<string, string>;
|
|
8
8
|
body?: string;
|
|
9
9
|
signal?: AbortSignal;
|
|
10
|
-
/**
|
|
11
|
-
* Relay the vendor's answer as it is produced instead of waiting for all of it — for any endpoint
|
|
12
|
-
* that streams, such as an LLM completion requested with `"stream": true`. The returned `Response`
|
|
13
|
-
* body is then the vendor's own stream, readable chunk by chunk. A vendor that answers in one piece
|
|
14
|
-
* still works.
|
|
15
|
-
*/
|
|
16
|
-
stream?: boolean;
|
|
17
10
|
}
|
|
18
11
|
declare function integrationCreateFetcher(action: IntegrationAction): (init?: IntegrationFetchInit) => Promise<Response>;
|
|
19
12
|
declare const integration: {
|
package/dist/core/integration.js
CHANGED
|
@@ -3,16 +3,15 @@ import { readSessionToken } from "../auth/session.js";
|
|
|
3
3
|
import { dsUrl, refererHeader } from "../datasource/record.js";
|
|
4
4
|
|
|
5
5
|
//#region src/core/integration.ts
|
|
6
|
-
|
|
7
|
-
async function integrationFetch(action, { params, query, headers = {}, body, signal, stream = false } = {}) {
|
|
6
|
+
async function integrationFetch(action, { params, query, headers = {}, body, signal } = {}) {
|
|
8
7
|
if (signal?.aborted) throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
9
8
|
const payload = {
|
|
10
9
|
method: action.resource.method,
|
|
11
10
|
url: buildUrl(action.resource.url, params, query),
|
|
12
|
-
headers
|
|
11
|
+
headers
|
|
13
12
|
};
|
|
14
13
|
if (body !== void 0) payload.body = body;
|
|
15
|
-
return fetch(`${dsUrl(action.id)}
|
|
14
|
+
return fetch(`${dsUrl(action.id)}/proxy`, {
|
|
16
15
|
method: "POST",
|
|
17
16
|
headers: {
|
|
18
17
|
"Content-Type": "application/json",
|
|
@@ -31,12 +30,6 @@ function buildUrl(template, params, query) {
|
|
|
31
30
|
if (query !== void 0 && Object.keys(query).length > 0) url += `?${new URLSearchParams(query).toString()}`;
|
|
32
31
|
return url;
|
|
33
32
|
}
|
|
34
|
-
function withAcceptEventStream(headers) {
|
|
35
|
-
return Object.keys(headers).some((name) => name.toLowerCase() === "accept") ? headers : {
|
|
36
|
-
...headers,
|
|
37
|
-
Accept: EVENT_STREAM
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
33
|
function integrationCreateFetcher(action) {
|
|
41
34
|
return (init) => integrationFetch(action, init);
|
|
42
35
|
}
|
package/dist/formatting.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as react0 from "react";
|
|
2
|
-
import { ReactNode } from "react";
|
|
3
1
|
import { z } from "zod";
|
|
2
|
+
import * as react2 from "react";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
4
|
|
|
5
5
|
//#region src/formatting.d.ts
|
|
6
6
|
type FixedDateFormat = "FRIENDLY" | "DD-MM-YYYY" | "MM-DD-YYYY" | "YYYY-MM-DD";
|
|
@@ -94,7 +94,7 @@ declare function FormattingScope({
|
|
|
94
94
|
/** A BCP 47 locale, such as `de-DE`. It drops the app's fixed date format inside the scope. */
|
|
95
95
|
locale?: string;
|
|
96
96
|
children?: ReactNode;
|
|
97
|
-
}):
|
|
97
|
+
}): react2.FunctionComponentElement<react2.ProviderProps<Scope>>;
|
|
98
98
|
/**
|
|
99
99
|
* A date in the app's format, safe to render on the server. A timestamp whose
|
|
100
100
|
* zone is not known yet renders `fallback` until the browser supplies it, so
|
|
@@ -115,6 +115,6 @@ declare function FormattedDate({
|
|
|
115
115
|
time?: boolean;
|
|
116
116
|
/** What renders while the visitor's zone is unknown. */
|
|
117
117
|
fallback?: ReactNode;
|
|
118
|
-
}):
|
|
118
|
+
}): react2.FunctionComponentElement<react2.FragmentProps>;
|
|
119
119
|
//#endregion
|
|
120
120
|
export { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting };
|
package/dist/formatting.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { logger } from "./logger.js";
|
|
2
|
-
import { Fragment, createContext, createElement, useContext, useMemo, useSyncExternalStore } from "react";
|
|
3
2
|
import { z } from "zod";
|
|
3
|
+
import { Fragment, createContext, createElement, useContext, useMemo, useSyncExternalStore } from "react";
|
|
4
4
|
import { rootRouteId, useRouteContext } from "@tanstack/react-router";
|
|
5
5
|
|
|
6
6
|
//#region src/formatting.ts
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
|
|
1
|
+
import { SoftrUserGroupId, SoftrUserGroups, SoftrUserProperties, 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
4
|
import { FileItem, useFileUpload } from "./core/use-file-upload.js";
|
|
@@ -14,4 +14,4 @@ import { useUserTimezone } from "./timezone.js";
|
|
|
14
14
|
import { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall } from "./pwa.js";
|
|
15
15
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
16
16
|
import { Audience } from "./visibility/audience.js";
|
|
17
|
-
export { type ActionPermissions, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type User, type WeekDay, assertRouteAccess, checkAccess, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, formatDate, formatDateTime, formatNumber, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, usePwaInstall, useUserTimezone };
|
|
17
|
+
export { type ActionPermissions, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type SoftrUserProperties, type User, type WeekDay, assertRouteAccess, checkAccess, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, formatDate, formatDateTime, formatNumber, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, usePwaInstall, useUserTimezone };
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatasourceAction } from "./action.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as _tanstack_react_start4 from "@tanstack/react-start";
|
|
3
3
|
|
|
4
4
|
//#region src/mcp.d.ts
|
|
5
5
|
|
|
@@ -31,6 +31,6 @@ type SoftrMcpOptions = {
|
|
|
31
31
|
* ],
|
|
32
32
|
* }))
|
|
33
33
|
*/
|
|
34
|
-
declare function softrMcpMiddleware(options?: SoftrMcpOptions):
|
|
34
|
+
declare function softrMcpMiddleware(options?: SoftrMcpOptions): _tanstack_react_start4.RequestMiddlewareAfterServer<{}, undefined, undefined>;
|
|
35
35
|
//#endregion
|
|
36
36
|
export { softrMcpMiddleware };
|
package/dist/mcp.js
CHANGED
|
@@ -2,12 +2,12 @@ import { assert } from "./assertions.js";
|
|
|
2
2
|
import { fetch } from "./fetch.js";
|
|
3
3
|
import { sessionConfig } from "./auth/config.js";
|
|
4
4
|
import { readSessionToken, withSessionToken } from "./auth/session.js";
|
|
5
|
+
import { softrConfig } from "./softr-config.js";
|
|
5
6
|
import { makePlatform, platformApiUrl } from "./auth/platform.js";
|
|
6
7
|
import { isDatasourceAction } from "./action.js";
|
|
7
|
-
import { softrConfig } from "./softr-config.js";
|
|
8
8
|
import { ds } from "./datasource/datasource.js";
|
|
9
|
-
import { createMiddleware } from "@tanstack/react-start";
|
|
10
9
|
import { z } from "zod";
|
|
10
|
+
import { createMiddleware } from "@tanstack/react-start";
|
|
11
11
|
|
|
12
12
|
//#region src/mcp.ts
|
|
13
13
|
const MCP_PATH = "/_softr/mcp";
|
package/dist/package.js
CHANGED
package/dist/pwa.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react0 from "react";
|
|
2
2
|
import "@tanstack/react-start";
|
|
3
3
|
|
|
4
4
|
//#region src/pwa.d.ts
|
|
@@ -23,7 +23,7 @@ import "@tanstack/react-start";
|
|
|
23
23
|
* )
|
|
24
24
|
* }
|
|
25
25
|
*/
|
|
26
|
-
declare function SoftrPwaHead():
|
|
26
|
+
declare function SoftrPwaHead(): react0.FunctionComponentElement<react0.FragmentProps>;
|
|
27
27
|
type PwaPlatform = "ios" | "android" | "desktop";
|
|
28
28
|
type PwaInstall = {
|
|
29
29
|
available: false;
|
package/dist/pwa.js
CHANGED
|
@@ -2,9 +2,9 @@ import { logger } from "./logger.js";
|
|
|
2
2
|
import { fetch } from "./fetch.js";
|
|
3
3
|
import { sessionConfig } from "./auth/config.js";
|
|
4
4
|
import { platformApiUrl } from "./auth/platform.js";
|
|
5
|
+
import { z } from "zod";
|
|
5
6
|
import { Fragment, createElement, useEffect, useState } from "react";
|
|
6
7
|
import { createMiddleware } from "@tanstack/react-start";
|
|
7
|
-
import { z } from "zod";
|
|
8
8
|
|
|
9
9
|
//#region src/pwa.ts
|
|
10
10
|
const MANIFEST_PATH = "/manifest.webmanifest";
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { assert, panic } from "./assertions.js";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import { z } from "zod";
|
|
2
5
|
|
|
3
6
|
//#region src/softr-config.schema.ts
|
|
7
|
+
const SOFTR_CONFIG_FILE = "softr.json";
|
|
4
8
|
const AccessBehaviour = z.union([
|
|
5
9
|
z.string(),
|
|
6
10
|
z.object({
|
|
@@ -9,32 +13,49 @@ const AccessBehaviour = z.union([
|
|
|
9
13
|
}),
|
|
10
14
|
z.object({ strategy: z.literal("throw") })
|
|
11
15
|
], { error: "must be a path like \"/login\", { \"strategy\": \"redirect\", \"to\": \"/login\" } or { \"strategy\": \"throw\" }" });
|
|
12
|
-
const AppPath = z.string().refine((path) => path.startsWith("/") && !path.includes("?") && !path.includes("#") && new URL(path, "https://app.invalid").origin === "https://app.invalid", { error: "must be a path on this app like \"/login\", with no \"?\" or \"#\"" });
|
|
13
|
-
const SoftrConfigSchema = z.object({
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const AppPath = z.string().refine((path$1) => path$1.startsWith("/") && !path$1.includes("?") && !path$1.includes("#") && new URL(path$1, "https://app.invalid").origin === "https://app.invalid", { error: "must be a path on this app like \"/login\", with no \"?\" or \"#\"" });
|
|
17
|
+
const SoftrConfigSchema = z.object({
|
|
18
|
+
auth: z.strictObject({
|
|
19
|
+
routes: z.strictObject({
|
|
20
|
+
login: z.strictObject({
|
|
21
|
+
path: AppPath,
|
|
22
|
+
redirectParam: z.string()
|
|
23
|
+
}),
|
|
24
|
+
resetPassword: z.strictObject({
|
|
25
|
+
path: AppPath,
|
|
26
|
+
tokenParam: z.string()
|
|
27
|
+
}),
|
|
28
|
+
acceptInvitation: z.strictObject({
|
|
29
|
+
path: AppPath,
|
|
30
|
+
tokenParam: z.string()
|
|
31
|
+
}),
|
|
32
|
+
magicLink: z.strictObject({
|
|
33
|
+
path: AppPath,
|
|
34
|
+
tokenParam: z.string()
|
|
35
|
+
})
|
|
18
36
|
}),
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
acceptInvitation: z.strictObject({
|
|
24
|
-
path: AppPath,
|
|
25
|
-
tokenParam: z.string()
|
|
26
|
-
}),
|
|
27
|
-
magicLink: z.strictObject({
|
|
28
|
-
path: AppPath,
|
|
29
|
-
tokenParam: z.string()
|
|
37
|
+
guard: z.strictObject({
|
|
38
|
+
unauthenticated: AccessBehaviour,
|
|
39
|
+
forbidden: AccessBehaviour,
|
|
40
|
+
alreadyAuthenticated: AccessBehaviour
|
|
30
41
|
})
|
|
31
42
|
}),
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
})
|
|
37
|
-
|
|
43
|
+
user: z.strictObject({ properties: z.record(z.string(), z.string()) }).optional()
|
|
44
|
+
});
|
|
45
|
+
function loadSoftrConfig(root) {
|
|
46
|
+
const file = path.join(root, SOFTR_CONFIG_FILE);
|
|
47
|
+
assert(existsSync(file), `${SOFTR_CONFIG_FILE} is missing from ${root}`);
|
|
48
|
+
const source = readFileSync(file, "utf8");
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(source);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
panic(`${SOFTR_CONFIG_FILE} is not valid JSON: ${error instanceof Error ? error.message : error}`);
|
|
54
|
+
}
|
|
55
|
+
const result = SoftrConfigSchema.safeParse(parsed);
|
|
56
|
+
if (!result.success) panic(`${SOFTR_CONFIG_FILE} does not match the schema:\n\n${z.prettifyError(result.error)}`);
|
|
57
|
+
return result.data;
|
|
58
|
+
}
|
|
38
59
|
|
|
39
60
|
//#endregion
|
|
40
|
-
export {
|
|
61
|
+
export { SOFTR_CONFIG_FILE, loadSoftrConfig };
|
package/dist/visibility/route.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ACCESS_REASONS, checkAccess } from "./visibility.js";
|
|
2
1
|
import { softrConfig } from "../softr-config.js";
|
|
2
|
+
import { ACCESS_REASONS, checkAccess } from "./visibility.js";
|
|
3
3
|
import { redirect } from "@tanstack/react-router";
|
|
4
4
|
|
|
5
5
|
//#region src/visibility/route.ts
|
package/dist/vite.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { SoftrConfigSchema } from "./softr-config.schema.js";
|
|
3
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { SOFTR_CONFIG_FILE, loadSoftrConfig } from "./softr-config.schema.js";
|
|
4
2
|
import path from "node:path";
|
|
5
|
-
import { z } from "zod";
|
|
6
3
|
import { pathToFileURL } from "node:url";
|
|
7
4
|
|
|
8
5
|
//#region src/vite.ts
|
|
9
|
-
const SOFTR_CONFIG_FILE = "softr.json";
|
|
10
6
|
function softr() {
|
|
11
7
|
return [softrConfig(), editEngine()];
|
|
12
8
|
}
|
|
@@ -27,20 +23,6 @@ function softrConfig() {
|
|
|
27
23
|
}
|
|
28
24
|
};
|
|
29
25
|
}
|
|
30
|
-
function loadSoftrConfig(root) {
|
|
31
|
-
const file = path.join(root, SOFTR_CONFIG_FILE);
|
|
32
|
-
assert(existsSync(file), `${SOFTR_CONFIG_FILE} is missing from ${root}`);
|
|
33
|
-
const source = readFileSync(file, "utf8");
|
|
34
|
-
let parsed;
|
|
35
|
-
try {
|
|
36
|
-
parsed = JSON.parse(source);
|
|
37
|
-
} catch (error) {
|
|
38
|
-
panic(`${SOFTR_CONFIG_FILE} is not valid JSON: ${error instanceof Error ? error.message : error}`);
|
|
39
|
-
}
|
|
40
|
-
const result = SoftrConfigSchema.safeParse(parsed);
|
|
41
|
-
if (!result.success) panic(`${SOFTR_CONFIG_FILE} does not match the schema:\n\n${z.prettifyError(result.error)}`);
|
|
42
|
-
return result.data;
|
|
43
|
-
}
|
|
44
26
|
/**
|
|
45
27
|
* The edit engine is provided by the pod runtime rather than an app dependency: it is never in the
|
|
46
28
|
* app's `package.json` and never in its `node_modules`. The pod points `SOFTR_EDIT_ENGINE_DIR` at
|
package/package.json
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softr/sdk",
|
|
3
|
-
"version": "0.0.68-
|
|
3
|
+
"version": "0.0.68-props.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
|
-
".": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"import": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"./client": {
|
|
11
|
+
"types": "./dist/client.d.ts",
|
|
12
|
+
"import": "./dist/client.js"
|
|
13
|
+
},
|
|
14
|
+
"./server": {
|
|
15
|
+
"types": "./dist/server.d.ts",
|
|
16
|
+
"import": "./dist/server.js"
|
|
17
|
+
},
|
|
18
|
+
"./vite": {
|
|
19
|
+
"types": "./dist/vite.d.ts",
|
|
20
|
+
"import": "./dist/vite.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"softr-sdk": "./dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"schema",
|
|
29
|
+
"skills",
|
|
30
|
+
"!skills/_artifacts"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
14
35
|
"scripts": {
|
|
15
36
|
"build": "tsdown",
|
|
16
37
|
"test": "node --import ./src/test-utils/setup.ts --test",
|
|
@@ -42,7 +63,9 @@
|
|
|
42
63
|
"optional": true
|
|
43
64
|
}
|
|
44
65
|
},
|
|
45
|
-
"keywords": [
|
|
66
|
+
"keywords": [
|
|
67
|
+
"tanstack-intent"
|
|
68
|
+
],
|
|
46
69
|
"intent": {
|
|
47
70
|
"version": 1,
|
|
48
71
|
"repo": "@softr/sdk",
|
package/schema/softr.schema.json
CHANGED
|
@@ -54,6 +54,19 @@
|
|
|
54
54
|
},
|
|
55
55
|
"required": ["routes", "guard"],
|
|
56
56
|
"additionalProperties": false
|
|
57
|
+
},
|
|
58
|
+
"user": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"description": "The signed-in user.",
|
|
61
|
+
"properties": {
|
|
62
|
+
"properties": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"description": "Fields of the users table the app reads as user.properties, keyed by the name the app uses. Each value is a field id. No other field leaves the server.",
|
|
65
|
+
"additionalProperties": { "type": "string" }
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"required": ["properties"],
|
|
69
|
+
"additionalProperties": false
|
|
57
70
|
}
|
|
58
71
|
},
|
|
59
72
|
"required": ["auth"],
|
|
@@ -19,7 +19,7 @@ description: >-
|
|
|
19
19
|
metadata:
|
|
20
20
|
type: sub-skill
|
|
21
21
|
library: softr-sdk
|
|
22
|
-
library_version: 0.0.68-
|
|
22
|
+
library_version: 0.0.68-props.0
|
|
23
23
|
---
|
|
24
24
|
|
|
25
25
|
**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.68-
|
|
15
|
+
library_version: 0.0.68-props.0
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -4,12 +4,13 @@ description: >-
|
|
|
4
4
|
Read the signed-in Softr visitor: useCurrentUser and the User type,
|
|
5
5
|
context.user in a route's beforeLoad, and context.user inside a server
|
|
6
6
|
function handler, where Start types it from the app's registered request
|
|
7
|
-
middleware. Covers the null guest
|
|
8
|
-
|
|
7
|
+
middleware. Covers the null guest, the generated SoftrUserGroups that narrows
|
|
8
|
+
group ids to the app's own, and user.properties, the fields of the visitor's
|
|
9
|
+
row that softr.json declares.
|
|
9
10
|
metadata:
|
|
10
11
|
type: sub-skill
|
|
11
12
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.68-
|
|
13
|
+
library_version: 0.0.68-props.0
|
|
13
14
|
---
|
|
14
15
|
|
|
15
16
|
# The current user
|
|
@@ -31,6 +32,7 @@ type User = {
|
|
|
31
32
|
email: string
|
|
32
33
|
fullName: string
|
|
33
34
|
userGroupIds: Array<SoftrUserGroupId> // the app's own group ids
|
|
35
|
+
properties: SoftrUserProperties // the aliases softr.json declares
|
|
34
36
|
}
|
|
35
37
|
```
|
|
36
38
|
|
|
@@ -90,6 +92,25 @@ For every group's name and id, read `docs/user-groups.md`. It lists every group
|
|
|
90
92
|
|
|
91
93
|
`user.userGroupIds` and every `customGroups` the SDK accepts take only those ids, so an id the app does not have stops compiling. A new group appears in the list once it exists on the app.
|
|
92
94
|
|
|
95
|
+
### Fields of the visitor's row: `user.properties`
|
|
96
|
+
|
|
97
|
+
To read more of the visitor's row in the users table, give each field an alias in `softr.json`, under `user.properties`. The value is the field's id:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"user": {
|
|
102
|
+
"properties": {
|
|
103
|
+
"plan": "OeIz1",
|
|
104
|
+
"seats": "1iHKE"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`user.properties` then holds exactly those aliases. To read them, see `docs/user-properties.md`.
|
|
111
|
+
|
|
112
|
+
Attachment and long text fields, and linked record and lookup fields with multiple entries, cannot be properties. Read those with `defineQuery` on the users table. See [record-queries](../record-queries/SKILL.md).
|
|
113
|
+
|
|
93
114
|
## References
|
|
94
115
|
|
|
95
116
|
- [routes-and-visibility/SKILL.md](../routes-and-visibility/SKILL.md): `isAuthorized` narrows `context.user` while applying the route's rule
|
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.68-
|
|
13
|
+
library_version: 0.0.68-props.0
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**Boundary.** `upload` lives on `@softr/sdk/server` and runs inside server functions. `useFileUpload` and `FileItem` live on `@softr/sdk` and run in components. `FileAttachment` comes from `@softr/sdk` and can be imported anywhere.
|
|
@@ -5,12 +5,12 @@ description: >-
|
|
|
5
5
|
defineIntegration from @softr/sdk declares the action with its integrationId,
|
|
6
6
|
the one HTTP method and endpoint url it allows, and permissions;
|
|
7
7
|
integration.createFetcher from @softr/sdk/server binds it and returns a
|
|
8
|
-
fetcher function you call with optional params, query, headers, body
|
|
9
|
-
|
|
8
|
+
fetcher function you call with optional params, query, headers, and body.
|
|
9
|
+
Check res.ok before res.json().
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.68-
|
|
13
|
+
library_version: 0.0.68-props.0
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineIntegration` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -164,41 +164,6 @@ const text = body.content.flatMap((block) => (block.type === 'text' && block.tex
|
|
|
164
164
|
the button is pressed. `claude-opus-5-5` is the current default; `claude-sonnet-5-5` is the cheaper one for a
|
|
165
165
|
short summary.
|
|
166
166
|
|
|
167
|
-
## Streaming a vendor answer
|
|
168
|
-
|
|
169
|
-
Some vendor endpoints can send their answer in pieces while they produce it — LLM completions token by token, long exports, live event feeds. The fetcher relays such an answer as it arrives when you pass `stream: true`; the response body is then the vendor's own stream, unchanged.
|
|
170
|
-
|
|
171
|
-
Decide per call, from the vendor's API docs for that endpoint, not from the integration type or the action:
|
|
172
|
-
|
|
173
|
-
- The endpoint offers a streaming mode (a `stream: true` body flag, an `Accept: text/event-stream` variant, a chunked response the docs say to read incrementally). Ask the vendor for it in the request and pass `stream: true` to the fetcher.
|
|
174
|
-
- The endpoint answers in one piece, which is the case for most REST calls. Call the fetcher without `stream`; the answer is buffered and `res.json()` works.
|
|
175
|
-
|
|
176
|
-
Hand a streamed body to the browser as a `Response` from the server function and read it there. The example below streams a completion through the `askClaude` action declared above; the same shape applies to any vendor endpoint that streams.
|
|
177
|
-
|
|
178
|
-
```ts
|
|
179
|
-
const streamAnswer = createServerFn({ method: 'POST' })
|
|
180
|
-
.validator((data: { prompt: string }) => data)
|
|
181
|
-
.handler(async ({ data }) => {
|
|
182
|
-
const claude = integration.createFetcher(askClaude)
|
|
183
|
-
const res = await claude({
|
|
184
|
-
stream: true,
|
|
185
|
-
headers: { 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01' },
|
|
186
|
-
body: JSON.stringify({
|
|
187
|
-
model: 'claude-opus-5-5',
|
|
188
|
-
max_tokens: 1024,
|
|
189
|
-
stream: true,
|
|
190
|
-
messages: [{ role: 'user', content: data.prompt }],
|
|
191
|
-
}),
|
|
192
|
-
})
|
|
193
|
-
if (!res.ok) throw new Error(`AI error: ${res.status} ${await res.text()}`)
|
|
194
|
-
return new Response(res.body, { headers: { 'Content-Type': 'text/event-stream' } })
|
|
195
|
-
})
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
On the client, call the server function's URL with `fetch` and read `response.body` with a `ReadableStream` reader; each chunk arrives in the vendor's own format, for an event stream one or more `event:`/`data:` frames. Use `stream: true` only together with a request that asks the vendor to stream: without it the vendor answers in one piece, which still works but gains nothing.
|
|
199
|
-
|
|
200
|
-
`stream: true` is not available for REST_API integrations; those always answer in one piece.
|
|
201
|
-
|
|
202
167
|
## Base URLs by integration type
|
|
203
168
|
|
|
204
169
|
Match the `integrationType` from the attached integrations section to find the base URL:
|
|
@@ -270,5 +235,3 @@ If the API the user needs is not in the table above, call it directly from a ser
|
|
|
270
235
|
**Not checking `res.ok`.** The fetcher returns the proxied response as-is. A 4xx or 5xx from the third-party API is not thrown — call `res.ok` or `res.status` before calling `res.json()`.
|
|
271
236
|
|
|
272
237
|
**Reusing one action for a different endpoint.** An action authorizes one `method` and one endpoint `url`. A different endpoint belongs to a different action — declare one for it, with its own audience.
|
|
273
|
-
|
|
274
|
-
**Reading a streamed body with `res.json()`.** With `stream: true` the body is an event stream, not one JSON document. Return `res.body` in a `Response` or read it with a stream reader; `res.json()` waits for the end and then fails to parse.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.68-
|
|
12
|
+
library_version: 0.0.68-props.0
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `softrMcpMiddleware` lives on `@softr/sdk` and is registered once, in `src/start.ts`. The actions you hand it are the same `defineQuery` / `defineMutation` values the app's own pages use — declared wherever they already live, not duplicated for MCP.
|
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
metadata:
|
|
12
12
|
type: sub-skill
|
|
13
13
|
library: softr-sdk
|
|
14
|
-
library_version: 0.0.68-
|
|
14
|
+
library_version: 0.0.68-props.0
|
|
15
15
|
---
|
|
16
16
|
|
|
17
17
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineMutation` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -13,7 +13,7 @@ description: >-
|
|
|
13
13
|
metadata:
|
|
14
14
|
type: sub-skill
|
|
15
15
|
library: softr-sdk
|
|
16
|
-
library_version: 0.0.68-
|
|
16
|
+
library_version: 0.0.68-props.0
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. The same holds for any helper that calls `ds`: declare it inside the handler. A function at module level that calls `ds` keeps the server-only import in the browser bundle, and every page stops loading until it moves. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -13,7 +13,7 @@ description: >-
|
|
|
13
13
|
metadata:
|
|
14
14
|
type: sub-skill
|
|
15
15
|
library: softr-sdk
|
|
16
|
-
library_version: 0.0.68-
|
|
16
|
+
library_version: 0.0.68-props.0
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
**Boundary.** `defineVisibility`, the access functions and the error predicates come from `@softr/sdk` and work anywhere.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.68-
|
|
12
|
+
library_version: 0.0.68-props.0
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `workflow` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineWorkflow` and every type come from `@softr/sdk` and can be imported anywhere.
|