@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.
Files changed (54) hide show
  1. package/dist/auth/current-user-row.d.ts +20 -0
  2. package/dist/auth/current-user-row.js +22 -0
  3. package/dist/auth/platform.d.ts +1 -0
  4. package/dist/auth/platform.js +13 -33
  5. package/dist/auth/server.d.ts +2 -0
  6. package/dist/auth/server.js +3 -1
  7. package/dist/auth/session.js +1 -1
  8. package/dist/auth/user.d.ts +1 -8
  9. package/dist/auth/user.js +1 -2
  10. package/dist/cli.js +18 -26
  11. package/dist/codegen/datasource-docs.js +1 -93
  12. package/dist/codegen/datasource-types.js +1 -16
  13. package/dist/codegen/fetch-metadata.js +17 -44
  14. package/dist/codegen/softr-env.js +1 -2
  15. package/dist/codegen/softr-fields.js +41 -0
  16. package/dist/cookies.js +2 -10
  17. package/dist/core/options.d.ts +36 -0
  18. package/dist/core/options.js +39 -0
  19. package/dist/core/use-overrides.d.ts +17 -0
  20. package/dist/core/use-overrides.js +30 -0
  21. package/dist/datasource/datasource.d.ts +2 -0
  22. package/dist/datasource/datasource.js +3 -1
  23. package/dist/datasource/fields.js +3 -2
  24. package/dist/datasource/record.d.ts +2 -1
  25. package/dist/fields/format.d.ts +45 -0
  26. package/dist/fields/format.js +140 -0
  27. package/dist/formatting.d.ts +4 -4
  28. package/dist/formatting.js +1 -1
  29. package/dist/index.d.ts +4 -2
  30. package/dist/index.js +3 -1
  31. package/dist/mcp.js +2 -2
  32. package/dist/package.js +1 -1
  33. package/dist/pwa.d.ts +2 -2
  34. package/dist/pwa.js +1 -1
  35. package/dist/server.d.ts +2 -1
  36. package/dist/softr-config.schema.js +24 -45
  37. package/dist/visibility/route.js +1 -1
  38. package/dist/vite.js +19 -1
  39. package/package.json +10 -33
  40. package/schema/softr.schema.json +0 -13
  41. package/skills/softr-sdk/SKILL.md +2 -2
  42. package/skills/softr-sdk/auth/SKILL.md +1 -1
  43. package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
  44. package/skills/softr-sdk/current-user/SKILL.md +3 -24
  45. package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
  46. package/skills/softr-sdk/formatting/SKILL.md +19 -1
  47. package/skills/softr-sdk/integrations/SKILL.md +1 -1
  48. package/skills/softr-sdk/mcp/SKILL.md +1 -1
  49. package/skills/softr-sdk/pwa/SKILL.md +1 -1
  50. package/skills/softr-sdk/record-mutations/SKILL.md +13 -1
  51. package/skills/softr-sdk/record-queries/SKILL.md +21 -1
  52. package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
  53. package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
  54. 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 };
@@ -1,3 +1,4 @@
1
+ import "./user.js";
1
2
  import "./context.js";
2
3
  import { z } from "zod";
3
4
 
@@ -1,8 +1,8 @@
1
- import { assert, panic } from "../assertions.js";
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 { softrConfig } from "../softr-config.js";
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${withAttributes}`, {
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${withAttributes}`, {
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${withAttributes}`, {
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${withAttributes}`, {
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${withAttributes}`, {
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${withAttributes}`, {
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${withAttributes}`, {
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${withAttributes}`, {
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${withAttributes}`, {
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${withAttributes}`, {
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${withAttributes}`, {
236
+ authenticatorVerify: (token, body) => request(`/v1/vibe/applications/${applicationId}/users/authenticator/totp/verify`, {
257
237
  method: "POST",
258
238
  token,
259
239
  json: body
@@ -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 };
@@ -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
@@ -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";
@@ -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, SoftrUserProperties, User };
24
+ export { SoftrUserGroupId, SoftrUserGroups, User };
package/dist/auth/user.js CHANGED
@@ -5,8 +5,7 @@ const userSchema = z.object({
5
5
  id: z.string(),
6
6
  email: z.string(),
7
7
  fullName: z.string(),
8
- userGroupIds: z.array(z.string()),
9
- properties: z.record(z.string(), z.unknown())
8
+ userGroupIds: z.array(z.string())
10
9
  });
11
10
 
12
11
  //#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 { loadSoftrConfig } from "./softr-config.schema.js";
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(), loadSoftrConfig(process.cwd()).user?.properties ?? {});
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
- subject: "datasources",
39
- path: `${DOCS_DIR}/README.md`,
40
- changed: datasourceDocs.changed
41
- },
42
- {
43
- subject: "user groups",
44
- path: USER_GROUPS_DOC,
45
- changed: writeIfChanged(path.resolve(process.cwd(), USER_GROUPS_DOC), renderUserGroupsDoc(input.userGroups))
46
- },
47
- ...input.users === null ? [] : [{
48
- subject: "user properties",
49
- path: USER_PROPERTIES_DOC,
50
- changed: writeIfChanged(path.resolve(process.cwd(), USER_PROPERTIES_DOC), renderUserPropertiesDoc(input.users, {
51
- datasources: new Map(input.datasources.map((d) => [d.id, d])),
52
- fileNames: datasourceDocs.fileNames
53
- }))
54
- }]
55
- ],
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, renderUserPropertiesDoc };
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, renderUserProperties, resolvedShape, scalarExample, underlyingType };
497
+ export { choiceEntries, fieldTypeLiteral, isWritable, linkedDatasource, option, readExample, renderDatasourceTypes, resolvedShape, scalarExample, underlyingType };
@@ -1,4 +1,4 @@
1
- import { assert, panic } from "../assertions.js";
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, aliases) {
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
- userGroups,
55
- users
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, body) {
59
- const response = await fetch(url, {
60
- method: body === void 0 ? "GET" : "POST",
61
- headers: {
62
- accept: "application/json",
63
- ...body === void 0 ? {} : { "content-type": "application/json" },
64
- Authorization: `Bearer ${token}`
65
- },
66
- body: body === void 0 ? void 0 : JSON.stringify(body)
67
- }).catch(() => panic(`Could not reach ${url}`));
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, renderUserProperties } from "./datasource-types.js";
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
- const written = cookies.filter((cookie) => {
46
- const size = cookie.split(";")[0].length;
47
- softAssert(size <= COOKIE_MAX_BYTES, () => `The ${cookie.slice(0, cookie.indexOf("="))} cookie is ${size} bytes, over the ${COOKIE_MAX_BYTES} a browser keeps. It was not written.`);
48
- return size <= COOKIE_MAX_BYTES;
49
- });
50
- if (written.length === 0) return;
51
- setResponseHeader("Set-Cookie", [...getResponseHeaders().getSetCookie(), ...written]);
42
+ if (cookies.length === 0) return;
43
+ setResponseHeader("Set-Cookie", [...getResponseHeaders().getSetCookie(), ...cookies]);
52
44
  }
53
45
 
54
46
  //#endregion