@softr/sdk 0.0.26-alpha-0 → 0.0.27
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/cli.js +618 -2
- package/dist/codegen/softr-env.js +2 -1
- package/dist/package.js +1 -1
- package/package.json +10 -32
- package/skills/softr-sdk/SKILL.md +11 -11
- 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 +56 -76
- package/skills/softr-sdk/integrations/SKILL.md +140 -0
- package/skills/softr-sdk/reading-records/SKILL.md +93 -77
- package/skills/softr-sdk/reading-records/references/field-types.md +48 -41
- package/skills/softr-sdk/reading-records/references/query-builder.md +70 -66
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/dist/codegen/fetch-metadata.js +0 -46
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +0 -178
- package/skills/softr-sdk/charts-and-metrics/references/aggregations.md +0 -76
package/package.json
CHANGED
|
@@ -1,36 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softr/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
|
-
".": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"./server": {
|
|
15
|
-
"types": "./dist/server.d.ts",
|
|
16
|
-
"import": "./dist/server.js"
|
|
17
|
-
},
|
|
18
|
-
"./vite": {
|
|
19
|
-
"types": "./dist/vite.d.ts",
|
|
20
|
-
"import": "./dist/vite.js"
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
"bin": {
|
|
24
|
-
"softr-sdk": "./dist/cli.js"
|
|
25
|
-
},
|
|
26
|
-
"files": [
|
|
27
|
-
"dist",
|
|
28
|
-
"skills",
|
|
29
|
-
"!skills/_artifacts"
|
|
30
|
-
],
|
|
31
|
-
"publishConfig": {
|
|
32
|
-
"access": "public"
|
|
33
|
-
},
|
|
6
|
+
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
|
|
7
|
+
"./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js" },
|
|
8
|
+
"./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js" },
|
|
9
|
+
"./vite": { "types": "./dist/vite.d.ts", "import": "./dist/vite.js" }
|
|
10
|
+
},
|
|
11
|
+
"bin": { "softr-sdk": "./dist/cli.js" },
|
|
12
|
+
"files": ["dist", "skills", "!skills/_artifacts"],
|
|
13
|
+
"publishConfig": { "access": "public" },
|
|
34
14
|
"scripts": {
|
|
35
15
|
"build": "tsdown",
|
|
36
16
|
"test": "node --import ./src/test-utils/setup.ts --test",
|
|
@@ -62,9 +42,7 @@
|
|
|
62
42
|
"optional": true
|
|
63
43
|
}
|
|
64
44
|
},
|
|
65
|
-
"keywords": [
|
|
66
|
-
"tanstack-intent"
|
|
67
|
-
],
|
|
45
|
+
"keywords": ["tanstack-intent"],
|
|
68
46
|
"intent": {
|
|
69
47
|
"version": 1,
|
|
70
48
|
"repo": "@softr/sdk",
|
|
@@ -5,13 +5,13 @@ description: >-
|
|
|
5
5
|
four entrypoints (@softr/sdk, /server, /client, /vite) and the client-server
|
|
6
6
|
boundary between them, softrAuthMiddleware, getSoftrContext,
|
|
7
7
|
initialSoftrContext, SoftrContext, and which sub-skill to load for route
|
|
8
|
-
visibility, the current user, reading records,
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
visibility, the current user, reading records, writing records, or auth. Load
|
|
9
|
+
this when unsure which sub-skill applies, or when touching the wiring; each
|
|
10
|
+
sub-skill stands alone for its own task.
|
|
11
11
|
metadata:
|
|
12
12
|
type: core
|
|
13
13
|
library: softr-sdk
|
|
14
|
-
library_version: 0.0.
|
|
14
|
+
library_version: 0.0.27
|
|
15
15
|
---
|
|
16
16
|
|
|
17
17
|
# Softr SDK core
|
|
@@ -21,7 +21,7 @@ metadata:
|
|
|
21
21
|
> **CRITICAL**: The SDK is split across four entrypoints and the split is enforced. `ds` and `auth` exist only on `@softr/sdk/server` and only run inside a `createServerFn` handler. Importing them anywhere else fails the build.
|
|
22
22
|
> **CRITICAL**: Every route file must declare its audience with `defineVisibility`. There is no default and nothing is inherited from a sibling. A route file without a declaration is a lint error.
|
|
23
23
|
> **CRITICAL**: The app is already wired. `src/start.ts`, `src/router.tsx` and `src/routes/__root.tsx` register the middleware and resolve the session. Do not add a second registration.
|
|
24
|
-
> **CRITICAL**: `workflow.trigger`
|
|
24
|
+
> **CRITICAL**: `workflow.trigger` is exploratory. It exists on the entrypoints below and typechecks, but its endpoint is not final. Do not build a feature on it. Tell the user the capability is not ready and ask how they want to proceed.
|
|
25
25
|
|
|
26
26
|
Each sub-skill stands alone for its task and repeats the boundary above, so most work needs **one** sub-skill and not this file. Load this one when you are unsure which sub-skill applies, or when the wiring below is what you are touching. Add a second sub-skill only when the work genuinely crosses that boundary: a page that lists records behind a group restriction needs `reading-records` plus `routes-and-visibility`.
|
|
27
27
|
|
|
@@ -29,7 +29,7 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
|
|
|
29
29
|
|
|
30
30
|
| Import | Runs | Holds |
|
|
31
31
|
| --- | --- | --- |
|
|
32
|
-
| `@softr/sdk` | Both | `
|
|
32
|
+
| `@softr/sdk` | Both | `defineVisibility`, `q`, `useCurrentUser`, `usePermissions`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
|
|
33
33
|
| `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration` |
|
|
34
34
|
| `@softr/sdk/client` | Browser only | Nothing yet, reserved for browser-only utilities |
|
|
35
35
|
| `@softr/sdk/vite` | Build | `softr()`, already in `vite.config.ts` |
|
|
@@ -41,9 +41,9 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
|
|
|
41
41
|
| Add a route, or change who can see one | [routes-and-visibility/SKILL.md](./routes-and-visibility/SKILL.md) |
|
|
42
42
|
| Read the signed-in visitor or what they may do | [current-user/SKILL.md](./current-user/SKILL.md) |
|
|
43
43
|
| Fetch records from a datasource | [reading-records/SKILL.md](./reading-records/SKILL.md) |
|
|
44
|
-
| Chart, aggregate or count records | [charts-and-metrics/SKILL.md](./charts-and-metrics/SKILL.md) |
|
|
45
44
|
| Create, update or delete a record | [data-mutations/SKILL.md](./data-mutations/SKILL.md) |
|
|
46
45
|
| Sign a visitor in, out, or up | [auth/SKILL.md](./auth/SKILL.md) |
|
|
46
|
+
| Call a connected third-party API (Slack, Stripe, Gmail, …) | [integrations/SKILL.md](./integrations/SKILL.md) |
|
|
47
47
|
|
|
48
48
|
## Quick Decision Tree
|
|
49
49
|
|
|
@@ -57,15 +57,15 @@ Showing the visitor's name, or hiding UI they cannot use?
|
|
|
57
57
|
Putting records on a page, in a table, a list or a detail view?
|
|
58
58
|
load softr-sdk/reading-records
|
|
59
59
|
|
|
60
|
-
Drawing a chart, a stat tile, or any aggregated number?
|
|
61
|
-
load softr-sdk/charts-and-metrics
|
|
62
|
-
|
|
63
60
|
Submitting a form, editing a record, or deleting one?
|
|
64
61
|
load softr-sdk/data-mutations
|
|
65
62
|
|
|
66
63
|
Wiring a login, signup, or sign-out?
|
|
67
64
|
load softr-sdk/auth
|
|
68
65
|
|
|
66
|
+
Calling a connected third-party API (Slack, Stripe, Gmail, Google Calendar, …)?
|
|
67
|
+
load softr-sdk/integrations
|
|
68
|
+
|
|
69
69
|
Writing a server function, loader, or middleware in general?
|
|
70
70
|
not a Softr concern. Use the TanStack Start skills.
|
|
71
71
|
```
|
|
@@ -128,4 +128,4 @@ type User = {
|
|
|
128
128
|
|
|
129
129
|
## Version
|
|
130
130
|
|
|
131
|
-
Targets `@softr/sdk` v0.0.
|
|
131
|
+
Targets `@softr/sdk` v0.0.26.
|
|
@@ -16,7 +16,7 @@ description: >-
|
|
|
16
16
|
metadata:
|
|
17
17
|
type: sub-skill
|
|
18
18
|
library: softr-sdk
|
|
19
|
-
library_version: 0.0.
|
|
19
|
+
library_version: 0.0.27
|
|
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. `defineVisibility`, the hooks and every type come from `@softr/sdk`. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -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.27
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**Boundary.** `useCurrentUser`, `usePermissions` and every type come from `@softr/sdk` and work anywhere; `ds` and `auth` live on `@softr/sdk/server` and run only inside a `createServerFn` handler. The app is already wired — `src/start.ts` registers `softrAuthMiddleware`, `src/routes/__root.tsx` publishes the visitor as route context — never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
name: data-mutations
|
|
3
3
|
description: >-
|
|
4
4
|
Create, update and delete records in a Softr datasource from inside a server
|
|
5
|
-
function:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
returns. Writes only. Reads, charts and metrics are not covered.
|
|
5
|
+
function: ds.create, ds.update and ds.delete from @softr/sdk/server, the data
|
|
6
|
+
payload key, select field aliasing on the way in and out, and what each call
|
|
7
|
+
returns or throws. Writes only. Reads, metrics and chart data are not covered.
|
|
9
8
|
metadata:
|
|
10
9
|
type: sub-skill
|
|
11
10
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
11
|
+
library_version: 0.0.27
|
|
13
12
|
---
|
|
14
13
|
|
|
15
14
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `q` and every type come from `@softr/sdk` and can be imported anywhere. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -23,101 +22,93 @@ metadata:
|
|
|
23
22
|
```ts
|
|
24
23
|
import { createServerFn } from '@tanstack/react-start'
|
|
25
24
|
import { z } from 'zod'
|
|
26
|
-
import { defineAction } from '@softr/sdk'
|
|
27
25
|
import { ds } from '@softr/sdk/server'
|
|
26
|
+
import { q } from '@softr/sdk'
|
|
28
27
|
```
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
Pass a zod schema straight to `.validator`. Start accepts it and types `data` from it.
|
|
31
30
|
|
|
32
|
-
|
|
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.
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
const createProject = defineAction({
|
|
38
|
-
id: 'createProject',
|
|
39
|
-
datasource: 'c9a51f22-3b8e-4d17-a6c0-5e92d84b7f31',
|
|
40
|
-
operation: 'create',
|
|
41
|
-
fields: {
|
|
42
|
-
name: 'fZk3Qm9pLw2Xa1', // SINGLE_LINE_TEXT
|
|
43
|
-
status: 'Ru7bVx2cNq0Ed9', // SELECT, choice ids 'draft' | 'active' | 'archived'
|
|
44
|
-
dueDate: 'Xp2dLh6vKz9Fa3', // DATETIME
|
|
45
|
-
},
|
|
46
|
-
permission: { userGroup: 'LOGGED_IN_USERS' },
|
|
47
|
-
})
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
The declaration follows the same rules as a read action, spelled out in [reading-records](../reading-records/SKILL.md): `datasource` is the opaque UUID from the table's registration, never derived from its name; the keys of `fields` are names you choose and the values are opaque field ids; everything sits at module scope with every property a literal.
|
|
51
|
-
|
|
52
|
-
The action's `fields` bound the write: `data` can only name them, and the returned record only carries them. A delete action declares no `fields`; giving it some is a type error.
|
|
33
|
+
**The datasource id must be a string literal at the call site.**
|
|
53
34
|
|
|
54
35
|
## Core Patterns
|
|
55
36
|
|
|
56
37
|
### Create a record
|
|
57
38
|
|
|
58
39
|
```ts
|
|
59
|
-
const NewProject = z.object({ name: z.string(),
|
|
40
|
+
const NewProject = z.object({ name: z.string(), status: z.string() })
|
|
60
41
|
|
|
61
|
-
const
|
|
42
|
+
const createProject = createServerFn({ method: 'POST' })
|
|
62
43
|
.validator(NewProject)
|
|
63
44
|
.handler(async ({ data }) =>
|
|
64
|
-
ds.create(
|
|
65
|
-
data
|
|
45
|
+
ds.create('8ffb4814-b028-4496-b679-7face2167ebc', {
|
|
46
|
+
data,
|
|
47
|
+
select: q.select({ name: 'fZk3Qm9pLw2Xa1', status: 'Ru7bVx2cNq0Ed9' }),
|
|
66
48
|
}),
|
|
67
49
|
)
|
|
68
50
|
```
|
|
69
51
|
|
|
70
|
-
|
|
52
|
+
Resolves to `{ id, fields }`, and on a Softr table `fields` holds **only the keys you wrote** — not the rest of the record. See "What a write answers with" below.
|
|
53
|
+
|
|
54
|
+
**The payload key is `data`, not `fields`.** A record answers with `fields`, which is where the confusion comes from, but the option you pass is `data`. `CreateOptions` requires it, so a call keyed by `fields` does not type-check.
|
|
55
|
+
|
|
56
|
+
**`data` speaks whatever `select` maps.** The keys above are the schema's own names because `select` maps them to field ids, which is why the validated input can go straight through. With no `select`, `data` has to be keyed by raw field ids instead.
|
|
71
57
|
|
|
72
58
|
### Update a record
|
|
73
59
|
|
|
74
60
|
```ts
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
fields: { status: 'Ru7bVx2cNq0Ed9', dueDate: 'Xp2dLh6vKz9Fa3' },
|
|
80
|
-
permission: { userGroup: 'LOGGED_IN_USERS' },
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
const archiveProject = createServerFn({ method: 'POST' })
|
|
84
|
-
.validator(z.object({ id: z.string() }))
|
|
61
|
+
const Rename = z.object({ id: z.string(), name: z.string() })
|
|
62
|
+
|
|
63
|
+
const renameProject = createServerFn({ method: 'POST' })
|
|
64
|
+
.validator(Rename)
|
|
85
65
|
.handler(async ({ data }) =>
|
|
86
|
-
ds.update(
|
|
66
|
+
ds.update('8ffb4814-b028-4496-b679-7face2167ebc', {
|
|
67
|
+
id: data.id,
|
|
68
|
+
data: { name: data.name },
|
|
69
|
+
select: q.select({ name: 'fZk3Qm9pLw2Xa1' }),
|
|
70
|
+
}),
|
|
87
71
|
)
|
|
88
72
|
```
|
|
89
73
|
|
|
90
|
-
The record id is its own option
|
|
74
|
+
The record id is its own option, so keep it out of `data`.
|
|
75
|
+
|
|
76
|
+
`update` writes the keys in `data` and leaves every other field alone, so a field you leave out is a field you are not writing. An empty `id` throws.
|
|
77
|
+
|
|
78
|
+
### What a write answers with
|
|
79
|
+
|
|
80
|
+
`create` and `update` resolve to `{ id, fields }`, but `fields` is **not the record**. On a Softr table it carries only the keys you sent, aliased through `select`; a status change answers with `{ id, fields: { status } }` and nothing else. (Other datasource types may answer with more. Rely on neither.)
|
|
91
81
|
|
|
92
|
-
|
|
82
|
+
So the answer is a receipt, not a replacement for what the page holds. Putting it where a loader-fetched record was blanks every column you did not write. Two honest ways to show the result:
|
|
83
|
+
|
|
84
|
+
- **A quick change to one field** — a status select, a card dragged to another column, an archive toggle — is optimistic: change that one field in your own state immediately, keep the rest of the record as the loader gave it, save in the background, and on a rejected write restore the old value with a clear message. Do not `router.invalidate()` or re-fetch after a successful write; the local change is already what the user sees.
|
|
85
|
+
- **A saved form, or anything that changes several fields**, refreshes the route's data — `router.invalidate()`, see `@tanstack/router-core#router-core/data-loading` — so the list or detail re-reads through its own `select`.
|
|
86
|
+
|
|
87
|
+
Do not re-read the one record with `ds.get` for either: it takes no `select`, so it answers with raw field ids.
|
|
88
|
+
|
|
89
|
+
### Writing a choice field
|
|
90
|
+
|
|
91
|
+
A `SELECT` or `LINKED_RECORD` field reads back as `{ id, label }` (see [field-types](../reading-records/references/field-types.md)). Writing one accepts the option's `id`, its `label`, or `{ id, label }`, matched case-insensitively.
|
|
92
|
+
|
|
93
|
+
**A value that matches no option is not an error — it silently adds a new option to the field.** A typo'd label leaves a junk choice in the schema. So write `{ label }` (or the `{ id, label }` you read), copy labels from the table's schema verbatim, and never invent one.
|
|
93
94
|
|
|
94
95
|
### Delete a record
|
|
95
96
|
|
|
96
97
|
```ts
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
const removeProject = createServerFn({ method: 'POST' })
|
|
105
|
-
.validator(z.object({ id: z.string() }))
|
|
106
|
-
.handler(async ({ data }) => {
|
|
107
|
-
await ds.delete(deleteProject, { id: data.id })
|
|
108
|
-
})
|
|
98
|
+
const RecordId = z.object({ id: z.string() })
|
|
99
|
+
|
|
100
|
+
const deleteProject = createServerFn({ method: 'POST' })
|
|
101
|
+
.validator(RecordId)
|
|
102
|
+
.handler(async ({ data }) => ds.delete('8ffb4814-b028-4496-b679-7face2167ebc', data.id))
|
|
109
103
|
```
|
|
110
104
|
|
|
111
|
-
|
|
105
|
+
**`delete` takes the record id as a positional argument**, unlike `create` and `update`, which take an options object. It resolves to nothing.
|
|
112
106
|
|
|
113
|
-
###
|
|
107
|
+
### Naming fields with `select`
|
|
114
108
|
|
|
115
|
-
|
|
109
|
+
`select` works in both directions on a write. Going in, it turns your names into Softr field ids. Coming back, it turns the returned field ids into your names. Without it, `data` has to be keyed by raw field ids and the returned record answers with them too.
|
|
116
110
|
|
|
117
|
-
|
|
118
|
-
- CHECKBOX takes a boolean. Dates take ISO strings, never `Date` instances.
|
|
119
|
-
- ATTACHMENT takes `{ url }`, optionally with `filename`, `id` and `type`.
|
|
120
|
-
- A multi-entry field takes an array of the value.
|
|
111
|
+
The map has to cover every key in `data`. A key it does not map is not translated, so the write does not land on the field you meant.
|
|
121
112
|
|
|
122
113
|
### Writing on behalf of the visitor
|
|
123
114
|
|
|
@@ -125,16 +116,6 @@ Stamping a record with who created it, or scoping an update to the person making
|
|
|
125
116
|
|
|
126
117
|
## Failure Modes
|
|
127
118
|
|
|
128
|
-
**A write with the wrong action.** Each operation needs an action declared for it. `ds.update` with a read action throws `action "..." is declared as read, not update` at the first call, not at build time.
|
|
129
|
-
|
|
130
|
-
**A payload keyed `fields`.** The option is `data`. A record answers with `fields`, which is where the confusion comes from, but the option you pass is `data`, and the options type requires it.
|
|
131
|
-
|
|
132
|
-
**A SELECT or LINKED_RECORD written as a bare string.** The write shape is `{ id }`. Writing `status: 'archived'` where `status: { id: 'archived' }` belongs is a type error with codegen in place and a silent misfire without it.
|
|
133
|
-
|
|
134
|
-
**Feeding a read value back into a write.** A read returns `{ id, label }`; a write takes `{ id }` only. Strip the label before writing the value back.
|
|
135
|
-
|
|
136
|
-
**A field missing from the action.** `data` can only name the action's own `fields`. A field you need to write must be declared on the action, and a delete action declares none.
|
|
137
|
-
|
|
138
119
|
**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.
|
|
139
120
|
|
|
140
121
|
**A datasource id stored in a variable.** 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.
|
|
@@ -153,6 +134,5 @@ Stamping a record with who created it, or scoping an update to the person making
|
|
|
153
134
|
|
|
154
135
|
## References
|
|
155
136
|
|
|
156
|
-
- [reading-records/SKILL.md](../reading-records/SKILL.md):
|
|
157
|
-
- [reading-records/references/field-types.md](../reading-records/references/field-types.md): the value shape every field reads back as
|
|
137
|
+
- [reading-records/SKILL.md](../reading-records/SKILL.md): fetching the record a write is about to change
|
|
158
138
|
- [current-user/SKILL.md](../current-user/SKILL.md): reading the visitor off `context` to stamp or scope a write
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: integrations
|
|
3
|
+
description: >-
|
|
4
|
+
Call a third-party service connected to the app as an integration: integration.fetch
|
|
5
|
+
from @softr/sdk/server, reading the datasource id and integration type from the
|
|
6
|
+
"Attached data sources" prompt section, looking up the base URL from the reference
|
|
7
|
+
table by integrationType, and constructing the full API URL from base URL plus the
|
|
8
|
+
path and query params the model knows from training. Check res.ok before res.json().
|
|
9
|
+
metadata:
|
|
10
|
+
type: sub-skill
|
|
11
|
+
library: softr-sdk
|
|
12
|
+
library_version: 0.0.27
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
**Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
16
|
+
|
|
17
|
+
# Integrations
|
|
18
|
+
|
|
19
|
+
`integration.fetch` calls a third-party API through Softr's authenticated proxy. It handles authorization automatically — you never set API keys or auth headers yourself.
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
25
|
+
import { integration } from '@softr/sdk/server'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Reading connected integrations from the prompt
|
|
29
|
+
|
|
30
|
+
When the user attaches integrations to the chat, they appear in the `## Attached data sources` section of the prompt in this form:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
- "Gmail" — database `<databaseId>`, integration `<integrationId>`, type `GMAIL`
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Use `databaseId` as the first argument to `integration.fetch`. Use `integrationType` (e.g. `GMAIL`) to look up the base URL from the reference table below — then construct the full URL by appending the path and query parameters your training knows for that API.
|
|
37
|
+
|
|
38
|
+
## Core pattern
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const getCalendarEvents = createServerFn().handler(async () => {
|
|
42
|
+
const now = new Date().toISOString()
|
|
43
|
+
const res = await integration.fetch(
|
|
44
|
+
'25d60213-ef4f-4fa0-b094-a46931b2d120',
|
|
45
|
+
`https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=${encodeURIComponent(now)}&singleEvents=true&orderBy=startTime&maxResults=10`,
|
|
46
|
+
)
|
|
47
|
+
if (!res.ok) throw new Error(`Integration request failed: ${res.status} ${await res.text()}`)
|
|
48
|
+
return res.json()
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The id (`25d60213-…`) is the `databaseId` from the attached data sources section. The base URL (`https://www.googleapis.com`) is looked up from the reference table below by matching `integrationType` (`GOOGLE_CALENDAR`). The path and query params come from your knowledge of the Google Calendar REST API.
|
|
53
|
+
|
|
54
|
+
## POST request
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const sendSlackMessage = createServerFn({ method: 'POST' })
|
|
58
|
+
.validator((data: { channel: string; text: string }) => data)
|
|
59
|
+
.handler(async ({ data }) => {
|
|
60
|
+
const res = await integration.fetch(
|
|
61
|
+
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
|
62
|
+
'https://slack.com/api/chat.postMessage',
|
|
63
|
+
{
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({ channel: data.channel, text: data.text }),
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
if (!res.ok) throw new Error(`Slack error: ${res.status} ${await res.text()}`)
|
|
70
|
+
return res.json()
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`integration.fetch` accepts the same `RequestInit` options as the native `fetch`. Do not pass `Authorization` headers — Softr injects the integration's credentials automatically.
|
|
75
|
+
|
|
76
|
+
## Base URLs by integration type
|
|
77
|
+
|
|
78
|
+
Match the `integrationType` from the attached data sources section to find the base URL:
|
|
79
|
+
|
|
80
|
+
| integrationType | Base URL |
|
|
81
|
+
|---|---|
|
|
82
|
+
| ANTHROPIC_AI | `https://api.anthropic.com` |
|
|
83
|
+
| APOLLO | `https://api.apollo.io` |
|
|
84
|
+
| APIFY | `https://api.apify.com` |
|
|
85
|
+
| ASANA | `https://app.asana.com` |
|
|
86
|
+
| ATTIO | `https://api.attio.com` |
|
|
87
|
+
| BASEROW | `https://api.baserow.io` |
|
|
88
|
+
| BITLY | `https://api-ssl.bitly.com` |
|
|
89
|
+
| BREVO | `https://api.brevo.com` |
|
|
90
|
+
| CALCOM | `https://api.cal.com` |
|
|
91
|
+
| CALENDLY | `https://api.calendly.com` |
|
|
92
|
+
| COHERE | `https://api.cohere.com` |
|
|
93
|
+
| DEEPL | `https://api.deepl.com` |
|
|
94
|
+
| DEEP_SEEK_AI | `https://api.deepseek.com` |
|
|
95
|
+
| DOCS_AUTOMATOR | `https://api.docsautomator.co` |
|
|
96
|
+
| DOCUMINT | `https://api.documint.me` |
|
|
97
|
+
| ELASTIC_EMAIL | `https://api.elasticemail.com` |
|
|
98
|
+
| ELEVEN_LABS | `https://api.elevenlabs.io` |
|
|
99
|
+
| FATHOM | `https://api.fathom.ai` |
|
|
100
|
+
| FIRECRAWL | `https://api.firecrawl.dev` |
|
|
101
|
+
| GEMINI_AI | `https://generativelanguage.googleapis.com` |
|
|
102
|
+
| GMAIL | `https://gmail.googleapis.com` |
|
|
103
|
+
| GOOGLE_CALENDAR | `https://www.googleapis.com` |
|
|
104
|
+
| GRANOLA | `https://public-api.granola.ai` |
|
|
105
|
+
| HUNTER | `https://api.hunter.io` |
|
|
106
|
+
| INSTANTLY | `https://api.instantly.ai` |
|
|
107
|
+
| LEMLIST | `https://api.lemlist.com` |
|
|
108
|
+
| LINEAR | `https://api.linear.app` |
|
|
109
|
+
| LINKUP | `https://api.linkup.so` |
|
|
110
|
+
| LOOPS | `https://app.loops.so` |
|
|
111
|
+
| MESSAGE_BIRD | `https://rest.messagebird.com` |
|
|
112
|
+
| MISTRAL_AI | `https://api.mistral.ai` |
|
|
113
|
+
| OPEN_AI | `https://api.openai.com` |
|
|
114
|
+
| OUTLOOK | `https://graph.microsoft.com` |
|
|
115
|
+
| PADDLE | `https://api.paddle.com` |
|
|
116
|
+
| PINECONE | `https://api.pinecone.io` |
|
|
117
|
+
| PIPELINE_CRM | `https://api.pipelinecrm.com` |
|
|
118
|
+
| POSTMARK | `https://api.postmarkapp.com` |
|
|
119
|
+
| REPLICATE | `https://api.replicate.com` |
|
|
120
|
+
| RESEND | `https://api.resend.com` |
|
|
121
|
+
| SLACK | `https://slack.com/api` |
|
|
122
|
+
| SMARTLEAD | `https://server.smartlead.ai` |
|
|
123
|
+
| STRIPE | `https://api.stripe.com` |
|
|
124
|
+
| TELEGRAM | `https://api.telegram.org` |
|
|
125
|
+
| TINYURL | `https://api.tinyurl.com` |
|
|
126
|
+
| TWILIO | `https://api.twilio.com` |
|
|
127
|
+
| XERO | `https://api.xero.com` |
|
|
128
|
+
| ZOOM | `https://api.zoom.us` |
|
|
129
|
+
|
|
130
|
+
## When the service is not a Softr integration
|
|
131
|
+
|
|
132
|
+
If the API the user needs is not in the table above, call it directly from a server function using the native `fetch`. The user supplies their own credentials and the call lives entirely in the server function handler.
|
|
133
|
+
|
|
134
|
+
## Failure modes
|
|
135
|
+
|
|
136
|
+
**Calling from client code.** `integration` is server-only. Importing it into a component or calling it outside a `createServerFn` handler fails the build or has no request context to act on.
|
|
137
|
+
|
|
138
|
+
**Setting Authorization headers.** Softr injects credentials automatically. A manually set `Authorization` header overrides the injected one and the request is rejected by the third-party API.
|
|
139
|
+
|
|
140
|
+
**Not checking `res.ok`.** `integration.fetch` 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()`.
|