@softr/sdk 0.0.50-alpha.1 → 0.0.50

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.
@@ -3,13 +3,20 @@ import { z } from "zod";
3
3
  //#region src/auth/user.d.ts
4
4
 
5
5
  /**
6
- * The app's group ids. The app's generated types declare them, the predefined
7
- * groups included. Every group id the SDK takes and returns narrows to them.
8
- * Never declare it by hand: a second declaration does not merge with the
9
- * generated one.
6
+ * Declare this interface in your app to narrow every group id the SDK takes and
7
+ * returns. Without a declaration, a group id is a plain `string`.
8
+ *
9
+ * @example
10
+ * declare module "@softr/sdk" {
11
+ * interface SoftrUserGroups {
12
+ * ids:
13
+ * | "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
14
+ * | "f9e8d7c6-5b4a-4938-8271-605f4e3d2c1b"
15
+ * }
16
+ * }
10
17
  */
11
18
  interface SoftrUserGroups {}
12
- /** A Softr user group id, narrowed to the app's own ids by {@link SoftrUserGroups}. */
19
+ /** A Softr user group id, narrowed to your own ids if you declare {@link SoftrUserGroups}. */
13
20
  type SoftrUserGroupId = SoftrUserGroups extends {
14
21
  ids: infer Id extends string;
15
22
  } ? Id : string;
package/dist/cli.js CHANGED
@@ -2,9 +2,7 @@
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 { renderUserGroupsDoc } from "./codegen/user-groups.js";
6
5
  import { renderSoftrEnv } from "./codegen/softr-env.js";
7
- import { writeIfChanged } from "./codegen/write-if-changed.js";
8
6
  import { writeDatasourceDocs } from "./codegen/write-datasource-docs.js";
9
7
  import { writeFileSync } from "node:fs";
10
8
  import path from "node:path";
@@ -13,44 +11,26 @@ import { parseArgs } from "node:util";
13
11
  //#region src/cli.ts
14
12
  const OUTPUT = "softr-env.d.ts";
15
13
  const DOCS_DIR = "docs/datasources";
