@softr/sdk 0.0.22-alpha.0 → 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.
- package/dist/core/datasource.js +1 -1
- package/dist/routes/discover.d.ts +6 -1
- package/dist/routes/discover.js +108 -1
- package/dist/routes/manifest.d.ts +8 -1
- package/dist/routes/manifest.js +29 -9
- package/dist/server.d.ts +1 -2
- package/dist/server.js +1 -2
- package/dist/vite.js +10 -1
- package/package.json +1 -1
- package/skills/softr-sdk/SKILL.md +1 -1
- package/skills/softr-sdk/auth/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/data-mutations/SKILL.md +9 -9
- package/skills/softr-sdk/reading-records/SKILL.md +20 -17
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/dist/auth/session.d.ts +0 -16
package/dist/core/datasource.js
CHANGED
|
@@ -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
|
|
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 };
|
package/dist/routes/discover.js
CHANGED
|
@@ -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 };
|
package/dist/routes/manifest.js
CHANGED
|
@@ -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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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/server.d.ts
CHANGED
|
@@ -2,6 +2,5 @@ import { auth } from "./auth/server.js";
|
|
|
2
2
|
import { ds } from "./core/datasource.js";
|
|
3
3
|
import { workflow } from "./core/workflow.js";
|
|
4
4
|
import { integration } from "./core/integration.js";
|
|
5
|
-
import { makeSessionClearingCookies, makeSessionTokenCookie, sessionTokenCookieName } from "./auth/session.js";
|
|
6
5
|
import "@tanstack/react-start/server-only";
|
|
7
|
-
export { auth, ds, integration,
|
|
6
|
+
export { auth, ds, integration, workflow };
|
package/dist/server.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { makeSessionClearingCookies, makeSessionTokenCookie, sessionTokenCookieName } from "./auth/session.js";
|
|
2
1
|
import { auth } from "./auth/server.js";
|
|
3
2
|
import { ds } from "./core/datasource.js";
|
|
4
3
|
import { workflow } from "./core/workflow.js";
|
|
5
4
|
import { integration } from "./core/integration.js";
|
|
6
5
|
import "@tanstack/react-start/server-only";
|
|
7
6
|
|
|
8
|
-
export { auth, ds, integration,
|
|
7
|
+
export { auth, ds, integration, workflow };
|
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
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
package/dist/auth/session.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import "./context.js";
|
|
2
|
-
|
|
3
|
-
//#region src/auth/session.d.ts
|
|
4
|
-
|
|
5
|
-
declare function makeSessionTokenCookie(token: string, secure?: boolean): string;
|
|
6
|
-
/** The clearing pair, for the same out-of-context caller as `makeSessionTokenCookie`. */
|
|
7
|
-
declare function makeSessionClearingCookies(secure: boolean): Array<string>;
|
|
8
|
-
/**
|
|
9
|
-
* The name `readSessionToken` would look the token up under on a request of this
|
|
10
|
-
* scheme, for a caller that only needs to know whether one is present and cannot
|
|
11
|
-
* decode it. Same `secure` argument as `makeSessionTokenCookie`, so a caller that
|
|
12
|
-
* writes and a caller that probes cannot disagree about the name.
|
|
13
|
-
*/
|
|
14
|
-
declare function sessionTokenCookieName(secure: boolean): string;
|
|
15
|
-
//#endregion
|
|
16
|
-
export { makeSessionClearingCookies, makeSessionTokenCookie, sessionTokenCookieName };
|