@softr/sdk 0.0.68-props.0 → 0.0.68
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/platform.d.ts +1 -0
- package/dist/auth/platform.js +13 -33
- package/dist/auth/server.d.ts +2 -0
- package/dist/auth/server.js +3 -1
- package/dist/auth/session.js +1 -1
- package/dist/auth/user.d.ts +1 -8
- package/dist/auth/user.js +1 -2
- 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/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.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 +1 -1
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +3 -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 };
|
package/dist/auth/platform.d.ts
CHANGED
package/dist/auth/platform.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { 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
|
package/dist/auth/server.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ 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 { currentUserRow } from "./current-user-row.js";
|
|
9
10
|
import "@tanstack/react-start/server-only";
|
|
10
11
|
|
|
11
12
|
//#region src/auth/server.d.ts
|
|
@@ -33,6 +34,7 @@ declare const auth: {
|
|
|
33
34
|
resetAuthenticator: typeof resetAuthenticator;
|
|
34
35
|
removeAuthenticator: typeof removeAuthenticator;
|
|
35
36
|
settings: typeof settings;
|
|
37
|
+
currentUserRow: typeof currentUserRow;
|
|
36
38
|
};
|
|
37
39
|
//#endregion
|
|
38
40
|
export { auth };
|
package/dist/auth/server.js
CHANGED
|
@@ -6,6 +6,7 @@ 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 { currentUserRow } from "./current-user-row.js";
|
|
9
10
|
import "@tanstack/react-start/server-only";
|
|
10
11
|
|
|
11
12
|
//#region src/auth/server.ts
|
|
@@ -31,7 +32,8 @@ const auth = {
|
|
|
31
32
|
sendAuthenticatorResetCode,
|
|
32
33
|
resetAuthenticator,
|
|
33
34
|
removeAuthenticator,
|
|
34
|
-
settings
|
|
35
|
+
settings,
|
|
36
|
+
currentUserRow
|
|
35
37
|
};
|
|
36
38
|
|
|
37
39
|
//#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";
|
package/dist/auth/user.d.ts
CHANGED
|
@@ -13,19 +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 {}
|
|
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
|
-
properties: SoftrUserProperties;
|
|
29
22
|
};
|
|
30
23
|
//#endregion
|
|
31
|
-
export { SoftrUserGroupId, SoftrUserGroups,
|
|
24
|
+
export { SoftrUserGroupId, SoftrUserGroups, User };
|
package/dist/auth/user.js
CHANGED
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 };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { panic } from "../assertions.js";
|
|
2
2
|
import { fetch } from "../fetch.js";
|
|
3
3
|
|
|
4
4
|
//#region src/codegen/fetch-metadata.ts
|
|
@@ -17,54 +17,27 @@ function metadataSourceFromEnv(env = process.env) {
|
|
|
17
17
|
token: env.SOFTR_STUDIO_TOKEN
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
-
async function fetchSoftrEnvInput(source
|
|
20
|
+
async function fetchSoftrEnvInput(source) {
|
|
21
21
|
const { studioApiUrl, datasourceApiUrl, applicationId, token } = source;
|
|
22
22
|
const [registered, userGroups] = await Promise.all([apiFetch(`${studioApiUrl}/v1/applications/${applicationId}/datasources`, token), apiFetch(`${studioApiUrl}/v1/applications/${applicationId}/access-control/user-groups`, token)]);
|
|
23
|
-
const datasources = await Promise.all(registered.map(async ({ name, datasource }) => {
|
|
24
|
-
const metadata = await apiFetch(`${datasourceApiUrl}/v1/datasource/vibe/studio/applications/${applicationId}/datasources/${datasource.id}/metadata`, token);
|
|
25
|
-
return {
|
|
26
|
-
...metadata,
|
|
27
|
-
id: datasource.id,
|
|
28
|
-
tableId: metadata.id,
|
|
29
|
-
name
|
|
30
|
-
};
|
|
31
|
-
}));
|
|
32
|
-
let users = null;
|
|
33
|
-
if (Object.keys(aliases).length > 0) {
|
|
34
|
-
const { payload } = await apiFetch(`${studioApiUrl}/v1/applications/${applicationId}`, token);
|
|
35
|
-
const connection = payload.user_connection?.datasource ?? panic("softr.json declares user.properties, but this app has no users table.");
|
|
36
|
-
const metadata = await apiFetch(`${datasourceApiUrl}/v1/datasource/${applicationId}/${connection.integration_id}/metadata`, token, {
|
|
37
|
-
datasourceType: connection.datasource_integration_type,
|
|
38
|
-
resourceIdentifier: { keys: connection.resource_identifier.keys }
|
|
39
|
-
});
|
|
40
|
-
users = {
|
|
41
|
-
datasourceId: connection.id,
|
|
42
|
-
tableId: metadata.id,
|
|
43
|
-
name: metadata.name,
|
|
44
|
-
properties: {}
|
|
45
|
-
};
|
|
46
|
-
for (const [alias, fieldId] of Object.entries(aliases)) {
|
|
47
|
-
const field = metadata.fields.find(({ id }) => id === fieldId) ?? panic(`softr.json user.properties.${alias} names "${fieldId}", which is not a field of the users table.`);
|
|
48
|
-
assert(field.type !== "ATTACHMENT" && field.type !== "LONG_TEXT" && !(field.allowMultipleEntries && (field.type === "LINKED_RECORD" || field.type === "LOOKUP")), `softr.json user.properties.${alias} names "${field.name}", a ${field.allowMultipleEntries ? "multiple " : ""}${field.type} field, which can be too big to read with the signed-in user. Remove it, and read it with defineQuery on the users table instead.`);
|
|
49
|
-
users.properties[alias] = field;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
23
|
return {
|
|
53
|
-
datasources,
|
|
54
|
-
|
|
55
|
-
|
|
24
|
+
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
|
+
userGroups
|
|
56
34
|
};
|
|
57
35
|
}
|
|
58
|
-
async function apiFetch(url, token
|
|
59
|
-
const response = await fetch(url, {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
...body === void 0 ? {} : { "content-type": "application/json" },
|
|
64
|
-
Authorization: `Bearer ${token}`
|
|
65
|
-
},
|
|
66
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
67
|
-
}).catch(() => panic(`Could not reach ${url}`));
|
|
36
|
+
async function apiFetch(url, token) {
|
|
37
|
+
const response = await fetch(url, { headers: {
|
|
38
|
+
accept: "application/json",
|
|
39
|
+
Authorization: `Bearer ${token}`
|
|
40
|
+
} }).catch(() => panic(`Could not reach ${url}`));
|
|
68
41
|
if (!response.ok) panic(`${url} answered ${response.status}`);
|
|
69
42
|
return await response.json();
|
|
70
43
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderDatasourceTypes
|
|
1
|
+
import { renderDatasourceTypes } from "./datasource-types.js";
|
|
2
2
|
import { renderUserGroups } from "./user-groups.js";
|
|
3
3
|
|
|
4
4
|
//#region src/codegen/softr-env.ts
|
|
@@ -15,7 +15,6 @@ 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))],
|
|
19
18
|
"}",
|
|
20
19
|
""
|
|
21
20
|
].join("\n");
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/codegen/softr-fields.ts
|
|
2
|
+
/**
|
|
3
|
+
* Renders the app's `src/softr-fields.ts`: every registered datasource's fields by id, with the type
|
|
4
|
+
* and options a value's text depends on, registered with the SDK as a side effect of being imported.
|
|
5
|
+
* The root route imports it once, and `formatValue(query, alias, value, formatting)` then formats any
|
|
6
|
+
* value a query read by its field's own type.
|
|
7
|
+
*/
|
|
8
|
+
function renderSoftrFields(datasources, sdkVersion) {
|
|
9
|
+
const entries = [...datasources].sort((a, b) => a.id.localeCompare(b.id)).map((datasource) => {
|
|
10
|
+
const fields = [...datasource.fields].sort((a, b) => a.id.localeCompare(b.id)).map((field) => {
|
|
11
|
+
const shape = { type: field.type };
|
|
12
|
+
if (field.options !== null && field.options !== void 0 && Object.keys(field.options).length > 0) shape.options = field.options;
|
|
13
|
+
if (field.allowMultipleEntries) shape.allowMultipleEntries = true;
|
|
14
|
+
return ` ${JSON.stringify(field.id)}: ${JSON.stringify(shape)},`;
|
|
15
|
+
});
|
|
16
|
+
return [
|
|
17
|
+
` // ${datasource.name}`,
|
|
18
|
+
` ${JSON.stringify(datasource.id)}: {`,
|
|
19
|
+
...fields,
|
|
20
|
+
" },"
|
|
21
|
+
].join("\n");
|
|
22
|
+
});
|
|
23
|
+
return [
|
|
24
|
+
`// Generated by @softr/sdk ${sdkVersion}. Do not edit: the SDK's codegen rewrites it after a schema change.`,
|
|
25
|
+
"//",
|
|
26
|
+
"// The type and options of every field, by datasource and field id, for `formatValue` and",
|
|
27
|
+
"// `fieldShape` from @softr/sdk. Imported once, in the root route, it registers them with the SDK.",
|
|
28
|
+
"import { registerSoftrFields } from \"@softr/sdk\";",
|
|
29
|
+
"import type { SoftrFieldRegistry } from \"@softr/sdk\";",
|
|
30
|
+
"",
|
|
31
|
+
"export const softrFields = {",
|
|
32
|
+
...entries,
|
|
33
|
+
"} as const satisfies SoftrFieldRegistry;",
|
|
34
|
+
"",
|
|
35
|
+
"registerSoftrFields(softrFields);",
|
|
36
|
+
""
|
|
37
|
+
].join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { renderSoftrFields };
|
package/dist/cookies.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { softAssert } from "./assertions.js";
|
|
2
1
|
import { getRequestHeader, getResponseHeaders, setResponseHeader } from "@tanstack/react-start/server";
|
|
3
2
|
|
|
4
3
|
//#region src/cookies.ts
|
|
@@ -39,16 +38,9 @@ const USER_TIMEZONE_COOKIE = "user-timezone";
|
|
|
39
38
|
function readUserTimezone() {
|
|
40
39
|
return readRequestCookies()[USER_TIMEZONE_COOKIE] ?? null;
|
|
41
40
|
}
|
|
42
|
-
/** What browsers keep of one cookie's name and value. They drop a larger one silently. */
|
|
43
|
-
const COOKIE_MAX_BYTES = 4096;
|
|
44
41
|
function writeCookies(cookies) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
softAssert(size <= COOKIE_MAX_BYTES, () => `The ${cookie.slice(0, cookie.indexOf("="))} cookie is ${size} bytes, over the ${COOKIE_MAX_BYTES} a browser keeps. It was not written.`);
|
|
48
|
-
return size <= COOKIE_MAX_BYTES;
|
|
49
|
-
});
|
|
50
|
-
if (written.length === 0) return;
|
|
51
|
-
setResponseHeader("Set-Cookie", [...getResponseHeaders().getSetCookie(), ...written]);
|
|
42
|
+
if (cookies.length === 0) return;
|
|
43
|
+
setResponseHeader("Set-Cookie", [...getResponseHeaders().getSetCookie(), ...cookies]);
|
|
52
44
|
}
|
|
53
45
|
|
|
54
46
|
//#endregion
|