@softr/sdk 0.0.21 → 0.0.22-alpha.1

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.
@@ -19,10 +19,13 @@ const HOST_PREFIX = "__Host-";
19
19
  function requestIsSecure() {
20
20
  return getRequestProtocol() === "https";
21
21
  }
22
+ function sessionCookieName(base, secure) {
23
+ return secure ? HOST_PREFIX + base : base;
24
+ }
22
25
  function sessionCookie(base) {
23
26
  const secure = requestIsSecure();
24
27
  return {
25
- name: secure ? HOST_PREFIX + base : base,
28
+ name: sessionCookieName(base, secure),
26
29
  secure
27
30
  };
28
31
  }
@@ -43,9 +46,8 @@ function readSessionData(sessionToken) {
43
46
  permissions: payload.permissions
44
47
  };
45
48
  }
46
- function makeSessionTokenCookie(token) {
47
- const { name, secure } = sessionCookie(SESSION_TOKEN);
48
- return serializeCookie(name, sealEnvelope(token, SESSION_TOKEN), {
49
+ function makeSessionTokenCookie(token, secure = requestIsSecure()) {
50
+ return serializeCookie(sessionCookieName(SESSION_TOKEN, secure), sealEnvelope(token, SESSION_TOKEN), {
49
51
  maxAge: sessionConfig().sessionMaxAge,
50
52
  secure
51
53
  });
@@ -69,6 +71,19 @@ function makeSessionDataCookie(session, sessionToken) {
69
71
  function clearSession() {
70
72
  clearCookies([SESSION_TOKEN, SESSION_DATA]);
71
73
  }
74
+ /** The clearing pair, for the same out-of-context caller as `makeSessionTokenCookie`. */
75
+ function makeSessionClearingCookies(secure) {
76
+ return serializeClearingCookies([SESSION_TOKEN, SESSION_DATA], secure);
77
+ }
78
+ /**
79
+ * The name `readSessionToken` would look the token up under on a request of this
80
+ * scheme, for a caller that only needs to know whether one is present and cannot
81
+ * decode it. Same `secure` argument as `makeSessionTokenCookie`, so a caller that
82
+ * writes and a caller that probes cannot disagree about the name.
83
+ */
84
+ function sessionTokenCookieName(secure) {
85
+ return sessionCookieName(SESSION_TOKEN, secure);
86
+ }
72
87
  const sessionDataSchema = z.object({
73
88
  user: userSchema.nullable(),
74
89
  permissions: permissionsSchema,
@@ -80,11 +95,13 @@ const sessionDataSchema = z.object({
80
95
  * schemes, and the one left behind would revive if the scheme changed back.
81
96
  */
82
97
  function clearCookies(bases) {
83
- const secure = requestIsSecure();
84
- writeCookies(bases.flatMap((base) => secure ? [HOST_PREFIX + base, base] : [base]).map((name) => serializeCookie(name, "", {
98
+ writeCookies(serializeClearingCookies(bases, requestIsSecure()));
99
+ }
100
+ function serializeClearingCookies(bases, secure) {
101
+ return bases.flatMap((base) => secure ? [sessionCookieName(base, secure), base] : [base]).map((name) => serializeCookie(name, "", {
85
102
  maxAge: 0,
86
103
  secure
87
- })));
104
+ }));
88
105
  }
89
106
  /**
90
107
  * Over the plaintext token, never the cookie value: that is resealed with a
@@ -160,4 +177,4 @@ function openEnvelope(raw, purpose) {
160
177
  }
161
178
 
162
179
  //#endregion
163
- export { clearSession, makeSessionDataCookie, makeSessionTokenCookie, readSessionData, readSessionToken };
180
+ export { clearSession, makeSessionClearingCookies, makeSessionDataCookie, makeSessionTokenCookie, readSessionData, readSessionToken, sessionTokenCookieName };
@@ -8,7 +8,7 @@ import { readSessionToken } from "../auth/session.js";
8
8
  const MAX_RECORD_COUNT = 100;
9
9
  const COUNT_CNT = "COUNT_CNT";
10
10
  const dsUrl = (datasourceId) => {
11
- const datasourceApiUrl = process.env.SOFTR_DATASOURCE_API_URL || "https://datasource-api-staging12.softr.io";
11
+ const datasourceApiUrl = process.env.SOFTR_DATASOURCE_API_URL;
12
12
  if (!datasourceApiUrl) panic("SOFTR_DATASOURCE_API_URL is not set.");
13
13
  const { applicationId } = sessionConfig();
14
14
  return `${withoutTrailingSlash(datasourceApiUrl)}/v1/datasource/vibe/applications/${applicationId}/datasources/${datasourceId}`;
@@ -2,8 +2,13 @@ import "@tanstack/router-generator";
2
2
 
3
3
  //#region src/routes/discover.d.ts
4
4
 
5
+ type DatasourceMethod = "list" | "get" | "create" | "update" | "delete" | "metric" | "chartData";
6
+ type DatasourceAccess = {
7
+ id: string;
8
+ methods: Array<DatasourceMethod>;
9
+ };
5
10
  type VisibilityValue = string | Array<VisibilityValue> | {
6
11
  [key: string]: VisibilityValue;
7
12
  };
8
13
  //#endregion
9
- export { VisibilityValue };
14
+ export { DatasourceAccess, VisibilityValue };
@@ -65,6 +65,42 @@ function extractVisibility(source, filePath) {
65
65
  }
66
66
  };
67
67
  }
68
+ /** Reads all ds.method() calls from the source text. The route file is never executed. */
69
+ function extractDatasources(source, filePath) {
70
+ let program;
71
+ try {
72
+ program = parse(source, {
73
+ sourceType: "module",
74
+ plugins: ["typescript", "jsx"]
75
+ });
76
+ } catch (error) {
77
+ const position = error.loc;
78
+ return {
79
+ ok: false,
80
+ error: {
81
+ code: "unparsable",
82
+ message: `could not parse ${filePath}: ${error.message}`,
83
+ filePath,
84
+ line: position?.line ?? 1,
85
+ column: position?.column ?? 0
86
+ }
87
+ };
88
+ }
89
+ const raw = findDatasourceCalls(program, filePath);
90
+ if (!raw.ok) return raw;
91
+ const byId = /* @__PURE__ */ new Map();
92
+ for (const { id, method } of raw.calls) {
93
+ if (!byId.has(id)) byId.set(id, /* @__PURE__ */ new Set());
94
+ byId.get(id).add(method);
95
+ }
96
+ return {
97
+ ok: true,
98
+ datasources: Array.from(byId.entries()).map(([id, methods]) => ({
99
+ id,
100
+ methods: [...methods]
101
+ }))
102
+ };
103
+ }
68
104
  /**
69
105
  * The generator `mkdirSync`s `tmpDir` and the directory of `generatedRouteTree` outside the `fs`
70
106
  * shim, so defaulted they create directories in the crawled project. Aimed at `os.tmpdir()` both
@@ -74,7 +110,18 @@ function extractVisibility(source, filePath) {
74
110
  const TMP_DIR = os.tmpdir();
75
111
  const generatedRouteTreePath = () => path.join(TMP_DIR, `softr-sdk-route-tree-${randomUUID()}.gen.ts`);
76
112
  const SDK_PACKAGE_NAME = "@softr/sdk";
113
+ const SDK_SERVER_PACKAGE_NAME = "@softr/sdk/server";
77
114
  const VISIBILITY_FUNCTION = "defineVisibility";
115
+ const DS_BINDING = "ds";
116
+ const DS_METHODS = new Set([
117
+ "list",
118
+ "get",
119
+ "create",
120
+ "update",
121
+ "delete",
122
+ "metric",
123
+ "chartData"
124
+ ]);
78
125
  function fail(code, message, filePath, line, column) {
79
126
  return {
80
127
  ok: false,
@@ -87,6 +134,66 @@ function fail(code, message, filePath, line, column) {
87
134
  }
88
135
  };
89
136
  }
137
+ function findDatasourceCalls(ast, filePath) {
138
+ const calls = [];
139
+ let error = null;
140
+ traverse(ast, { CallExpression(path$1) {
141
+ const { node } = path$1;
142
+ const { callee } = node;
143
+ if (callee.type !== "MemberExpression") return;
144
+ if (callee.computed) return;
145
+ if (callee.object.type !== "Identifier") return;
146
+ if (callee.property.type !== "Identifier") return;
147
+ const method = callee.property.name;
148
+ if (!DS_METHODS.has(method)) return;
149
+ if (!isDsImport(path$1, callee.object.name)) return;
150
+ const firstArg = node.arguments[0];
151
+ if (firstArg === void 0 || firstArg.type === "SpreadElement") {
152
+ error = {
153
+ code: "not-literal",
154
+ message: `${filePath}: ds.${method}() must be called with a string literal datasource id`,
155
+ filePath,
156
+ line: node.loc?.start.line ?? 1,
157
+ column: node.loc?.start.column ?? 0
158
+ };
159
+ path$1.stop();
160
+ return;
161
+ }
162
+ if (firstArg.type !== "StringLiteral") {
163
+ error = {
164
+ code: "not-literal",
165
+ message: `${filePath}: the datasource id passed to ds.${method}() must be a string literal`,
166
+ filePath,
167
+ line: firstArg.loc?.start.line ?? 1,
168
+ column: firstArg.loc?.start.column ?? 0
169
+ };
170
+ path$1.stop();
171
+ return;
172
+ }
173
+ calls.push({
174
+ id: firstArg.value,
175
+ method
176
+ });
177
+ } });
178
+ if (error !== null) return {
179
+ ok: false,
180
+ error
181
+ };
182
+ return {
183
+ ok: true,
184
+ calls
185
+ };
186
+ }
187
+ function isDsImport(path$1, name) {
188
+ const binding = path$1.scope.getBinding(name);
189
+ if (binding === void 0 || binding.kind !== "module") return false;
190
+ const specifier = binding.path.node;
191
+ const declaration = binding.path.parent;
192
+ if (specifier.type !== "ImportSpecifier" || declaration.type !== "ImportDeclaration") return false;
193
+ if (specifier.importKind === "type" || declaration.importKind === "type") return false;
194
+ if (declaration.source.value !== SDK_SERVER_PACKAGE_NAME) return false;
195
+ return (specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value) === DS_BINDING;
196
+ }
90
197
  const traverse = babelTraverse.default ?? babelTraverse;
91
198
  /** Only a call initialising a top-level variable counts; one inside a function or block does not. */
92
199
  function findVisibilityCalls(ast) {
@@ -195,4 +302,4 @@ function readLiteral(input) {
195
302
  }
196
303
 
197
304
  //#endregion
198
- export { crawlRoutes, extractVisibility };
305
+ export { crawlRoutes, extractDatasources, extractVisibility };
@@ -1,6 +1,12 @@
1
- import { VisibilityValue } from "./discover.js";
1
+ import { DatasourceAccess, VisibilityValue } from "./discover.js";
2
2
 
3
3
  //#region src/routes/manifest.d.ts
4
+ type DatasourceEntry = DatasourceAccess & {
5
+ /** Project-relative path to the file where the datasource is accessed. */
6
+ filename: string;
7
+ /** Route path, if the file is a route file. */
8
+ route?: string;
9
+ };
4
10
  type ManifestNode = {
5
11
  routePath: string;
6
12
  filePath: string;
@@ -12,6 +18,7 @@ type ManifestNode = {
12
18
  };
13
19
  type Manifest = {
14
20
  root: ManifestNode;
21
+ datasources: Array<DatasourceEntry>;
15
22
  };
16
23
  //#endregion
17
24
  export { Manifest, ManifestNode };
@@ -1,4 +1,4 @@
1
- import { crawlRoutes, extractVisibility } from "./discover.js";
1
+ import { crawlRoutes, extractDatasources, extractVisibility } from "./discover.js";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { inferFullPath } from "@tanstack/router-generator";
@@ -33,11 +33,13 @@ async function generateRoutesManifest(root) {
33
33
  }
34
34
  function buildManifest(opts) {
35
35
  const routesDirectory = opts.crawl.routesDirectory;
36
+ const datasources = [];
36
37
  const toNode = (node) => {
37
38
  const projectRelative = toProjectRelative(opts.root, routesDirectory, node.filePath);
38
39
  const routePath = node.routePath;
39
40
  if (routePath === void 0) throw new Error(`the route crawl produced a node with no route path: ${projectRelative}`);
40
41
  let declared = visibilityOf(opts, node.filePath);
42
+ datasources.push(...datasourceEntriesOf(opts, node.filePath, routePath));
41
43
  const lazyPiece = opts.crawl.routePiecesByPath[routePath]?.lazy;
42
44
  if (lazyPiece && lazyPiece.filePath !== node.filePath) {
43
45
  const lazyDeclared = visibilityOf(opts, lazyPiece.filePath);
@@ -49,6 +51,7 @@ function buildManifest(opts) {
49
51
  column: lazyDeclared.column
50
52
  });
51
53
  if (lazyDeclared !== null) declared = lazyDeclared;
54
+ datasources.push(...datasourceEntriesOf(opts, lazyPiece.filePath, routePath));
52
55
  }
53
56
  return {
54
57
  routePath,
@@ -70,14 +73,18 @@ function buildManifest(opts) {
70
73
  line: rootDeclared.line,
71
74
  column: rootDeclared.column
72
75
  });
73
- return { root: {
74
- routePath: ROOT_ROUTE_ID,
75
- filePath: rootRelative,
76
- inferFullPath: inferFullPath(rootNode) ?? null,
77
- createFileRouteProps: [...rootNode.createFileRouteProps ?? []].sort(),
78
- visibility: null,
79
- children: opts.crawl.routeTree.map(toNode)
80
- } };
76
+ datasources.push(...datasourceEntriesOf(opts, rootNode.filePath, ROOT_ROUTE_ID));
77
+ return {
78
+ root: {
79
+ routePath: ROOT_ROUTE_ID,
80
+ filePath: rootRelative,
81
+ inferFullPath: inferFullPath(rootNode) ?? null,
82
+ createFileRouteProps: [...rootNode.createFileRouteProps ?? []].sort(),
83
+ visibility: null,
84
+ children: opts.crawl.routeTree.map(toNode)
85
+ },
86
+ datasources
87
+ };
81
88
  }
82
89
  /**
83
90
  * `rootRouteId` from `@tanstack/router-core`, which is what a consumer matches against — not the
@@ -131,6 +138,19 @@ function visibilityOf(opts, filePath) {
131
138
  checkVisibilityShape(declared);
132
139
  return declared;
133
140
  }
141
+ function datasourceEntriesOf(opts, filePath, routePath) {
142
+ const projectRelative = toProjectRelative(opts.root, opts.crawl.routesDirectory, filePath);
143
+ const source = opts.sources.get(path.resolve(opts.root, opts.crawl.routesDirectory, filePath));
144
+ if (source === void 0) return [];
145
+ const result = extractDatasources(source, projectRelative);
146
+ if (!result.ok) throw new ManifestError(result.error);
147
+ return result.datasources.map(({ id, methods }) => ({
148
+ id,
149
+ methods,
150
+ filename: projectRelative,
151
+ route: routePath
152
+ }));
153
+ }
134
154
  /**
135
155
  * Only ENOENT becomes `null`. The caller reads `null` as "nothing declared here", so swallowing any
136
156
  * other read error would turn an unreadable declaration into an ungated route.
package/dist/vite.js CHANGED
@@ -61,12 +61,21 @@ async function editEngine() {
61
61
  if (!dir) return false;
62
62
  const entry = pathToFileURL(path.join(dir, "dist", "vite.js")).href;
63
63
  try {
64
- return (await import(entry)).editEngineStudio();
64
+ return (await import(entry)).editEngineStudio({ sessionCookies: await sessionCookies() });
65
65
  } catch (error) {
66
66
  console.error(`[softr] could not load the edit engine from ${entry}:`, error);
67
67
  return false;
68
68
  }
69
69
  }
70
+ /** Imported only for a preview, so an ordinary build never loads the session machinery at all. */
71
+ async function sessionCookies() {
72
+ const { makeSessionTokenCookie, makeSessionClearingCookies, sessionTokenCookieName } = await import("./auth/session.js");
73
+ return {
74
+ makeSessionTokenCookie,
75
+ makeSessionClearingCookies,
76
+ sessionTokenCookieName
77
+ };
78
+ }
70
79
 
71
80
  //#endregion
72
81
  export { MANIFEST_RELATIVE_PATH, generateManifest, softr };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.21",
3
+ "version": "0.0.22-alpha.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: core
12
12
  library: softr-sdk
13
- library_version: 0.0.21
13
+ library_version: 0.0.22-alpha.1
14
14
  ---
15
15
 
16
16
  # Softr SDK core
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.21
13
+ library_version: 0.0.22-alpha.1
14
14
  ---
15
15
 
16
16
  This skill builds on softr-sdk. Read it first for the entrypoints and the client/server boundary.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.21
13
+ library_version: 0.0.22-alpha.1
14
14
  ---
15
15
 
16
16
  This skill builds on softr-sdk. Read it first for the entrypoints and the wiring that puts a visitor on route context.
@@ -8,7 +8,7 @@ description: >-
8
8
  metadata:
9
9
  type: sub-skill
10
10
  library: softr-sdk
11
- library_version: 0.0.21
11
+ library_version: 0.0.22-alpha.1
12
12
  ---
13
13
 
14
14
  This skill builds on softr-sdk. Read it first for the entrypoints and the client/server boundary.
@@ -24,15 +24,13 @@ import { createServerFn } from '@tanstack/react-start'
24
24
  import { z } from 'zod'
25
25
  import { ds } from '@softr/sdk/server'
26
26
  import { q } from '@softr/sdk'
27
-
28
- // The datasource id Softr assigned when the table was registered on the application.
29
- // Copy it from the registration (datasource.datasourceId); it is not the table id.
30
- const PROJECTS = '8ffb4814-b028-4496-b679-7face2167ebc'
31
27
  ```
32
28
 
33
29
  Pass a zod schema straight to `.validator`. Start accepts it and types `data` from it.
34
30
 
35
- Every `ds` call names its datasource by that id: an opaque UUID from the table's registration on the application (Softr Studio's Data tab, or the `register_application_data_source` tool when an agent does it). It is never derived from the table's name. Field ids are opaque too (`fZk3Qm9pLw2Xa1`, not `fld_<name>`) and come from the table's schema.
31
+ Every `ds` call names its datasource by an opaque UUID from the table's registration on the application (Softr Studio's Data tab, or the `register_application_data_source` tool when an agent does it). It is never derived from the table's name. Field ids are opaque too (`fZk3Qm9pLw2Xa1`, not `fld_<name>`) and come from the table's schema.
32
+
33
+ **The datasource id must be a string literal at the call site.**
36
34
 
37
35
  ## Core Patterns
38
36
 
@@ -44,7 +42,7 @@ const NewProject = z.object({ name: z.string(), status: z.string() })
44
42
  const createProject = createServerFn({ method: 'POST' })
45
43
  .validator(NewProject)
46
44
  .handler(async ({ data }) =>
47
- ds.create(PROJECTS, {
45
+ ds.create('8ffb4814-b028-4496-b679-7face2167ebc', {
48
46
  data,
49
47
  select: q.select({ name: 'fZk3Qm9pLw2Xa1', status: 'Ru7bVx2cNq0Ed9' }),
50
48
  }),
@@ -65,7 +63,7 @@ const Rename = z.object({ id: z.string(), name: z.string() })
65
63
  const renameProject = createServerFn({ method: 'POST' })
66
64
  .validator(Rename)
67
65
  .handler(async ({ data }) =>
68
- ds.update(PROJECTS, {
66
+ ds.update('8ffb4814-b028-4496-b679-7face2167ebc', {
69
67
  id: data.id,
70
68
  data: { name: data.name },
71
69
  select: q.select({ name: 'fZk3Qm9pLw2Xa1' }),
@@ -84,7 +82,7 @@ const RecordId = z.object({ id: z.string() })
84
82
 
85
83
  const deleteProject = createServerFn({ method: 'POST' })
86
84
  .validator(RecordId)
87
- .handler(async ({ data }) => ds.delete(PROJECTS, data.id))
85
+ .handler(async ({ data }) => ds.delete('8ffb4814-b028-4496-b679-7face2167ebc', data.id))
88
86
  ```
89
87
 
90
88
  **`delete` takes the record id as a positional argument**, unlike `create` and `update`, which take an options object. It resolves to nothing.
@@ -103,6 +101,8 @@ Stamping a record with who created it, or scoping an update to the person making
103
101
 
104
102
  **A datasource id built from a name.** `'ds_projects'`, the table id, or the table's name glued to a uuid are not datasource ids. The id is the opaque UUID assigned when the table was registered on the application; copy it from there. Anything else fails at request time as not found or forbidden.
105
103
 
104
+ **A datasource id stored in a variable.** The build reads ids statically to build the route manifest; it cannot resolve a name. Pass the UUID as a string literal directly at the call site. The lint rule `@softr/ds-no-computed-id` catches this at development time.
105
+
106
106
  **A write that sends `fields`.** The option is `data`. Only typecheck catches this, so it survives any run that does not typecheck.
107
107
 
108
108
  **A key missing from `select`.** The key is not translated, so the field it was meant for is not the field written. Check the map covers every key rather than trusting the call to complain.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.21
13
+ library_version: 0.0.22-alpha.1
14
14
  ---
15
15
 
16
16
  This skill builds on softr-sdk. Read it first for the entrypoints and the client/server boundary.
@@ -25,15 +25,22 @@ This skill builds on softr-sdk. Read it first for the entrypoints and the client
25
25
  import { createServerFn } from '@tanstack/react-start'
26
26
  import { ds } from '@softr/sdk/server'
27
27
  import { q } from '@softr/sdk'
28
-
29
- // The datasource id Softr assigned when the table was registered on the application.
30
- // Copy it from the registration (datasource.datasourceId); it is not the table id.
31
- const POSTS = '8ffb4814-b028-4496-b679-7face2167ebc'
32
28
  ```
33
29
 
34
30
  `ds` is server-only and lives on `@softr/sdk/server`. `q` builds queries and nothing more, so it ships on the root entrypoint and can be imported anywhere.
35
31
 
36
- Every `ds` call names its datasource by that id: an opaque UUID from the table's registration on the application (Softr Studio's Data tab, or the `register_application_data_source` tool when an agent does it). It is not the table id, and it is never derived from the table's name — `'ds_posts'` or a name glued to a uuid fails at request time as not found or forbidden.
32
+ Every `ds` call names its datasource by an opaque UUID from the table's registration on the application (Softr Studio's Data tab, or the `register_application_data_source` tool when an agent does it). It is not the table id, and it is never derived from the table's name — `'ds_posts'` or a name glued to a uuid fails at request time as not found or forbidden.
33
+
34
+ **The datasource id must be a string literal at the call site.** Do not assign it to a variable and pass the variable — the build reads ids statically and cannot resolve a name. Write the UUID directly as the first argument:
35
+
36
+ ```ts
37
+ // correct
38
+ ds.list('8ffb4814-b028-4496-b679-7face2167ebc', { ... })
39
+
40
+ // wrong — the build cannot read this
41
+ const POSTS = '8ffb4814-b028-4496-b679-7face2167ebc'
42
+ ds.list(POSTS, { ... })
43
+ ```
37
44
 
38
45
  A record comes back as `{ id: string; fields: Record<string, unknown> }`. When you write a TypeScript type for the fields, the shape of each value depends on the Softr field type — see [references/field-types.md](./references/field-types.md) for the full mapping.
39
46
 
@@ -59,10 +66,8 @@ Cast at the `ds.list` call using the `ListResult` generic so the type flows thro
59
66
  ```ts
60
67
  import type { ListResult, SoftrRecord } from '@softr/sdk'
61
68
 
62
- const PRODUCTS = '3c1e8a52-6f0d-4b7e-9a21-5d84c0f7b2e6' // from the table's registration
63
-
64
69
  const fetchProducts = createServerFn().handler(() =>
65
- ds.list(PRODUCTS, { select, limit: 100 }) as Promise<ListResult<Product>>
70
+ ds.list('3c1e8a52-6f0d-4b7e-9a21-5d84c0f7b2e6', { select, limit: 100 }) as Promise<ListResult<Product>>
66
71
  )
67
72
  ```
68
73
 
@@ -74,7 +79,7 @@ const fetchProducts = createServerFn().handler(() =>
74
79
 
75
80
  ```ts
76
81
  const listPublishedPosts = createServerFn().handler(async () => {
77
- return await ds.list(POSTS, {
82
+ return await ds.list('8ffb4814-b028-4496-b679-7face2167ebc', {
78
83
  select: q.select({
79
84
  title: 'fZk3Qm9pLw2Xa1',
80
85
  author: 'mB4kQ8yTn1sWc7',
@@ -99,7 +104,7 @@ Softr field ids are opaque strings (`fZk3Qm9pLw2Xa1`), copied from the table's s
99
104
  ```ts
100
105
  const select = q.select({ title: 'fZk3Qm9pLw2Xa1', views: 'Ru7bVx2cNq0Ed9' })
101
106
 
102
- await ds.list(POSTS, {
107
+ await ds.list('8ffb4814-b028-4496-b679-7face2167ebc', {
103
108
  select,
104
109
  where: q.number('views').gt(100), // alias, not Ru7bVx2cNq0Ed9
105
110
  orderBy: q.asc('title'), // alias here too
@@ -112,7 +117,7 @@ Once `select` is present it is the whole vocabulary. The returned record carries
112
117
  ### Get one record
113
118
 
114
119
  ```ts
115
- const post = await ds.get(POSTS, { id: postId })
120
+ const post = await ds.get('8ffb4814-b028-4496-b679-7face2167ebc', { id: postId })
116
121
  if (post === null) throw notFound()
117
122
  ```
118
123
 
@@ -138,9 +143,9 @@ Five field types, `q.text`, `q.number`, `q.boolean`, `q.date` and `q.array`, com
138
143
  ### Paginate
139
144
 
140
145
  ```ts
141
- const first = await ds.list(POSTS, { limit: 50 })
146
+ const first = await ds.list('8ffb4814-b028-4496-b679-7face2167ebc', { limit: 50 })
142
147
  if (first.nextOffset !== null) {
143
- const second = await ds.list(POSTS, { limit: 50, offset: first.nextOffset })
148
+ const second = await ds.list('8ffb4814-b028-4496-b679-7face2167ebc', { limit: 50, offset: first.nextOffset })
144
149
  }
145
150
  ```
146
151
 
@@ -149,12 +154,10 @@ if (first.nextOffset !== null) {
149
154
  ### Scope the read to the visitor
150
155
 
151
156
  ```ts
152
- const INVOICES = 'e4b7c2a1-9d3f-4e68-b05a-7f1c6d2e8a93' // from the table's registration
153
-
154
157
  const listMyInvoices = createServerFn().handler(async ({ context }) => {
155
158
  if (!context.user) throw new Error('Not signed in')
156
159
 
157
- return await ds.list(INVOICES, {
160
+ return await ds.list('e4b7c2a1-9d3f-4e68-b05a-7f1c6d2e8a93', {
158
161
  select: q.select({ owner: 'Gq5wNt8rJy3Hb1', amount: 'Vk9cXm2pSz6Ld4' }),
159
162
  where: q.text('owner').is(context.user.id),
160
163
  })
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.21
15
+ library_version: 0.0.22-alpha.1
16
16
  ---
17
17
 
18
18
  This skill builds on softr-sdk. Read it first for the entrypoints and the wiring that puts a visitor on route context.