@softr/sdk 0.0.78-alpha.2 → 0.0.78
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 -34
- package/dist/action.js +1 -12
- 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 +3 -4
- package/dist/index.js +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/server.d.ts +1 -2
- package/dist/server.js +1 -2
- 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 +4 -12
- 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 +4 -4
- 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/core/ai.d.ts +0 -76
- package/dist/core/ai.js +0 -64
- package/skills/softr-sdk/ai/SKILL.md +0 -161
package/dist/action.d.ts
CHANGED
|
@@ -264,38 +264,5 @@ type CommentsDefinition<TId extends string> = {
|
|
|
264
264
|
* const thread = await comments.list(orderComments, { recordId })
|
|
265
265
|
*/
|
|
266
266
|
declare function defineComments<const TId extends string>(definition: CommentsDefinition<TId>): CommentsAction<TId>;
|
|
267
|
-
type AiResource = {
|
|
268
|
-
type: "AI";
|
|
269
|
-
model: string;
|
|
270
|
-
};
|
|
271
|
-
type AiAction<TId extends string = string> = Action<AiResource> & {
|
|
272
|
-
id: TId;
|
|
273
|
-
};
|
|
274
|
-
type AiDefinition<TId extends string> = {
|
|
275
|
-
id: TId;
|
|
276
|
-
model: string;
|
|
277
|
-
permissions: ActionPermissions;
|
|
278
|
-
};
|
|
279
|
-
/**
|
|
280
|
-
* Declares an AI action: a model call run by Softr on its own provider keys and billed to the
|
|
281
|
-
* workspace's AI credits, so nothing has to be connected. `model` is the id of a model from
|
|
282
|
-
* Softr's AI catalog, chosen when the app is built: read the catalog with the `list_ai_models`
|
|
283
|
-
* tool and copy the id exactly. An id outside the catalog is refused when the action is saved.
|
|
284
|
-
*
|
|
285
|
-
* Every call spends the workspace's credits, so keep the audience to signed-in users unless a
|
|
286
|
-
* public page really needs the call.
|
|
287
|
-
*
|
|
288
|
-
* Pass the returned object to `ai.generate()` or `ai.stream()` at runtime.
|
|
289
|
-
*
|
|
290
|
-
* @example
|
|
291
|
-
* const summarizeDeal = defineAiAction({
|
|
292
|
-
* id: "summarizeDeal",
|
|
293
|
-
* model: "anthropic-claude-sonnet-4-6",
|
|
294
|
-
* permissions: { userGroupTypes: ["LOGGED_IN_USERS"] },
|
|
295
|
-
* })
|
|
296
|
-
*
|
|
297
|
-
* const answer = await ai.generate(summarizeDeal, { system: "...", prompt: facts })
|
|
298
|
-
*/
|
|
299
|
-
declare function defineAiAction<const TId extends string>(definition: AiDefinition<TId>): AiAction<TId>;
|
|
300
267
|
//#endregion
|
|
301
|
-
export { ActionData, ActionPermissions, ActionRecord, ActionShape,
|
|
268
|
+
export { ActionData, ActionPermissions, ActionRecord, ActionShape, CommentsAction, DatasourceAction, FieldTypeOf, IntegrationAction, ReadOf, ReadShape, SoftrDatasources, WorkflowAction, defineComments, defineIntegration, defineMutation, defineQuery, defineWorkflow };
|
package/dist/action.js
CHANGED
|
@@ -81,17 +81,6 @@ function isDatasourceAction(value) {
|
|
|
81
81
|
const resource = candidate.resource;
|
|
82
82
|
return typeof candidate.id === "string" && typeof resource === "object" && resource !== null && resource.type === "DATASOURCE";
|
|
83
83
|
}
|
|
84
|
-
function defineAiAction(definition) {
|
|
85
|
-
const { id, model, permissions } = definition;
|
|
86
|
-
return {
|
|
87
|
-
id,
|
|
88
|
-
resource: {
|
|
89
|
-
type: "AI",
|
|
90
|
-
model
|
|
91
|
-
},
|
|
92
|
-
permissions
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
84
|
|
|
96
85
|
//#endregion
|
|
97
|
-
export {
|
|
86
|
+
export { defineComments, defineIntegration, defineMutation, defineQuery, defineWorkflow, isDatasourceAction };
|
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
|