@softr/sdk 0.0.78-alpha.2 → 0.0.79-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/action.d.ts +1 -1
- package/dist/action.js +1 -1
- package/dist/assertions.js +1 -1
- package/dist/auth/methods/profile.d.ts +5 -5
- package/dist/auth/methods/profile.js +2 -2
- package/dist/auth/platform.d.ts +0 -1
- package/dist/auth/platform.js +36 -14
- 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 +38 -12
- package/dist/codegen/datasource-docs.js +92 -1
- package/dist/codegen/datasource-types.js +16 -1
- package/dist/codegen/fetch-metadata.js +27 -11
- package/dist/codegen/softr-env.js +2 -1
- package/dist/cookies.js +10 -2
- package/dist/formatting.d.ts +1 -1
- 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.js +1 -1
- package/dist/route/route.js +1 -1
- package/dist/softr-config.schema.js +52 -24
- package/dist/vite.js +1 -19
- package/package.json +1 -1
- package/schema/softr.schema.json +13 -0
- package/skills/softr-sdk/SKILL.md +1 -1
- package/skills/softr-sdk/ai/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/comments/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- 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 +1 -1
- package/skills/softr-sdk/mcp/SKILL.md +1 -1
- package/skills/softr-sdk/pwa/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +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/action.d.ts
CHANGED
|
@@ -265,7 +265,7 @@ type CommentsDefinition<TId extends string> = {
|
|
|
265
265
|
*/
|
|
266
266
|
declare function defineComments<const TId extends string>(definition: CommentsDefinition<TId>): CommentsAction<TId>;
|
|
267
267
|
type AiResource = {
|
|
268
|
-
type: "
|
|
268
|
+
type: "SOFTR_AI";
|
|
269
269
|
model: string;
|
|
270
270
|
};
|
|
271
271
|
type AiAction<TId extends string = string> = Action<AiResource> & {
|
package/dist/action.js
CHANGED
package/dist/assertions.js
CHANGED
|
@@ -2,7 +2,7 @@ import { User } from "../user.js";
|
|
|
2
2
|
import { AuthError, Result } from "./utils.js";
|
|
3
3
|
|
|
4
4
|
//#region src/auth/methods/profile.d.ts
|
|
5
|
-
|
|
5
|
+
type Profile = Pick<User, "id" | "email" | "fullName" | "userGroupIds" | "avatarUrl" | "avatarEditable">;
|
|
6
6
|
/**
|
|
7
7
|
* Changes the signed-in visitor's own name, photo, or both. A field left out
|
|
8
8
|
* stays as it is. {@link removeAvatar} removes the photo.
|
|
@@ -10,7 +10,7 @@ import { AuthError, Result } from "./utils.js";
|
|
|
10
10
|
* The photo can change only where `user.avatarEditable` is true. Elsewhere a
|
|
11
11
|
* photo answers `avatarNotSupported`, so hide the photo controls instead.
|
|
12
12
|
*
|
|
13
|
-
* Answers with the visitor as they are now
|
|
13
|
+
* Answers with the visitor as they are now, without `properties`. The page keeps showing what it
|
|
14
14
|
* loaded until its route context loads again, so call `router.invalidate()`
|
|
15
15
|
* after a success.
|
|
16
16
|
*
|
|
@@ -21,15 +21,15 @@ import { AuthError, Result } from "./utils.js";
|
|
|
21
21
|
declare function updateProfile(changes: {
|
|
22
22
|
fullName?: string;
|
|
23
23
|
avatar?: File;
|
|
24
|
-
}): Promise<Result<
|
|
24
|
+
}): Promise<Result<Profile, AuthError>>;
|
|
25
25
|
/**
|
|
26
26
|
* Removes the signed-in visitor's own photo. Like {@link updateProfile}, it
|
|
27
27
|
* works only where `user.avatarEditable` is true, and answers with the visitor
|
|
28
|
-
* as they are now
|
|
28
|
+
* as they are now, without `properties`.
|
|
29
29
|
*
|
|
30
30
|
* @example
|
|
31
31
|
* const result = await auth.removeAvatar()
|
|
32
32
|
*/
|
|
33
|
-
declare function removeAvatar(): Promise<Result<
|
|
33
|
+
declare function removeAvatar(): Promise<Result<Profile, AuthError>>;
|
|
34
34
|
//#endregion
|
|
35
35
|
export { removeAvatar, updateProfile };
|
|
@@ -12,7 +12,7 @@ import { authError } from "./utils.js";
|
|
|
12
12
|
* The photo can change only where `user.avatarEditable` is true. Elsewhere a
|
|
13
13
|
* photo answers `avatarNotSupported`, so hide the photo controls instead.
|
|
14
14
|
*
|
|
15
|
-
* Answers with the visitor as they are now
|
|
15
|
+
* Answers with the visitor as they are now, without `properties`. The page keeps showing what it
|
|
16
16
|
* loaded until its route context loads again, so call `router.invalidate()`
|
|
17
17
|
* after a success.
|
|
18
18
|
*
|
|
@@ -32,7 +32,7 @@ async function updateProfile(changes) {
|
|
|
32
32
|
/**
|
|
33
33
|
* Removes the signed-in visitor's own photo. Like {@link updateProfile}, it
|
|
34
34
|
* works only where `user.avatarEditable` is true, and answers with the visitor
|
|
35
|
-
* as they are now
|
|
35
|
+
* as they are now, without `properties`.
|
|
36
36
|
*
|
|
37
37
|
* @example
|
|
38
38
|
* const result = await auth.removeAvatar()
|
package/dist/auth/platform.d.ts
CHANGED
package/dist/auth/platform.js
CHANGED
|
@@ -2,7 +2,7 @@ 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,32 @@ 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 profileSchema = z.object({
|
|
39
|
+
id: z.string(),
|
|
40
|
+
email: z.string(),
|
|
41
|
+
fullName: z.string(),
|
|
42
|
+
userGroupIds: z.array(z.string()),
|
|
43
|
+
avatarUrl: z.string().nullable().default(null),
|
|
44
|
+
avatarEditable: z.boolean().default(false)
|
|
45
|
+
});
|
|
46
|
+
const userSchema = profileSchema.extend({ attributes: z.record(z.string(), z.unknown()).optional() }).transform(({ attributes,...identity }) => {
|
|
47
|
+
const properties = {};
|
|
48
|
+
for (const [alias, field] of aliases) {
|
|
49
|
+
assert(attributes !== void 0, "The signed-in user came back without their properties.");
|
|
50
|
+
assert(Object.hasOwn(attributes, field), `softr.json user.properties.${alias} names the field "${field}", which the users table does not have.`);
|
|
51
|
+
properties[alias] = attributes[field];
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
...identity,
|
|
55
|
+
properties
|
|
56
|
+
};
|
|
57
|
+
});
|
|
36
58
|
return {
|
|
37
59
|
authSettings: () => request(`/v1/vibe/applications/${applicationId}/users/auth-settings`, { method: "GET" }, authSettingsSchema),
|
|
38
60
|
session: async (token) => {
|
|
39
|
-
const result = await request(`/v1/vibe/applications/${applicationId}/users/session`, {
|
|
61
|
+
const result = await request(`/v1/vibe/applications/${applicationId}/users/session${withAttributes}`, {
|
|
40
62
|
method: "GET",
|
|
41
63
|
token
|
|
42
64
|
}, z.object({
|
|
@@ -46,7 +68,7 @@ function makePlatform(applicationId) {
|
|
|
46
68
|
if (result.status === 404) panic(`Application ${applicationId} not found. Check SOFTR_APPLICATION_ID.`);
|
|
47
69
|
return result;
|
|
48
70
|
},
|
|
49
|
-
tokens: (credentials) => request(`/v1/vibe/applications/${applicationId}/users/tokens`, {
|
|
71
|
+
tokens: (credentials) => request(`/v1/vibe/applications/${applicationId}/users/tokens${withAttributes}`, {
|
|
50
72
|
method: "POST",
|
|
51
73
|
json: credentials
|
|
52
74
|
}, z.object({
|
|
@@ -69,7 +91,7 @@ function makePlatform(applicationId) {
|
|
|
69
91
|
method: "POST",
|
|
70
92
|
json: { email }
|
|
71
93
|
}, z.null()),
|
|
72
|
-
signInWithCode: (credentials) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-code`, {
|
|
94
|
+
signInWithCode: (credentials) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-code${withAttributes}`, {
|
|
73
95
|
method: "POST",
|
|
74
96
|
json: credentials
|
|
75
97
|
}, z.object({
|
|
@@ -88,7 +110,7 @@ function makePlatform(applicationId) {
|
|
|
88
110
|
challengeToken: z.string()
|
|
89
111
|
}).nullish()
|
|
90
112
|
})),
|
|
91
|
-
signInWithGoogle: (body) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-google`, {
|
|
113
|
+
signInWithGoogle: (body) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-google${withAttributes}`, {
|
|
92
114
|
method: "POST",
|
|
93
115
|
json: body
|
|
94
116
|
}, z.object({
|
|
@@ -107,7 +129,7 @@ function makePlatform(applicationId) {
|
|
|
107
129
|
challengeToken: z.string()
|
|
108
130
|
}).nullish()
|
|
109
131
|
})),
|
|
110
|
-
signInWithMagicLink: (body) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-magic-link`, {
|
|
132
|
+
signInWithMagicLink: (body) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-magic-link${withAttributes}`, {
|
|
111
133
|
method: "POST",
|
|
112
134
|
json: body
|
|
113
135
|
}, z.object({
|
|
@@ -134,7 +156,7 @@ function makePlatform(applicationId) {
|
|
|
134
156
|
method: "POST",
|
|
135
157
|
json: body
|
|
136
158
|
}, z.object({ email: z.string() })),
|
|
137
|
-
setPassword: (body) => request(`/v1/vibe/applications/${applicationId}/users/set-password`, {
|
|
159
|
+
setPassword: (body) => request(`/v1/vibe/applications/${applicationId}/users/set-password${withAttributes}`, {
|
|
138
160
|
method: "POST",
|
|
139
161
|
json: body
|
|
140
162
|
}, z.object({
|
|
@@ -157,7 +179,7 @@ function makePlatform(applicationId) {
|
|
|
157
179
|
method: "POST",
|
|
158
180
|
json: body
|
|
159
181
|
}, z.null()),
|
|
160
|
-
signUpWithCode: (body) => request(`/v1/vibe/applications/${applicationId}/users/sign-up/with-code`, {
|
|
182
|
+
signUpWithCode: (body) => request(`/v1/vibe/applications/${applicationId}/users/sign-up/with-code${withAttributes}`, {
|
|
161
183
|
method: "POST",
|
|
162
184
|
json: body
|
|
163
185
|
}, z.object({
|
|
@@ -176,7 +198,7 @@ function makePlatform(applicationId) {
|
|
|
176
198
|
challengeToken: z.string()
|
|
177
199
|
}).nullish()
|
|
178
200
|
})),
|
|
179
|
-
signUpWithPassword: (body) => request(`/v1/vibe/applications/${applicationId}/users/sign-up/with-password`, {
|
|
201
|
+
signUpWithPassword: (body) => request(`/v1/vibe/applications/${applicationId}/users/sign-up/with-password${withAttributes}`, {
|
|
180
202
|
method: "POST",
|
|
181
203
|
json: body
|
|
182
204
|
}, z.object({
|
|
@@ -202,7 +224,7 @@ function makePlatform(applicationId) {
|
|
|
202
224
|
otpauthUri: z.string(),
|
|
203
225
|
qrPngBase64: z.string()
|
|
204
226
|
})),
|
|
205
|
-
authenticatorSetupComplete: (token, code) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/setup/complete`, {
|
|
227
|
+
authenticatorSetupComplete: (token, code) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/setup/complete${withAttributes}`, {
|
|
206
228
|
method: "POST",
|
|
207
229
|
token,
|
|
208
230
|
json: { code }
|
|
@@ -214,7 +236,7 @@ function makePlatform(applicationId) {
|
|
|
214
236
|
}),
|
|
215
237
|
backupCodes: z.array(z.string())
|
|
216
238
|
})),
|
|
217
|
-
authenticatorSetupDismiss: (token) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/setup/dismiss`, {
|
|
239
|
+
authenticatorSetupDismiss: (token) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/setup/dismiss${withAttributes}`, {
|
|
218
240
|
method: "POST",
|
|
219
241
|
token
|
|
220
242
|
}, z.object({
|
|
@@ -233,7 +255,7 @@ function makePlatform(applicationId) {
|
|
|
233
255
|
challengeToken: z.string()
|
|
234
256
|
}).nullish()
|
|
235
257
|
})),
|
|
236
|
-
authenticatorVerify: (token, body) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/totp/verify`, {
|
|
258
|
+
authenticatorVerify: (token, body) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/totp/verify${withAttributes}`, {
|
|
237
259
|
method: "POST",
|
|
238
260
|
token,
|
|
239
261
|
json: body
|
|
@@ -271,11 +293,11 @@ function makePlatform(applicationId) {
|
|
|
271
293
|
method: "PATCH",
|
|
272
294
|
token,
|
|
273
295
|
form
|
|
274
|
-
},
|
|
296
|
+
}, profileSchema),
|
|
275
297
|
removeAvatar: (token) => request(`/v1/vibe/applications/${applicationId}/users/me/profile/avatar`, {
|
|
276
298
|
method: "DELETE",
|
|
277
299
|
token
|
|
278
|
-
},
|
|
300
|
+
}, profileSchema),
|
|
279
301
|
signOut: (token) => request(`/v1/applications/${applicationId}/users/sign-out`, {
|
|
280
302
|
method: "POST",
|
|
281
303
|
token
|
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,6 +13,12 @@ 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;
|
|
@@ -23,6 +29,7 @@ type User = {
|
|
|
23
29
|
avatarUrl: string | null;
|
|
24
30
|
/** Whether `auth.updateProfile` can change their photo in this app. */
|
|
25
31
|
avatarEditable: boolean;
|
|
32
|
+
properties: SoftrUserProperties;
|
|
26
33
|
};
|
|
27
34
|
//#endregion
|
|
28
|
-
export { SoftrUserGroupId, SoftrUserGroups, User };
|
|
35
|
+
export { SoftrUserGroupId, SoftrUserGroups, SoftrUserProperties, User };
|
package/dist/auth/user.js
CHANGED
|
@@ -7,7 +7,8 @@ const userSchema = z.object({
|
|
|
7
7
|
fullName: z.string(),
|
|
8
8
|
userGroupIds: z.array(z.string()),
|
|
9
9
|
avatarUrl: z.string().nullable().default(null),
|
|
10
|
-
avatarEditable: z.boolean().default(false)
|
|
10
|
+
avatarEditable: z.boolean().default(false),
|
|
11
|
+
properties: z.record(z.string(), z.unknown())
|
|
11
12
|
});
|
|
12
13
|
|
|
13
14
|
//#endregion
|
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { version } from "./package.js";
|
|
3
3
|
import { requireDefined } from "./assertions.js";
|
|
4
|
+
import { SoftrConfigError, loadSoftrConfig } from "./softr-config.schema.js";
|
|
4
5
|
import { fetchSoftrEnvInput, metadataSourceFromEnv } from "./codegen/fetch-metadata.js";
|
|
6
|
+
import { renderUserPropertiesDoc } from "./codegen/datasource-docs.js";
|
|
5
7
|
import { renderUserGroupsDoc } from "./codegen/user-groups.js";
|
|
6
8
|
import { renderSoftrEnv } from "./codegen/softr-env.js";
|
|
7
9
|
import { renderSoftrFields } from "./codegen/softr-fields.js";
|
|
8
10
|
import { writeIfChanged } from "./codegen/write-if-changed.js";
|
|
9
11
|
import { writeDatasourceDocs } from "./codegen/write-datasource-docs.js";
|
|
10
|
-
import { writeFileSync } from "node:fs";
|
|
12
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
11
13
|
import path from "node:path";
|
|
12
14
|
import { parseArgs } from "node:util";
|
|
13
15
|
|
|
@@ -16,9 +18,10 @@ const OUTPUT = "softr-env.d.ts";
|
|
|
16
18
|
const FIELDS_OUTPUT = "src/softr-fields.ts";
|
|
17
19
|
const DOCS_DIR = "docs/datasources";
|
|
18
20
|
const USER_GROUPS_DOC = "docs/user-groups.md";
|
|
21
|
+
const USER_PROPERTIES_DOC = "docs/user-properties.md";
|
|
19
22
|
async function generate({ json }) {
|
|
20
23
|
if (json) console.info = console.error;
|
|
21
|
-
const input = await fetchSoftrEnvInput(metadataSourceFromEnv());
|
|
24
|
+
const input = await fetchSoftrEnvInput(metadataSourceFromEnv(), loadSoftrConfig(process.cwd()).user?.properties ?? {});
|
|
22
25
|
writeFileSync(path.resolve(process.cwd(), OUTPUT), renderSoftrEnv(input, version), "utf8");
|
|
23
26
|
const report = writeDocs(input);
|
|
24
27
|
report.docs.push({
|
|
@@ -33,18 +36,30 @@ async function generate({ json }) {
|
|
|
33
36
|
printReport(report);
|
|
34
37
|
}
|
|
35
38
|
function writeDocs(input) {
|
|
39
|
+
if (input.users === null) rmSync(path.resolve(process.cwd(), USER_PROPERTIES_DOC), { force: true });
|
|
36
40
|
const datasourceDocs = writeDatasourceDocs(input.datasources, path.resolve(process.cwd(), DOCS_DIR));
|
|
37
41
|
return {
|
|
38
42
|
types: OUTPUT,
|
|
39
|
-
docs: [
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
docs: [
|
|
44
|
+
{
|
|
45
|
+
subject: "datasources",
|
|
46
|
+
path: `${DOCS_DIR}/README.md`,
|
|
47
|
+
changed: datasourceDocs.changed
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
subject: "user groups",
|
|
51
|
+
path: USER_GROUPS_DOC,
|
|
52
|
+
changed: writeIfChanged(path.resolve(process.cwd(), USER_GROUPS_DOC), renderUserGroupsDoc(input.userGroups))
|
|
53
|
+
},
|
|
54
|
+
...input.users === null ? [] : [{
|
|
55
|
+
subject: "user properties",
|
|
56
|
+
path: USER_PROPERTIES_DOC,
|
|
57
|
+
changed: writeIfChanged(path.resolve(process.cwd(), USER_PROPERTIES_DOC), renderUserPropertiesDoc(input.users, {
|
|
58
|
+
datasources: new Map(input.datasources.map((d) => [d.id, d])),
|
|
59
|
+
fileNames: datasourceDocs.fileNames
|
|
60
|
+
}))
|
|
61
|
+
}]
|
|
62
|
+
],
|
|
48
63
|
datasources: input.datasources.map(({ id, name }) => ({
|
|
49
64
|
id,
|
|
50
65
|
name,
|
|
@@ -68,7 +83,18 @@ const { positionals, values } = parseArgs({
|
|
|
68
83
|
} }
|
|
69
84
|
});
|
|
70
85
|
const [command] = positionals;
|
|
71
|
-
if (command === "generate")
|
|
86
|
+
if (command === "generate") try {
|
|
87
|
+
await generate({ json: values.json });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (!values.json) throw error;
|
|
90
|
+
const kind = error instanceof SoftrConfigError ? "softr-config" : "other";
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
92
|
+
process.stdout.write(`${JSON.stringify({ error: {
|
|
93
|
+
kind,
|
|
94
|
+
message
|
|
95
|
+
} })}\n`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
72
98
|
else {
|
|
73
99
|
console.error(command ? `softr-sdk: unknown command '${command}'` : "softr-sdk: no command given");
|
|
74
100
|
console.error("usage: softr-sdk generate [--json]");
|
|
@@ -18,6 +18,97 @@ 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}` + (registered === void 0 ? "" : ` \nRegistered as: [${datasourceLabel(registered)}](./datasources/${requireDefined(context.fileNames.get(registered.id))}) (${registered.id})`),
|
|
37
|
+
"",
|
|
38
|
+
"> This document is generated automatically when softr.json's `user.properties` or the users table changes.",
|
|
39
|
+
"",
|
|
40
|
+
"Each alias below is declared in softr.json under `user.properties`, and read as `user.properties.<alias>`.",
|
|
41
|
+
"",
|
|
42
|
+
"## Field reference",
|
|
43
|
+
"",
|
|
44
|
+
...aliases.flatMap(([alias, field]) => {
|
|
45
|
+
const linkedId = option(field.options, "linkedTableId");
|
|
46
|
+
const linked = typeof linkedId === "string" ? linkedDatasource(linkedId, context.datasources) : void 0;
|
|
47
|
+
return [
|
|
48
|
+
`### ${alias}`,
|
|
49
|
+
"",
|
|
50
|
+
`- Field: ${displayName(field.name)}`,
|
|
51
|
+
`- ID: ${field.id}`,
|
|
52
|
+
`- Type: ${fieldTypeLiteral(field).slice(1, -1)}`,
|
|
53
|
+
...optionLines(field),
|
|
54
|
+
...choiceLines(field),
|
|
55
|
+
...typeof linkedId !== "string" ? [] : linked === void 0 ? [`- Links to: ${linkedId}, not registered`] : [`- Links to: [${datasourceLabel(linked)}](./datasources/${requireDefined(context.fileNames.get(linked.id))}) (${linked.id})`],
|
|
56
|
+
""
|
|
57
|
+
];
|
|
58
|
+
}),
|
|
59
|
+
"## Read shape",
|
|
60
|
+
"",
|
|
61
|
+
"In a component:",
|
|
62
|
+
"",
|
|
63
|
+
"```tsx",
|
|
64
|
+
"import { useCurrentUser } from '@softr/sdk'",
|
|
65
|
+
"",
|
|
66
|
+
"const user = useCurrentUser()",
|
|
67
|
+
"if (user === null) return null",
|
|
68
|
+
"",
|
|
69
|
+
...sampled.map(([alias, field]) => `user.properties.${alias} // ${fieldTypeLiteral(field).slice(1, -1)}`),
|
|
70
|
+
"```",
|
|
71
|
+
"",
|
|
72
|
+
"In a route's `beforeLoad`, to send a visitor elsewhere. It guards navigation only, not data:",
|
|
73
|
+
"",
|
|
74
|
+
"```tsx",
|
|
75
|
+
"import { createFileRoute, redirect } from '@tanstack/react-router'",
|
|
76
|
+
"import { assertRouteAccess, defineVisibility } from '@softr/sdk'",
|
|
77
|
+
"",
|
|
78
|
+
"const visibility = defineVisibility({ userGroupTypes: ['LOGGED_IN_USERS'] })",
|
|
79
|
+
"",
|
|
80
|
+
"export const Route = createFileRoute('/example')({",
|
|
81
|
+
" beforeLoad: (opts) => {",
|
|
82
|
+
" assertRouteAccess(visibility, opts)",
|
|
83
|
+
` if (opts.context.user.properties.${first} == null) throw redirect({ to: '/' })`,
|
|
84
|
+
" },",
|
|
85
|
+
"})",
|
|
86
|
+
"```",
|
|
87
|
+
"",
|
|
88
|
+
"In a server function, called from a route's `loader`. The handler checks the visitor itself:",
|
|
89
|
+
"",
|
|
90
|
+
"```tsx",
|
|
91
|
+
"import { createFileRoute } from '@tanstack/react-router'",
|
|
92
|
+
"import { createServerFn } from '@tanstack/react-start'",
|
|
93
|
+
"",
|
|
94
|
+
`const get${pascalCase(first)} = createServerFn().handler(async ({ context }) => {`,
|
|
95
|
+
" if (!context.user) throw new Error('Not signed in')",
|
|
96
|
+
` return context.user.properties.${first}`,
|
|
97
|
+
"})",
|
|
98
|
+
"",
|
|
99
|
+
"export const Route = createFileRoute('/example')({",
|
|
100
|
+
` loader: () => get${pascalCase(first)}(),`,
|
|
101
|
+
" component: Example,",
|
|
102
|
+
"})",
|
|
103
|
+
"",
|
|
104
|
+
"function Example() {",
|
|
105
|
+
` const ${first} = Route.useLoaderData()`,
|
|
106
|
+
" // ...",
|
|
107
|
+
"}",
|
|
108
|
+
"```",
|
|
109
|
+
""
|
|
110
|
+
].join("\n");
|
|
111
|
+
}
|
|
21
112
|
/** Two trailing spaces are a markdown hard break, which keeps the block one paragraph. */
|
|
22
113
|
function summary(datasource, context) {
|
|
23
114
|
const lines = [`Datasource ID: ${datasource.id}`];
|
|
@@ -315,4 +406,4 @@ function words(value) {
|
|
|
315
406
|
}
|
|
316
407
|
|
|
317
408
|
//#endregion
|
|
318
|
-
export { DOC_NOTE, renderDatasourceDoc, renderDatasourceIndex };
|
|
409
|
+
export { DOC_NOTE, renderDatasourceDoc, renderDatasourceIndex, renderUserPropertiesDoc };
|
|
@@ -238,6 +238,21 @@ function linkedRecord(options, context) {
|
|
|
238
238
|
if (linked === void 0 || linked.primaryFieldId === "") return "LinkedRecord";
|
|
239
239
|
return `LinkedRecord<NonNullable<${`${nameOf(linked.id, context)}Read[${quote(linked.primaryFieldId)}]`}>>`;
|
|
240
240
|
}
|
|
241
|
+
/** `SoftrUserProperties`, each alias typed as its field reads. */
|
|
242
|
+
function renderUserProperties(properties, datasources) {
|
|
243
|
+
const context = {
|
|
244
|
+
datasources: byId(datasources),
|
|
245
|
+
names: typeNames(datasources)
|
|
246
|
+
};
|
|
247
|
+
return [
|
|
248
|
+
"interface SoftrUserProperties {",
|
|
249
|
+
...Object.entries(properties).map(([alias, field]) => indent(readMember({
|
|
250
|
+
...field,
|
|
251
|
+
id: alias
|
|
252
|
+
}, context))),
|
|
253
|
+
"}"
|
|
254
|
+
].join("\n");
|
|
255
|
+
}
|
|
241
256
|
function renderRegistry(datasources, context) {
|
|
242
257
|
return [
|
|
243
258
|
"interface SoftrDatasources {",
|
|
@@ -481,4 +496,4 @@ function indent(block) {
|
|
|
481
496
|
}
|
|
482
497
|
|
|
483
498
|
//#endregion
|
|
484
|
-
export { choiceEntries, fieldTypeLiteral, isWritable, linkedDatasource, option, readExample, renderDatasourceTypes, resolvedShape, scalarExample, underlyingType };
|
|
499
|
+
export { choiceEntries, fieldTypeLiteral, isWritable, linkedDatasource, option, readExample, renderDatasourceTypes, renderUserProperties, resolvedShape, scalarExample, underlyingType };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { panic } from "../assertions.js";
|
|
2
2
|
import { fetch } from "../fetch.js";
|
|
3
|
+
import { panicConfig } from "../softr-config.schema.js";
|
|
3
4
|
|
|
4
5
|
//#region src/codegen/fetch-metadata.ts
|
|
5
6
|
function metadataSourceFromEnv(env = process.env) {
|
|
@@ -17,20 +18,35 @@ function metadataSourceFromEnv(env = process.env) {
|
|
|
17
18
|
token: env.SOFTR_STUDIO_TOKEN
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
|
-
async function fetchSoftrEnvInput(source) {
|
|
21
|
+
async function fetchSoftrEnvInput(source, aliases) {
|
|
21
22
|
const { studioApiUrl, datasourceApiUrl, applicationId, token } = source;
|
|
22
23
|
const [registered, userGroups] = await Promise.all([apiFetch(`${studioApiUrl}/v1/applications/${applicationId}/datasources`, token), apiFetch(`${studioApiUrl}/v1/applications/${applicationId}/access-control/user-groups`, token)]);
|
|
24
|
+
const datasources = await Promise.all(registered.map(async ({ name, datasource }) => {
|
|
25
|
+
const metadata = await apiFetch(`${datasourceApiUrl}/v1/datasource/vibe/studio/applications/${applicationId}/datasources/${datasource.id}/metadata`, token);
|
|
26
|
+
return {
|
|
27
|
+
...metadata,
|
|
28
|
+
id: datasource.id,
|
|
29
|
+
tableId: metadata.id,
|
|
30
|
+
name
|
|
31
|
+
};
|
|
32
|
+
}));
|
|
33
|
+
let users = null;
|
|
34
|
+
if (Object.keys(aliases).length > 0) {
|
|
35
|
+
const metadata = await apiFetch(`${datasourceApiUrl}/v1/datasource/vibe/studio/applications/${applicationId}/user-connection/metadata`, token);
|
|
36
|
+
users = {
|
|
37
|
+
tableId: metadata.id,
|
|
38
|
+
name: metadata.name,
|
|
39
|
+
properties: {}
|
|
40
|
+
};
|
|
41
|
+
for (const [alias, fieldId] of Object.entries(aliases)) {
|
|
42
|
+
const field = metadata.fields.find(({ id }) => id === fieldId) ?? panicConfig(`softr.json user.properties.${alias} names "${fieldId}", which is not a field of the users table.`);
|
|
43
|
+
users.properties[alias] = field;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
23
46
|
return {
|
|
24
|
-
datasources
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
...metadata,
|
|
28
|
-
id: datasource.id,
|
|
29
|
-
tableId: metadata.id,
|
|
30
|
-
name
|
|
31
|
-
};
|
|
32
|
-
})),
|
|
33
|
-
userGroups
|
|
47
|
+
datasources,
|
|
48
|
+
userGroups,
|
|
49
|
+
users
|
|
34
50
|
};
|
|
35
51
|
}
|
|
36
52
|
async function apiFetch(url, token) {
|
|
@@ -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
|
|
@@ -42,9 +43,16 @@ const USER_TIMEZONE_COOKIE = "user-timezone";
|
|
|
42
43
|
function readUserTimezone() {
|
|
43
44
|
return readRequestCookies()[USER_TIMEZONE_COOKIE] ?? null;
|
|
44
45
|
}
|
|
46
|
+
/** What browsers keep of one cookie's name and value. They drop a larger one silently. */
|
|
47
|
+
const COOKIE_MAX_BYTES = 4096;
|
|
45
48
|
function writeCookies(cookies) {
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
const written = cookies.filter((cookie) => {
|
|
50
|
+
const size = cookie.split(";")[0].length;
|
|
51
|
+
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.`);
|
|
52
|
+
return size <= COOKIE_MAX_BYTES;
|
|
53
|
+
});
|
|
54
|
+
if (written.length === 0) return;
|
|
55
|
+
setResponseHeader("Set-Cookie", [...getResponseHeaders().getSetCookie(), ...written]);
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
//#endregion
|
package/dist/formatting.d.ts
CHANGED
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, defineAiAction, defineComments, 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";
|
|
@@ -19,4 +19,4 @@ import { useUserTimezone } from "./timezone.js";
|
|
|
19
19
|
import { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall } from "./pwa.js";
|
|
20
20
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
21
21
|
import { Audience } from "./route/audience.js";
|
|
22
|
-
export { type ActionPermissions, type AiAnswer, type AiFile, type AiGenerateInit, type AiMessage, type AiUsage, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type CommentAttachmentInput, type CommentMention, type CommentThread, type CommentView, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type Page, type PageSettings, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type User, type WeekDay, assertRouteAccess, checkAccess, defineAiAction, defineComments, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
|
22
|
+
export { type ActionPermissions, type AiAnswer, type AiFile, type AiGenerateInit, type AiMessage, type AiUsage, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type CommentAttachmentInput, type CommentMention, type CommentThread, type CommentView, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type Page, type PageSettings, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type SoftrUserProperties, type User, type WeekDay, assertRouteAccess, checkAccess, defineAiAction, defineComments, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, 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_start5 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_start5.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.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";
|
package/dist/route/route.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { panic } from "../assertions.js";
|
|
2
|
-
import { ACCESS_REASONS, checkRule } from "./visibility.js";
|
|
3
2
|
import { softrConfig } from "../softr-config.js";
|
|
3
|
+
import { ACCESS_REASONS, checkRule } from "./visibility.js";
|
|
4
4
|
import { redirect } from "@tanstack/react-router";
|
|
5
5
|
|
|
6
6
|
//#region src/route/route.ts
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
import { throwAtCaller } 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";
|
|
8
|
+
/** The cause is in softr.json, so the app's author can fix it. */
|
|
9
|
+
var SoftrConfigError = class extends Error {
|
|
10
|
+
name = "SoftrConfigError";
|
|
11
|
+
};
|
|
12
|
+
function panicConfig(message) {
|
|
13
|
+
throwAtCaller(panicConfig, new SoftrConfigError(message));
|
|
14
|
+
}
|
|
4
15
|
const AccessBehaviour = z.union([
|
|
5
16
|
z.string(),
|
|
6
17
|
z.object({
|
|
@@ -9,32 +20,49 @@ const AccessBehaviour = z.union([
|
|
|
9
20
|
}),
|
|
10
21
|
z.object({ strategy: z.literal("throw") })
|
|
11
22
|
], { 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
|
-
|
|
23
|
+
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 \"#\"" });
|
|
24
|
+
const SoftrConfigSchema = z.object({
|
|
25
|
+
auth: z.strictObject({
|
|
26
|
+
routes: z.strictObject({
|
|
27
|
+
login: z.strictObject({
|
|
28
|
+
path: AppPath,
|
|
29
|
+
redirectParam: z.string()
|
|
30
|
+
}),
|
|
31
|
+
resetPassword: z.strictObject({
|
|
32
|
+
path: AppPath,
|
|
33
|
+
tokenParam: z.string()
|
|
34
|
+
}),
|
|
35
|
+
acceptInvitation: z.strictObject({
|
|
36
|
+
path: AppPath,
|
|
37
|
+
tokenParam: z.string()
|
|
38
|
+
}),
|
|
39
|
+
magicLink: z.strictObject({
|
|
40
|
+
path: AppPath,
|
|
41
|
+
tokenParam: z.string()
|
|
42
|
+
})
|
|
18
43
|
}),
|
|
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()
|
|
44
|
+
guard: z.strictObject({
|
|
45
|
+
unauthenticated: AccessBehaviour,
|
|
46
|
+
forbidden: AccessBehaviour,
|
|
47
|
+
alreadyAuthenticated: AccessBehaviour
|
|
30
48
|
})
|
|
31
49
|
}),
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
})
|
|
37
|
-
|
|
50
|
+
user: z.strictObject({ properties: z.record(z.string(), z.string()) }).optional()
|
|
51
|
+
});
|
|
52
|
+
function loadSoftrConfig(root) {
|
|
53
|
+
const file = path.join(root, SOFTR_CONFIG_FILE);
|
|
54
|
+
if (!existsSync(file)) panicConfig(`${SOFTR_CONFIG_FILE} is missing from ${root}`);
|
|
55
|
+
const source = readFileSync(file, "utf8");
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(source);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
panicConfig(`${SOFTR_CONFIG_FILE} is not valid JSON: ${error instanceof Error ? error.message : error}`);
|
|
61
|
+
}
|
|
62
|
+
const result = SoftrConfigSchema.safeParse(parsed);
|
|
63
|
+
if (!result.success) panicConfig(`${SOFTR_CONFIG_FILE} does not match the schema:\n\n${z.prettifyError(result.error)}`);
|
|
64
|
+
return result.data;
|
|
65
|
+
}
|
|
38
66
|
|
|
39
67
|
//#endregion
|
|
40
|
-
export {
|
|
68
|
+
export { SOFTR_CONFIG_FILE, SoftrConfigError, loadSoftrConfig, panicConfig };
|
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
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"],
|
|
@@ -12,7 +12,7 @@ description: >-
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: sub-skill
|
|
14
14
|
library: softr-sdk
|
|
15
|
-
library_version: 0.0.
|
|
15
|
+
library_version: 0.0.79-alpha.0
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
**Boundary.** `ai` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineAiAction` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.78
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.78
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**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.
|
|
@@ -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.78
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
**Boundary.** `comments` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler. Importing it into a component fails the build. `useCommentComposer` lives on `@softr/sdk/client` and runs in components. `defineComments` comes from `@softr/sdk` and can be imported anywhere.
|
|
@@ -7,7 +7,7 @@ description: >-
|
|
|
7
7
|
metadata:
|
|
8
8
|
type: sub-skill
|
|
9
9
|
library: softr-sdk
|
|
10
|
-
library_version: 0.0.
|
|
10
|
+
library_version: 0.0.78
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
**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.
|
|
@@ -7,7 +7,7 @@ description: >-
|
|
|
7
7
|
metadata:
|
|
8
8
|
type: sub-skill
|
|
9
9
|
library: softr-sdk
|
|
10
|
-
library_version: 0.0.
|
|
10
|
+
library_version: 0.0.78
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
**Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineIntegration` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.78
|
|
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.
|
|
@@ -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.78
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
**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.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.78
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**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.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.78
|
|
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.
|