16
- const USER_GROUPS_DOC = "docs/user-groups.md";
17
14
  async function generate({ json }) {
18
15
  if (json) console.info = console.error;
19
16
  const input = await fetchSoftrEnvInput(metadataSourceFromEnv());
20
17
  writeFileSync(path.resolve(process.cwd(), OUTPUT), renderSoftrEnv(input, version), "utf8");
21
- const report = writeDocs(input);
22
- if (json) {
23
- process.stdout.write(`${JSON.stringify(report)}\n`);
24
- return;
25
- }
26
- printReport(report);
27
- }
28
- function writeDocs(input) {
29
- const datasourceDocs = writeDatasourceDocs(input.datasources, path.resolve(process.cwd(), DOCS_DIR));
30
- return {
18
+ const docFiles = writeDatasourceDocs(input.datasources, path.resolve(process.cwd(), DOCS_DIR));
19
+ const report = {
31
20
  types: OUTPUT,
32
- docs: [{
33
- subject: "datasources",
34
- path: `${DOCS_DIR}/README.md`,
35
- changed: datasourceDocs.changed
36
- }, {
37
- subject: "user groups",
38
- path: USER_GROUPS_DOC,
39
- changed: writeIfChanged(path.resolve(process.cwd(), USER_GROUPS_DOC), renderUserGroupsDoc(input.userGroups))
40
- }],
21
+ docsIndex: `${DOCS_DIR}/README.md`,
41
22
  datasources: input.datasources.map(({ id, name }) => ({
42
23
  id,
43
24
  name,
44
- doc: `${DOCS_DIR}/${requireDefined(datasourceDocs.fileNames.get(id))}`
25
+ doc: `${DOCS_DIR}/${requireDefined(docFiles.get(id))}`
45
26
  }))
46
27
  };
47
- }
48
- function printReport(report) {
49
- console.log(`[softr] wrote ${report.types}`);
50
- for (const doc of report.docs) {
51
- const state = doc.changed ? "updated" : "unchanged";
52
- console.log(`[softr] ${state} ${doc.path} for ${doc.subject}`);
28
+ if (json) {
29
+ process.stdout.write(`${JSON.stringify(report)}\n`);
30
+ return;
53
31
  }
32
+ console.log(`[softr] wrote ${report.types}`);
33
+ console.log(`[softr] wrote ${report.docsIndex}`);
54
34
  for (const datasource of report.datasources) console.log(`[softr] wrote ${datasource.doc} for ${datasource.name} (${datasource.id})`);
55
35
  }
56
36
  const { positionals, values } = parseArgs({
@@ -19,7 +19,7 @@ function metadataSourceFromEnv(env = process.env) {
19
19
  }
20
20
  async function fetchSoftrEnvInput(source) {
21
21
  const { studioApiUrl, datasourceApiUrl, applicationId, token } = source;
22
- const [registered, userGroups] = await Promise.all([apiFetch(`${studioApiUrl}/v1/applications/${applicationId}/datasources`, token), apiFetch(`${studioApiUrl}/v1/applications/${applicationId}/access-control/user-groups`, token)]);
22
+ const registered = await apiFetch(`${studioApiUrl}/v1/applications/${applicationId}/datasources`, token);
23
23
  return {
24
24
  datasources: await Promise.all(registered.map(async ({ name, datasource }) => {
25
25
  return {
@@ -28,7 +28,7 @@ async function fetchSoftrEnvInput(source) {
28
28
  name
29
29
  };
30
30
  })),
31
- userGroups
31
+ userGroupIds: null
32
32
  };
33
33
  }
34
34
  async function apiFetch(url, token) {
@@ -13,8 +13,7 @@ function renderSoftrEnv(input, sdkVersion) {
13
13
  "",
14
14
  "declare module \"@softr/sdk\" {",
15
15
  indent(interfaces),
16
- "",
17
- indent(renderUserGroups(input.userGroups)),
16
+ ...input.userGroupIds === null ? [] : ["", indent(renderUserGroups(input.userGroupIds))],
18
17
  "}",
19
18
  ""
20
19
  ].join("\n");
@@ -34,11 +33,10 @@ function header(sdkVersion) {
34
33
  * THIS FILE IS AUTO GENERATED, DO NOT EDIT THIS MANUALLY
35
34
  *
36
35
  * This file exists for type checking. See docs/datasources/ for field reference
37
- * and guide how to use it, and docs/user-groups.md for the app's user groups.
36
+ * and guide how to use it.
38
37
  *
39
38
  * Generated by @softr/sdk@${sdkVersion}
40
39
  * This file is regenerated automatically when a change is made to the datasource
41
- * or to the app's user groups
42
40
  */
43
41
 
44
42
  export {};`;
@@ -1,32 +1,11 @@
1
1
  //#region src/codegen/user-groups.ts
2
- const DOC_NOTE = "> This document is generated automatically when a change is made in the app's user groups.";
3
- function renderUserGroups(groups) {
2
+ function renderUserGroups(ids) {
4
3
  return [
5
4
  "interface SoftrUserGroups {",
6
- ` ids: ${groups.length === 0 ? "never" : groups.map(({ id }) => JSON.stringify(id)).join(" | ")};`,
5
+ ` ids: ${ids.length === 0 ? "never" : ids.map((id) => JSON.stringify(id)).join(" | ")};`,
7
6
  "}"
8
7
  ].join("\n");
9
8
  }
10
- function renderUserGroupsDoc(groups) {
11
- const heading = [
12
- "# User groups",
13
- "",
14
- DOC_NOTE,
15
- ""
16
- ];
17
- if (groups.length === 0) return [
18
- ...heading,
19
- "This app has no user groups.",
20
- ""
21
- ].join("\n");
22
- return [
23
- ...heading,
24
- "User groups available in this app:",
25
- "",
26
- ...groups.map(({ id, name, type }) => `- ${name} (${id}), ${type.toLowerCase()}`),
27
- ""
28
- ].join("\n");
29
- }
30
9
 
31
10
  //#endregion
32
- export { renderUserGroups, renderUserGroupsDoc };
11
+ export { renderUserGroups };
@@ -1,26 +1,24 @@
1
1
  import { requireDefined } from "../assertions.js";
2
2
  import { renderDatasourceDoc, renderDatasourceIndex } from "./datasource-docs.js";
3
3
  import { docFileNames } from "./doc-file-names.js";
4
- import { writeIfChanged } from "./write-if-changed.js";
4
+ import { mkdirSync, writeFileSync } from "node:fs";
5
5
  import path from "node:path";
6
6
 
7
7
  //#region src/codegen/write-datasource-docs.ts
8
8
  /**
9
9
  * Writes the index and one file per datasource, and touches nothing else in
10
10
  * `docsDir`: the caller resolves it against `process.cwd()`, which can be any
11
- * directory a developer ran the bin from. `changed` is true when any file it
12
- * wrote now reads differently.
11
+ * directory a developer ran the bin from.
13
12
  */
14
13
  function writeDatasourceDocs(datasources, docsDir) {
15
14
  const context = {
16
15
  datasources: new Map(datasources.map((datasource) => [datasource.id, datasource])),
17
16
  fileNames: docFileNames(datasources)
18
17
  };
19
- const writes = [writeIfChanged(path.join(docsDir, "README.md"), renderDatasourceIndex(datasources, context)), ...datasources.map((datasource) => writeIfChanged(path.join(docsDir, requireDefined(context.fileNames.get(datasource.id))), renderDatasourceDoc(datasource, context)))];
20
- return {
21
- fileNames: context.fileNames,
22
- changed: writes.includes(true)
23
- };
18
+ mkdirSync(docsDir, { recursive: true });
19
+ writeFileSync(path.join(docsDir, "README.md"), renderDatasourceIndex(datasources, context), "utf8");
20
+ for (const datasource of datasources) writeFileSync(path.join(docsDir, requireDefined(context.fileNames.get(datasource.id))), renderDatasourceDoc(datasource, context), "utf8");
21
+ return context.fileNames;
24
22
  }
25
23
 
26
24
  //#endregion
@@ -24,9 +24,17 @@ function makeSortSet(aliases) {
24
24
  };
25
25
  return set;
26
26
  }
27
- /** Ands a list of filters into one group, which is what an accumulated `where` needs. */
27
+ /**
28
+ * Ands the filters of every `where` call into one, which is what an accumulated `where` needs.
29
+ *
30
+ * The backend takes a group inside a group and no deeper, and the docs promise the caller both levels.
31
+ * A wrapper added here would spend one of them: a single `where` handing over an AND with an OR inside
32
+ * would arrive three deep and be refused. So one filter goes through as it is, and a group that is itself
33
+ * an AND contributes its members to the group built here rather than sitting inside it.
34
+ */
28
35
  function andAll(filters) {
29
- return group("AND", filters);
36
+ if (filters.length === 1) return filters[0];
37
+ return group("AND", filters.flatMap((filter) => filter[INTERNAL_KIND] === "LOGICAL_GROUP" && filter.logicalOperator === "AND" ? filter.filters : [filter]));
30
38
  }
31
39
  function group(logicalOperator, filters) {
32
40
  return {
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.50-alpha.1";
2
+ var version = "0.0.50";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.50-alpha.1",
3
+ "version": "0.0.50",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
@@ -11,7 +11,7 @@ description: >-
11
11
  metadata:
12
12
  type: core
13
13
  library: softr-sdk
14
- library_version: 0.0.50-alpha.1
14
+ library_version: 0.0.50
15
15
  ---
16
16
 
17
17
  # Softr SDK core
@@ -137,6 +137,6 @@ type User = {
137
137
  id: string
138
138
  email: string
139
139
  fullName: string
140
- userGroupIds: Array<SoftrUserGroupId> // the app's own group ids
140
+ userGroupIds: Array<SoftrUserGroupId> // plain string unless you declare your ids
141
141
  }
142
142
  ```
@@ -16,7 +16,7 @@ description: >-
16
16
  metadata:
17
17
  type: sub-skill
18
18
  library: softr-sdk
19
- library_version: 0.0.50-alpha.1
19
+ library_version: 0.0.50
20
20
  ---
21
21
 
22
22
  **Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.50-alpha.1
15
+ library_version: 0.0.50
16
16
  ---
17
17
 
18
18
  **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -4,12 +4,11 @@ description: >-
4
4
  Read the signed-in Softr visitor: useCurrentUser and the User type,
5
5
  context.user in a route's beforeLoad, and context.user inside a server
6
6
  function handler, where Start types it from the app's registered request
7
- middleware. Covers the null guest and the generated SoftrUserGroups that
8
- narrows group ids to the app's own.
7
+ middleware. Covers the null guest.
9
8
  metadata:
10
9
  type: sub-skill
11
10
  library: softr-sdk
12
- library_version: 0.0.50-alpha.1
11
+ library_version: 0.0.50
13
12
  ---
14
13
 
15
14
  # The current user
@@ -30,7 +29,7 @@ type User = {
30
29
  id: string
31
30
  email: string
32
31
  fullName: string
33
- userGroupIds: Array<SoftrUserGroupId> // the app's own group ids
32
+ userGroupIds: Array<SoftrUserGroupId> // plain string unless you declare your ids
34
33
  }
35
34
  ```
36
35
 
@@ -84,14 +83,6 @@ const getMyInvoices = createServerFn().handler(async ({ context }) => {
84
83
 
85
84
  `context.user` is typed. Start derives a handler's context from the app's registered request middleware as well as the server function's own, so there is nothing to import and nothing to cast.
86
85
 
87
- ### Group ids are the app's own
88
-
89
- The generated `softr-env.d.ts` declares `SoftrUserGroups` with every group id the app has, the predefined groups included. So `user.userGroupIds` and every `userGroupIds` the SDK accepts narrow to those ids, and an id the app does not have stops compiling. A new group appears there once it exists on the app.
90
-
91
- For every group's name and id, read `docs/user-groups.md`. It lists every group the app has, the predefined ones included. Copy an id from there; never derive one from a group's name.
92
-
93
- Never declare `SoftrUserGroups` yourself. A second declaration does not merge with the generated one. TypeScript keeps one of the two without an error, so the ids you wrote are either ignored or hide the real ones.
94
-
95
86
  ## References
96
87
 
97
88
  - [routes-and-visibility/SKILL.md](../routes-and-visibility/SKILL.md): `isAuthorized` narrows `context.user` while applying the route's rule
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.50-alpha.1
13
+ library_version: 0.0.50
14
14
  ---
15
15
 
16
16
  **Boundary.** `upload` lives on `@softr/sdk/server` and runs inside server functions. `useFileUpload` and `FileItem` live on `@softr/sdk` and run in components. `FileAttachment` comes from `@softr/sdk` and can be imported anywhere.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.50-alpha.1
13
+ library_version: 0.0.50
14
14
  ---
15
15
 
16
16
  **Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineIntegration` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.50-alpha.1
12
+ library_version: 0.0.50
13
13
  ---
14
14
 
15
15
  **Boundary.** `softrMcpMiddleware` lives on `@softr/sdk` and is registered once, in `src/start.ts`. The actions you hand it are the same `defineQuery` / `defineMutation` values the app's own pages use — declared wherever they already live, not duplicated for MCP.
@@ -11,7 +11,7 @@ description: >-
11
11
  metadata:
12
12
  type: sub-skill
13
13
  library: softr-sdk
14
- library_version: 0.0.50-alpha.1
14
+ library_version: 0.0.50
15
15
  ---
16
16
 
17
17
  **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineMutation` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -13,7 +13,7 @@ description: >-
13
13
  metadata:
14
14
  type: sub-skill
15
15
  library: softr-sdk
16
- library_version: 0.0.50-alpha.1
16
+ library_version: 0.0.50
17
17
  ---
18
18
 
19
19
  **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.50-alpha.1
12
+ library_version: 0.0.50
13
13
  ---
14
14
 
15
15
  **Boundary.** `Audience` and `defineVisibility` come from `@softr/sdk` and work anywhere.
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.50-alpha.1
15
+ library_version: 0.0.50
16
16
  ---
17
17
 
18
18
  **Boundary.** `defineVisibility`, the access functions and the error predicates come from `@softr/sdk` and work anywhere.
@@ -64,8 +64,6 @@ defineVisibility({ // those groups only
64
64
 
65
65
  Absent or empty `userGroupIds` means **any** signed-in visitor, not "nobody".
66
66
 
67
- Take every id in `userGroupIds` from `docs/user-groups.md`, which lists each group the app has with its name and id. An id the app does not have is a type error.
68
-
69
67
  ## Core Patterns
70
68
 
71
69
  ### Guard a whole section with one layout route
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.50-alpha.1
12
+ library_version: 0.0.50
13
13
  ---
14
14
 
15
15
  **Boundary.** `workflow` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineWorkflow` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -1,22 +0,0 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import path from "node:path";
3
-
4
- //#region src/codegen/write-if-changed.ts
5
- /** Returns whether it had to write. */
6
- function writeIfChanged(file, content) {
7
- if (readOrNull(file) === content) return false;
8
- mkdirSync(path.dirname(file), { recursive: true });
9
- writeFileSync(file, content, "utf8");
10
- return true;
11
- }
12
- function readOrNull(file) {
13
- try {
14
- return readFileSync(file, "utf8");
15
- } catch (error) {
16
- if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
17
- throw error;
18
- }
19
- }
20
-
21
- //#endregion
22
- export { writeIfChanged };