@softr/sdk 0.0.68-props.0 → 0.0.69-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/current-user-row.d.ts +20 -0
- package/dist/auth/current-user-row.js +22 -0
- package/dist/auth/methods/profile.d.ts +26 -0
- package/dist/auth/methods/profile.js +52 -0
- package/dist/auth/methods/utils.d.ts +1 -1
- package/dist/auth/platform.d.ts +1 -0
- package/dist/auth/platform.js +20 -33
- package/dist/auth/server.d.ts +4 -0
- package/dist/auth/server.js +5 -1
- package/dist/auth/session.js +12 -2
- package/dist/auth/user.d.ts +5 -8
- package/dist/auth/user.js +2 -1
- package/dist/cli.js +18 -26
- package/dist/codegen/datasource-docs.js +1 -93
- package/dist/codegen/datasource-types.js +1 -16
- package/dist/codegen/fetch-metadata.js +17 -44
- package/dist/codegen/softr-env.js +1 -2
- package/dist/codegen/softr-fields.js +41 -0
- package/dist/cookies.js +2 -10
- package/dist/core/options.d.ts +36 -0
- package/dist/core/options.js +39 -0
- package/dist/core/use-overrides.d.ts +17 -0
- package/dist/core/use-overrides.js +30 -0
- package/dist/datasource/datasource.d.ts +2 -0
- package/dist/datasource/datasource.js +3 -1
- package/dist/datasource/fields.js +3 -2
- package/dist/datasource/record.d.ts +2 -1
- package/dist/default-error-overlay.d.ts +2 -2
- package/dist/fields/format.d.ts +45 -0
- package/dist/fields/format.js +140 -0
- package/dist/formatting.d.ts +4 -4
- package/dist/formatting.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- 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/server.d.ts +2 -1
- package/dist/softr-config.schema.js +24 -45
- package/dist/visibility/route.js +1 -1
- package/dist/vite.js +19 -1
- package/package.json +10 -33
- package/schema/softr.schema.json +0 -13
- package/skills/softr-sdk/SKILL.md +2 -2
- package/skills/softr-sdk/auth/SKILL.md +29 -10
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +5 -24
- package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
- package/skills/softr-sdk/formatting/SKILL.md +19 -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 +13 -1
- package/skills/softr-sdk/record-queries/SKILL.md +21 -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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { User } from "./user.js";
|
|
2
|
+
import { DatasourceAction, ReadShape } from "../action.js";
|
|
3
|
+
import { GetRecord } from "../datasource/record.js";
|
|
4
|
+
import "@tanstack/react-start/server-only";
|
|
5
|
+
|
|
6
|
+
//#region src/auth/current-user-row.d.ts
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The signed-in visitor's own row of the Users table, found by email, or null when they are signed out
|
|
10
|
+
* or the table holds no row for them. For a "mine" filter, an owner set on create, or a page scoped to
|
|
11
|
+
* what the visitor owns: the row's id is what a link to Users holds.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const me = await auth.currentUserRow(context.user, usersQuery, 'email')
|
|
15
|
+
* if (me === null) return []
|
|
16
|
+
* return ds.list(tasks).where((q) => q.assignee.isOneOf([me.id]))
|
|
17
|
+
*/
|
|
18
|
+
declare function currentUserRow<TShape extends ReadShape>(user: User | null, usersQuery: DatasourceAction<TShape>, email: keyof TShape["fields"] & string): Promise<GetRecord<TShape>>;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { currentUserRow };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { makeChain } from "../datasource/fields.js";
|
|
2
|
+
import { listRecords } from "../datasource/record.js";
|
|
3
|
+
import "@tanstack/react-start/server-only";
|
|
4
|
+
|
|
5
|
+
//#region src/auth/current-user-row.ts
|
|
6
|
+
/**
|
|
7
|
+
* The signed-in visitor's own row of the Users table, found by email, or null when they are signed out
|
|
8
|
+
* or the table holds no row for them. For a "mine" filter, an owner set on create, or a page scoped to
|
|
9
|
+
* what the visitor owns: the row's id is what a link to Users holds.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const me = await auth.currentUserRow(context.user, usersQuery, 'email')
|
|
13
|
+
* if (me === null) return []
|
|
14
|
+
* return ds.list(tasks).where((q) => q.assignee.isOneOf([me.id]))
|
|
15
|
+
*/
|
|
16
|
+
async function currentUserRow(user, usersQuery, email) {
|
|
17
|
+
if (user === null) return null;
|
|
18
|
+
return (await listRecords(usersQuery).where(() => makeChain(email).is(user.email)).limit(1)).records[0] ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
export { currentUserRow };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { User } from "../user.js";
|
|
2
|
+
import { AuthError, Result } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
//#region src/auth/methods/profile.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Changes the signed-in visitor's own name, photo, or both. A field left out
|
|
8
|
+
* stays as it is, and `avatar: null` removes the photo.
|
|
9
|
+
*
|
|
10
|
+
* The photo can change only where `user.avatarEditable` is true. Elsewhere a
|
|
11
|
+
* photo answers `avatarNotSupported`, so hide the photo controls instead.
|
|
12
|
+
*
|
|
13
|
+
* Answers with the visitor as they are now. The page keeps showing what it
|
|
14
|
+
* loaded until its route context loads again, so call `router.invalidate()`
|
|
15
|
+
* after a success.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const result = await auth.updateProfile({ fullName: 'Ann Lee' })
|
|
19
|
+
* const result = await auth.updateProfile({ avatar: file })
|
|
20
|
+
*/
|
|
21
|
+
declare function updateProfile(changes: {
|
|
22
|
+
fullName?: string;
|
|
23
|
+
avatar?: File | null;
|
|
24
|
+
}): Promise<Result<User, AuthError>>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { updateProfile };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { assert, panic } from "../../assertions.js";
|
|
2
|
+
import { sessionConfig } from "../config.js";
|
|
3
|
+
import { clearSessionData, readSessionToken } from "../session.js";
|
|
4
|
+
import { makePlatform } from "../platform.js";
|
|
5
|
+
import { authError } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
//#region src/auth/methods/profile.ts
|
|
8
|
+
/**
|
|
9
|
+
* Changes the signed-in visitor's own name, photo, or both. A field left out
|
|
10
|
+
* stays as it is, and `avatar: null` removes the photo.
|
|
11
|
+
*
|
|
12
|
+
* The photo can change only where `user.avatarEditable` is true. Elsewhere a
|
|
13
|
+
* photo answers `avatarNotSupported`, so hide the photo controls instead.
|
|
14
|
+
*
|
|
15
|
+
* Answers with the visitor as they are now. The page keeps showing what it
|
|
16
|
+
* loaded until its route context loads again, so call `router.invalidate()`
|
|
17
|
+
* after a success.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const result = await auth.updateProfile({ fullName: 'Ann Lee' })
|
|
21
|
+
* const result = await auth.updateProfile({ avatar: file })
|
|
22
|
+
*/
|
|
23
|
+
async function updateProfile(changes) {
|
|
24
|
+
assert(changes.fullName !== void 0 || changes.avatar !== void 0, "updateProfile needs a name or a photo to change");
|
|
25
|
+
const token = readSessionToken();
|
|
26
|
+
if (token === null) return authError("notAuthenticated", "Nobody is signed in.");
|
|
27
|
+
const form = new FormData();
|
|
28
|
+
if (changes.fullName !== void 0) form.set("fullName", changes.fullName);
|
|
29
|
+
if (changes.avatar === null) form.set("removeAvatar", "true");
|
|
30
|
+
if (changes.avatar) form.set("avatar", changes.avatar);
|
|
31
|
+
const result = await makePlatform(sessionConfig().applicationId).updateProfile(token, form);
|
|
32
|
+
if (!result.ok) {
|
|
33
|
+
switch (result.status) {
|
|
34
|
+
case 400: return authError("invalidName", result.message);
|
|
35
|
+
case 401: return authError("notAuthenticated", result.message);
|
|
36
|
+
case 409: return authError("avatarNotSupported", result.message);
|
|
37
|
+
case 413: return authError("fileTooLarge", result.message);
|
|
38
|
+
case 415: return authError("notAnImage", result.message);
|
|
39
|
+
case 429: return authError("tooManyAttempts", result.message);
|
|
40
|
+
}
|
|
41
|
+
if (result.status >= 500) panic(`Expected the profile to update, but got ${result.status}: ` + result.message);
|
|
42
|
+
throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID.`);
|
|
43
|
+
}
|
|
44
|
+
clearSessionData();
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
data: result.data
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { updateProfile };
|
|
@@ -10,6 +10,6 @@ type AuthError = {
|
|
|
10
10
|
code: AuthErrorCode;
|
|
11
11
|
message: string;
|
|
12
12
|
};
|
|
13
|
-
type AuthErrorCode = "invalidCredentials" | "tooManyAttempts" | "emailTaken" | "invalidDetails" | "invalidCode" | "notAuthenticated" | "emailDomainNotAllowed" | "linkExpired";
|
|
13
|
+
type AuthErrorCode = "invalidCredentials" | "tooManyAttempts" | "emailTaken" | "invalidDetails" | "invalidCode" | "notAuthenticated" | "emailDomainNotAllowed" | "linkExpired" | "invalidName" | "fileTooLarge" | "notAnImage" | "avatarNotSupported";
|
|
14
14
|
//#endregion
|
|
15
15
|
export { AuthError, Result };
|
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 { userSchema } from "./user.js";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
|
|
8
8
|
//#region src/auth/platform.ts
|
|
@@ -33,30 +33,10 @@ 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
|
-
});
|
|
56
36
|
return {
|
|
57
37
|
authSettings: () => request(`/v1/vibe/applications/${applicationId}/users/auth-settings`, { method: "GET" }, authSettingsSchema),
|
|
58
38
|
session: async (token) => {
|
|
59
|
-
const result = await request(`/v1/vibe/applications/${applicationId}/users/session
|
|
39
|
+
const result = await request(`/v1/vibe/applications/${applicationId}/users/session`, {
|
|
60
40
|
method: "GET",
|
|
61
41
|
token
|
|
62
42
|
}, z.object({
|
|
@@ -66,7 +46,7 @@ function makePlatform(applicationId) {
|
|
|
66
46
|
if (result.status === 404) panic(`Application ${applicationId} not found. Check SOFTR_APPLICATION_ID.`);
|
|
67
47
|
return result;
|
|
68
48
|
},
|
|
69
|
-
tokens: (credentials) => request(`/v1/vibe/applications/${applicationId}/users/tokens
|
|
49
|
+
tokens: (credentials) => request(`/v1/vibe/applications/${applicationId}/users/tokens`, {
|
|
70
50
|
method: "POST",
|
|
71
51
|
json: credentials
|
|
72
52
|
}, z.object({
|
|
@@ -89,7 +69,7 @@ function makePlatform(applicationId) {
|
|
|
89
69
|
method: "POST",
|
|
90
70
|
json: { email }
|
|
91
71
|
}, z.null()),
|
|
92
|
-
signInWithCode: (credentials) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-code
|
|
72
|
+
signInWithCode: (credentials) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-code`, {
|
|
93
73
|
method: "POST",
|
|
94
74
|
json: credentials
|
|
95
75
|
}, z.object({
|
|
@@ -108,7 +88,7 @@ function makePlatform(applicationId) {
|
|
|
108
88
|
challengeToken: z.string()
|
|
109
89
|
}).nullish()
|
|
110
90
|
})),
|
|
111
|
-
signInWithGoogle: (body) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-google
|
|
91
|
+
signInWithGoogle: (body) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-google`, {
|
|
112
92
|
method: "POST",
|
|
113
93
|
json: body
|
|
114
94
|
}, z.object({
|
|
@@ -127,7 +107,7 @@ function makePlatform(applicationId) {
|
|
|
127
107
|
challengeToken: z.string()
|
|
128
108
|
}).nullish()
|
|
129
109
|
})),
|
|
130
|
-
signInWithMagicLink: (body) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-magic-link
|
|
110
|
+
signInWithMagicLink: (body) => request(`/v1/vibe/applications/${applicationId}/users/tokens/with-magic-link`, {
|
|
131
111
|
method: "POST",
|
|
132
112
|
json: body
|
|
133
113
|
}, z.object({
|
|
@@ -154,7 +134,7 @@ function makePlatform(applicationId) {
|
|
|
154
134
|
method: "POST",
|
|
155
135
|
json: body
|
|
156
136
|
}, z.object({ email: z.string() })),
|
|
157
|
-
setPassword: (body) => request(`/v1/vibe/applications/${applicationId}/users/set-password
|
|
137
|
+
setPassword: (body) => request(`/v1/vibe/applications/${applicationId}/users/set-password`, {
|
|
158
138
|
method: "POST",
|
|
159
139
|
json: body
|
|
160
140
|
}, z.object({
|
|
@@ -177,7 +157,7 @@ function makePlatform(applicationId) {
|
|
|
177
157
|
method: "POST",
|
|
178
158
|
json: body
|
|
179
159
|
}, z.null()),
|
|
180
|
-
signUpWithCode: (body) => request(`/v1/vibe/applications/${applicationId}/users/sign-up/with-code
|
|
160
|
+
signUpWithCode: (body) => request(`/v1/vibe/applications/${applicationId}/users/sign-up/with-code`, {
|
|
181
161
|
method: "POST",
|
|
182
162
|
json: body
|
|
183
163
|
}, z.object({
|
|
@@ -196,7 +176,7 @@ function makePlatform(applicationId) {
|
|
|
196
176
|
challengeToken: z.string()
|
|
197
177
|
}).nullish()
|
|
198
178
|
})),
|
|
199
|
-
signUpWithPassword: (body) => request(`/v1/vibe/applications/${applicationId}/users/sign-up/with-password
|
|
179
|
+
signUpWithPassword: (body) => request(`/v1/vibe/applications/${applicationId}/users/sign-up/with-password`, {
|
|
200
180
|
method: "POST",
|
|
201
181
|
json: body
|
|
202
182
|
}, z.object({
|
|
@@ -222,7 +202,7 @@ function makePlatform(applicationId) {
|
|
|
222
202
|
otpauthUri: z.string(),
|
|
223
203
|
qrPngBase64: z.string()
|
|
224
204
|
})),
|
|
225
|
-
authenticatorSetupComplete: (token, code) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/setup/complete
|
|
205
|
+
authenticatorSetupComplete: (token, code) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/setup/complete`, {
|
|
226
206
|
method: "POST",
|
|
227
207
|
token,
|
|
228
208
|
json: { code }
|
|
@@ -234,7 +214,7 @@ function makePlatform(applicationId) {
|
|
|
234
214
|
}),
|
|
235
215
|
backupCodes: z.array(z.string())
|
|
236
216
|
})),
|
|
237
|
-
authenticatorSetupDismiss: (token) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/setup/dismiss
|
|
217
|
+
authenticatorSetupDismiss: (token) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/setup/dismiss`, {
|
|
238
218
|
method: "POST",
|
|
239
219
|
token
|
|
240
220
|
}, z.object({
|
|
@@ -253,7 +233,7 @@ function makePlatform(applicationId) {
|
|
|
253
233
|
challengeToken: z.string()
|
|
254
234
|
}).nullish()
|
|
255
235
|
})),
|
|
256
|
-
authenticatorVerify: (token, body) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/totp/verify
|
|
236
|
+
authenticatorVerify: (token, body) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/totp/verify`, {
|
|
257
237
|
method: "POST",
|
|
258
238
|
token,
|
|
259
239
|
json: body
|
|
@@ -287,6 +267,11 @@ function makePlatform(applicationId) {
|
|
|
287
267
|
token,
|
|
288
268
|
json: { code }
|
|
289
269
|
}, z.null()),
|
|
270
|
+
updateProfile: (token, form) => request(`/v1/vibe/applications/${applicationId}/users/me/profile`, {
|
|
271
|
+
method: "PATCH",
|
|
272
|
+
token,
|
|
273
|
+
form
|
|
274
|
+
}, userSchema),
|
|
290
275
|
signOut: (token) => request(`/v1/applications/${applicationId}/users/sign-out`, {
|
|
291
276
|
method: "POST",
|
|
292
277
|
token
|
|
@@ -295,6 +280,7 @@ function makePlatform(applicationId) {
|
|
|
295
280
|
}
|
|
296
281
|
const errorSchema = z.object({ message: z.string() });
|
|
297
282
|
async function request(path, init, schema, parse = (response$1) => response$1.json()) {
|
|
283
|
+
assert(init.json === void 0 || init.form === void 0, "A platform request carries a JSON body or a form, not both");
|
|
298
284
|
const response = await fetch(`${await platformApiUrl()}${path}`, {
|
|
299
285
|
method: init.method,
|
|
300
286
|
headers: {
|
|
@@ -302,7 +288,8 @@ async function request(path, init, schema, parse = (response$1) => response$1.js
|
|
|
302
288
|
...init.json === void 0 ? {} : { "content-type": "application/json" },
|
|
303
289
|
...init.token ? { authorization: `Bearer ${init.token}` } : {}
|
|
304
290
|
},
|
|
305
|
-
...init.json === void 0 ? {} : { body: JSON.stringify(init.json) }
|
|
291
|
+
...init.json === void 0 ? {} : { body: JSON.stringify(init.json) },
|
|
292
|
+
...init.form === void 0 ? {} : { body: init.form }
|
|
306
293
|
});
|
|
307
294
|
if (!response.ok) {
|
|
308
295
|
const error = errorSchema.safeParse(await response.json().catch(() => null));
|
package/dist/auth/server.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { checkPasswordLink, requestPasswordReset, setPasswordWithLink } from "./
|
|
|
6
6
|
import { settings } from "./methods/settings.js";
|
|
7
7
|
import { confirmSignUp, signUp } from "./methods/signup.js";
|
|
8
8
|
import { signInWithGoogle } from "./methods/social.js";
|
|
9
|
+
import { updateProfile } from "./methods/profile.js";
|
|
10
|
+
import { currentUserRow } from "./current-user-row.js";
|
|
9
11
|
import "@tanstack/react-start/server-only";
|
|
10
12
|
|
|
11
13
|
//#region src/auth/server.d.ts
|
|
@@ -33,6 +35,8 @@ declare const auth: {
|
|
|
33
35
|
resetAuthenticator: typeof resetAuthenticator;
|
|
34
36
|
removeAuthenticator: typeof removeAuthenticator;
|
|
35
37
|
settings: typeof settings;
|
|
38
|
+
updateProfile: typeof updateProfile;
|
|
39
|
+
currentUserRow: typeof currentUserRow;
|
|
36
40
|
};
|
|
37
41
|
//#endregion
|
|
38
42
|
export { auth };
|
package/dist/auth/server.js
CHANGED
|
@@ -6,6 +6,8 @@ import { signIn, signOut } from "./methods/password.js";
|
|
|
6
6
|
import { checkPasswordLink, requestPasswordReset, setPasswordWithLink } from "./methods/passwordLink.js";
|
|
7
7
|
import { confirmSignUp, signUp } from "./methods/signup.js";
|
|
8
8
|
import { signInWithGoogle } from "./methods/social.js";
|
|
9
|
+
import { updateProfile } from "./methods/profile.js";
|
|
10
|
+
import { currentUserRow } from "./current-user-row.js";
|
|
9
11
|
import "@tanstack/react-start/server-only";
|
|
10
12
|
|
|
11
13
|
//#region src/auth/server.ts
|
|
@@ -31,7 +33,9 @@ const auth = {
|
|
|
31
33
|
sendAuthenticatorResetCode,
|
|
32
34
|
resetAuthenticator,
|
|
33
35
|
removeAuthenticator,
|
|
34
|
-
settings
|
|
36
|
+
settings,
|
|
37
|
+
updateProfile,
|
|
38
|
+
currentUserRow
|
|
35
39
|
};
|
|
36
40
|
|
|
37
41
|
//#endregion
|
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";
|
|
6
5
|
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
7
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
7
|
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";
|
|
@@ -118,6 +118,16 @@ function clearSession() {
|
|
|
118
118
|
PENDING_AUTH
|
|
119
119
|
]);
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Drops the cached visitor and leaves the session signed in, so the next
|
|
123
|
+
* request needing a visitor re-reads one from the platform.
|
|
124
|
+
*
|
|
125
|
+
* For a mutation to what the visitor's own record holds — their name, their
|
|
126
|
+
* groups — which the cache would otherwise answer stale for up to five minutes.
|
|
127
|
+
*/
|
|
128
|
+
function clearSessionData() {
|
|
129
|
+
clearCookies([SESSION_DATA]);
|
|
130
|
+
}
|
|
121
131
|
const standInToken = new AsyncLocalStorage();
|
|
122
132
|
const sessionDataSchema = z.object({
|
|
123
133
|
user: userSchema.nullable(),
|
|
@@ -210,4 +220,4 @@ function openEnvelope(raw, purpose) {
|
|
|
210
220
|
}
|
|
211
221
|
|
|
212
222
|
//#endregion
|
|
213
|
-
export { clearPendingAuth, clearSession, makePendingAuthCookie, makeSessionDataCookie, makeSessionTokenCookie, readPendingAuthToken, readSessionData, readSessionToken, withSessionToken };
|
|
223
|
+
export { clearPendingAuth, clearSession, clearSessionData, makePendingAuthCookie, makeSessionDataCookie, makeSessionTokenCookie, readPendingAuthToken, readSessionData, readSessionToken, withSessionToken };
|
package/dist/auth/user.d.ts
CHANGED
|
@@ -13,19 +13,16 @@ 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 {}
|
|
22
16
|
/** The signed-in visitor. */
|
|
23
17
|
type User = {
|
|
24
18
|
id: string;
|
|
25
19
|
email: string;
|
|
26
20
|
fullName: string;
|
|
27
21
|
userGroupIds: Array<SoftrUserGroupId>;
|
|
28
|
-
|
|
22
|
+
/** Their photo, or `null` when they have none. Show their initials then. */
|
|
23
|
+
avatarUrl: string | null;
|
|
24
|
+
/** Whether `auth.updateProfile` can change their photo in this app. */
|
|
25
|
+
avatarEditable: boolean;
|
|
29
26
|
};
|
|
30
27
|
//#endregion
|
|
31
|
-
export { SoftrUserGroupId, SoftrUserGroups,
|
|
28
|
+
export { SoftrUserGroupId, SoftrUserGroups, User };
|
package/dist/auth/user.js
CHANGED
|
@@ -6,7 +6,8 @@ const userSchema = z.object({
|
|
|
6
6
|
email: z.string(),
|
|
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
11
|
});
|
|
11
12
|
|
|
12
13
|
//#endregion
|
package/dist/cli.js
CHANGED
|
@@ -2,26 +2,30 @@
|
|
|
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";
|
|
6
5
|
import { renderUserGroupsDoc } from "./codegen/user-groups.js";
|
|
7
6
|
import { renderSoftrEnv } from "./codegen/softr-env.js";
|
|
7
|
+
import { renderSoftrFields } from "./codegen/softr-fields.js";
|
|
8
8
|
import { writeIfChanged } from "./codegen/write-if-changed.js";
|
|
9
9
|
import { writeDatasourceDocs } from "./codegen/write-datasource-docs.js";
|
|
10
|
-
import {
|
|
11
|
-
import { rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { writeFileSync } from "node:fs";
|
|
12
11
|
import path from "node:path";
|
|
13
12
|
import { parseArgs } from "node:util";
|
|
14
13
|
|
|
15
14
|
//#region src/cli.ts
|
|
16
15
|
const OUTPUT = "softr-env.d.ts";
|
|
16
|
+
const FIELDS_OUTPUT = "src/softr-fields.ts";
|
|
17
17
|
const DOCS_DIR = "docs/datasources";
|
|
18
18
|
const USER_GROUPS_DOC = "docs/user-groups.md";
|
|
19
|
-
const USER_PROPERTIES_DOC = "docs/user-properties.md";
|
|
20
19
|
async function generate({ json }) {
|
|
21
20
|
if (json) console.info = console.error;
|
|
22
|
-
const input = await fetchSoftrEnvInput(metadataSourceFromEnv()
|
|
21
|
+
const input = await fetchSoftrEnvInput(metadataSourceFromEnv());
|
|
23
22
|
writeFileSync(path.resolve(process.cwd(), OUTPUT), renderSoftrEnv(input, version), "utf8");
|
|
24
23
|
const report = writeDocs(input);
|
|
24
|
+
report.docs.push({
|
|
25
|
+
subject: "field shapes",
|
|
26
|
+
path: FIELDS_OUTPUT,
|
|
27
|
+
changed: writeIfChanged(path.resolve(process.cwd(), FIELDS_OUTPUT), renderSoftrFields(input.datasources, version))
|
|
28
|
+
});
|
|
25
29
|
if (json) {
|
|
26
30
|
process.stdout.write(`${JSON.stringify(report)}\n`);
|
|
27
31
|
return;
|
|
@@ -29,30 +33,18 @@ async function generate({ json }) {
|
|
|
29
33
|
printReport(report);
|
|
30
34
|
}
|
|
31
35
|
function writeDocs(input) {
|
|
32
|
-
if (input.users === null) rmSync(path.resolve(process.cwd(), USER_PROPERTIES_DOC), { force: true });
|
|
33
36
|
const datasourceDocs = writeDatasourceDocs(input.datasources, path.resolve(process.cwd(), DOCS_DIR));
|
|
34
37
|
return {
|
|
35
38
|
types: OUTPUT,
|
|
36
|
-
docs: [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
],
|
|
39
|
+
docs: [{
|
|
40
|
+
subject: "datasources",
|
|
41
|
+
path: `${DOCS_DIR}/README.md`,
|
|
42
|
+
changed: datasourceDocs.changed
|
|
43
|
+
}, {
|
|
44
|
+
subject: "user groups",
|
|
45
|
+
path: USER_GROUPS_DOC,
|
|
46
|
+
changed: writeIfChanged(path.resolve(process.cwd(), USER_GROUPS_DOC), renderUserGroupsDoc(input.userGroups))
|
|
47
|
+
}],
|
|
56
48
|
datasources: input.datasources.map(({ id, name }) => ({
|
|
57
49
|
id,
|
|
58
50
|
name,
|
|
@@ -18,98 +18,6 @@ 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
|
-
}
|
|
113
21
|
/** Two trailing spaces are a markdown hard break, which keeps the block one paragraph. */
|
|
114
22
|
function summary(datasource, context) {
|
|
115
23
|
const lines = [`Datasource ID: ${datasource.id}`];
|
|
@@ -407,4 +315,4 @@ function words(value) {
|
|
|
407
315
|
}
|
|
408
316
|
|
|
409
317
|
//#endregion
|
|
410
|
-
export { renderDatasourceDoc, renderDatasourceIndex
|
|
318
|
+
export { renderDatasourceDoc, renderDatasourceIndex };
|
|
@@ -251,21 +251,6 @@ 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
|
-
}
|
|
269
254
|
function renderRegistry(datasources, context) {
|
|
270
255
|
return [
|
|
271
256
|
"interface SoftrDatasources {",
|
|
@@ -509,4 +494,4 @@ function indent(block) {
|
|
|
509
494
|
}
|
|
510
495
|
|
|
511
496
|
//#endregion
|
|
512
|
-
export { choiceEntries, fieldTypeLiteral, isWritable, linkedDatasource, option, readExample, renderDatasourceTypes,
|
|
497
|
+
export { choiceEntries, fieldTypeLiteral, isWritable, linkedDatasource, option, readExample, renderDatasourceTypes, resolvedShape, scalarExample, underlyingType };
|