@softr/sdk 0.0.66 → 0.0.68-alpha.0
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/integration.d.ts +7 -0
- package/dist/core/integration.js +10 -3
- package/dist/mcp.d.ts +2 -2
- package/dist/package.js +1 -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/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
- package/skills/softr-sdk/formatting/SKILL.md +1 -1
- package/skills/softr-sdk/integrations/SKILL.md +77 -3
- package/skills/softr-sdk/mcp/SKILL.md +1 -1
- package/skills/softr-sdk/pwa/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
- package/skills/softr-sdk/record-queries/SKILL.md +1 -1
- package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
|
@@ -7,6 +7,13 @@ interface IntegrationFetchInit {
|
|
|
7
7
|
headers?: Record<string, string>;
|
|
8
8
|
body?: string;
|
|
9
9
|
signal?: AbortSignal;
|
|
10
|
+
/**
|
|
11
|
+
* Relay the vendor's answer as it is produced instead of waiting for all of it — for any endpoint
|
|
12
|
+
* that streams, such as an LLM completion requested with `"stream": true`. The returned `Response`
|
|
13
|
+
* body is then the vendor's own stream, readable chunk by chunk. A vendor that answers in one piece
|
|
14
|
+
* still works.
|
|
15
|
+
*/
|
|
16
|
+
stream?: boolean;
|
|
10
17
|
}
|
|
11
18
|
declare function integrationCreateFetcher(action: IntegrationAction): (init?: IntegrationFetchInit) => Promise<Response>;
|
|
12
19
|
declare const integration: {
|
package/dist/core/integration.js
CHANGED
|
@@ -3,15 +3,16 @@ import { readSessionToken } from "../auth/session.js";
|
|
|
3
3
|
import { dsUrl, refererHeader } from "../datasource/record.js";
|
|
4
4
|
|
|
5
5
|
//#region src/core/integration.ts
|
|
6
|
-
|
|
6
|
+
const EVENT_STREAM = "text/event-stream";
|
|
7
|
+
async function integrationFetch(action, { params, query, headers = {}, body, signal, stream = false } = {}) {
|
|
7
8
|
if (signal?.aborted) throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
8
9
|
const payload = {
|
|
9
10
|
method: action.resource.method,
|
|
10
11
|
url: buildUrl(action.resource.url, params, query),
|
|
11
|
-
headers
|
|
12
|
+
headers: stream ? withAcceptEventStream(headers) : headers
|
|
12
13
|
};
|
|
13
14
|
if (body !== void 0) payload.body = body;
|
|
14
|
-
return fetch(`${dsUrl(action.id)}/proxy`, {
|
|
15
|
+
return fetch(`${dsUrl(action.id)}/${stream ? "proxy/stream" : "proxy"}`, {
|
|
15
16
|
method: "POST",
|
|
16
17
|
headers: {
|
|
17
18
|
"Content-Type": "application/json",
|
|
@@ -30,6 +31,12 @@ function buildUrl(template, params, query) {
|
|
|
30
31
|
if (query !== void 0 && Object.keys(query).length > 0) url += `?${new URLSearchParams(query).toString()}`;
|
|
31
32
|
return url;
|
|
32
33
|
}
|
|
34
|
+
function withAcceptEventStream(headers) {
|
|
35
|
+
return Object.keys(headers).some((name) => name.toLowerCase() === "accept") ? headers : {
|
|
36
|
+
...headers,
|
|
37
|
+
Accept: EVENT_STREAM
|
|
38
|
+
};
|
|
39
|
+
}
|
|
33
40
|
function integrationCreateFetcher(action) {
|
|
34
41
|
return (init) => integrationFetch(action, init);
|
|
35
42
|
}
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatasourceAction } from "./action.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as _tanstack_react_start0 from "@tanstack/react-start";
|
|
3
3
|
|
|
4
4
|
//#region src/mcp.d.ts
|
|
5
5
|
|
|
@@ -31,6 +31,6 @@ type SoftrMcpOptions = {
|
|
|
31
31
|
* ],
|
|
32
32
|
* }))
|
|
33
33
|
*/
|
|
34
|
-
declare function softrMcpMiddleware(options?: SoftrMcpOptions):
|
|
34
|
+
declare function softrMcpMiddleware(options?: SoftrMcpOptions): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>;
|
|
35
35
|
//#endregion
|
|
36
36
|
export { softrMcpMiddleware };
|
package/dist/package.js
CHANGED
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ description: >-
|
|
|
19
19
|
metadata:
|
|
20
20
|
type: sub-skill
|
|
21
21
|
library: softr-sdk
|
|
22
|
-
library_version: 0.0.
|
|
22
|
+
library_version: 0.0.68-alpha.0
|
|
23
23
|
---
|
|
24
24
|
|
|
25
25
|
**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.
|
|
15
|
+
library_version: 0.0.68-alpha.0
|
|
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.
|
|
@@ -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.68-alpha.0
|
|
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.
|
|
@@ -5,12 +5,12 @@ description: >-
|
|
|
5
5
|
defineIntegration from @softr/sdk declares the action with its integrationId,
|
|
6
6
|
the one HTTP method and endpoint url it allows, and permissions;
|
|
7
7
|
integration.createFetcher from @softr/sdk/server binds it and returns a
|
|
8
|
-
fetcher function you call with optional params, query, headers, and
|
|
9
|
-
Check res.ok before res.json().
|
|
8
|
+
fetcher function you call with optional params, query, headers, body, and
|
|
9
|
+
stream for a vendor answer that arrives in pieces. Check res.ok before res.json().
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
13
|
+
library_version: 0.0.68-alpha.0
|
|
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.
|
|
@@ -127,6 +127,78 @@ const sendSlackMessage = createServerFn({ method: 'POST' })
|
|
|
127
127
|
|
|
128
128
|
Do not add an `Authorization` key to `headers` — Softr injects the integration's credentials automatically.
|
|
129
129
|
|
|
130
|
+
## Calling an AI model
|
|
131
|
+
|
|
132
|
+
A button on one record that hands its fields to the model and shows the answer is the `ai-action` pattern:
|
|
133
|
+
it carries this call and the model id, so write the call by hand only for another shape — a batch, a chat,
|
|
134
|
+
a field the model fills on create. An AI integration is called like any other. For `ANTHROPIC_AI`, the action is `POST` on
|
|
135
|
+
`https://api.anthropic.com/v1/messages`, the request carries the `anthropic-version` header and a body of
|
|
136
|
+
`model`, `max_tokens`, `system` and `messages`, and the answer's text is in `content[].text`:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const askClaude = defineIntegration({
|
|
140
|
+
id: 'askClaude',
|
|
141
|
+
integrationId: '<the ANTHROPIC_AI integration id from the attached integrations>',
|
|
142
|
+
method: 'POST',
|
|
143
|
+
url: 'https://api.anthropic.com/v1/messages',
|
|
144
|
+
permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const claude = integration.createFetcher(askClaude)
|
|
148
|
+
const res = await claude({
|
|
149
|
+
headers: { 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01' },
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
model: 'claude-opus-5-5',
|
|
152
|
+
max_tokens: 1024,
|
|
153
|
+
system: 'You write a short briefing for a sales team. Use only the facts given.',
|
|
154
|
+
messages: [{ role: 'user', content: facts }],
|
|
155
|
+
}),
|
|
156
|
+
})
|
|
157
|
+
if (!res.ok) throw new Error(`AI error: ${res.status} ${await res.text()}`)
|
|
158
|
+
const body = (await res.json()) as { content: Array<{ type: string; text?: string }> }
|
|
159
|
+
const text = body.content.flatMap((block) => (block.type === 'text' && block.text ? [block.text] : [])).join('')
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Use the model id above as it is written here.** Model ids are not derivable: one spelled from memory
|
|
163
|
+
(`claude-3-5-…`, `claude-sonnet-4-…`) names a model that no longer answers, and the error only shows up when
|
|
164
|
+
the button is pressed. `claude-opus-5-5` is the current default; `claude-sonnet-5-5` is the cheaper one for a
|
|
165
|
+
short summary.
|
|
166
|
+
|
|
167
|
+
## Streaming a vendor answer
|
|
168
|
+
|
|
169
|
+
Some vendor endpoints can send their answer in pieces while they produce it — LLM completions token by token, long exports, live event feeds. The fetcher relays such an answer as it arrives when you pass `stream: true`; the response body is then the vendor's own stream, unchanged.
|
|
170
|
+
|
|
171
|
+
Decide per call, from the vendor's API docs for that endpoint, not from the integration type or the action:
|
|
172
|
+
|
|
173
|
+
- The endpoint offers a streaming mode (a `stream: true` body flag, an `Accept: text/event-stream` variant, a chunked response the docs say to read incrementally). Ask the vendor for it in the request and pass `stream: true` to the fetcher.
|
|
174
|
+
- The endpoint answers in one piece, which is the case for most REST calls. Call the fetcher without `stream`; the answer is buffered and `res.json()` works.
|
|
175
|
+
|
|
176
|
+
Hand a streamed body to the browser as a `Response` from the server function and read it there. The example below streams a completion through the `askClaude` action declared above; the same shape applies to any vendor endpoint that streams.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const streamAnswer = createServerFn({ method: 'POST' })
|
|
180
|
+
.validator((data: { prompt: string }) => data)
|
|
181
|
+
.handler(async ({ data }) => {
|
|
182
|
+
const claude = integration.createFetcher(askClaude)
|
|
183
|
+
const res = await claude({
|
|
184
|
+
stream: true,
|
|
185
|
+
headers: { 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01' },
|
|
186
|
+
body: JSON.stringify({
|
|
187
|
+
model: 'claude-opus-5-5',
|
|
188
|
+
max_tokens: 1024,
|
|
189
|
+
stream: true,
|
|
190
|
+
messages: [{ role: 'user', content: data.prompt }],
|
|
191
|
+
}),
|
|
192
|
+
})
|
|
193
|
+
if (!res.ok) throw new Error(`AI error: ${res.status} ${await res.text()}`)
|
|
194
|
+
return new Response(res.body, { headers: { 'Content-Type': 'text/event-stream' } })
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
On the client, call the server function's URL with `fetch` and read `response.body` with a `ReadableStream` reader; each chunk arrives in the vendor's own format, for an event stream one or more `event:`/`data:` frames. Use `stream: true` only together with a request that asks the vendor to stream: without it the vendor answers in one piece, which still works but gains nothing.
|
|
199
|
+
|
|
200
|
+
`stream: true` is not available for REST_API integrations; those always answer in one piece.
|
|
201
|
+
|
|
130
202
|
## Base URLs by integration type
|
|
131
203
|
|
|
132
204
|
Match the `integrationType` from the attached integrations section to find the base URL:
|
|
@@ -198,3 +270,5 @@ If the API the user needs is not in the table above, call it directly from a ser
|
|
|
198
270
|
**Not checking `res.ok`.** The fetcher returns the proxied response as-is. A 4xx or 5xx from the third-party API is not thrown — call `res.ok` or `res.status` before calling `res.json()`.
|
|
199
271
|
|
|
200
272
|
**Reusing one action for a different endpoint.** An action authorizes one `method` and one endpoint `url`. A different endpoint belongs to a different action — declare one for it, with its own audience.
|
|
273
|
+
|
|
274
|
+
**Reading a streamed body with `res.json()`.** With `stream: true` the body is an event stream, not one JSON document. Return `res.body` in a `Response` or read it with a stream reader; `res.json()` waits for the end and then fails to parse.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.68-alpha.0
|
|
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.
|
|
14
|
+
library_version: 0.0.68-alpha.0
|
|
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.
|
|
16
|
+
library_version: 0.0.68-alpha.0
|
|
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. The same holds for any helper that calls `ds`: declare it inside the handler. A function at module level that calls `ds` keeps the server-only import in the browser bundle, and every page stops loading until it moves. `defineQuery` 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.
|
|
16
|
+
library_version: 0.0.68-alpha.0
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
**Boundary.** `defineVisibility`, the access functions and the error predicates come from `@softr/sdk` and work anywhere.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.68-alpha.0
|
|
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.
|