@softr/sdk 0.0.30 → 0.0.32
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/action.d.ts +104 -26
- package/dist/action.js +45 -8
- package/dist/auth/platform.js +2 -1
- package/dist/codegen/fetch-metadata.js +1 -0
- package/dist/core/integration.d.ts +5 -14
- package/dist/core/integration.js +7 -8
- package/dist/core/workflow.d.ts +4 -1
- package/dist/core/workflow.js +22 -7
- package/dist/datasource/chart.d.ts +5 -5
- package/dist/datasource/chart.js +1 -0
- package/dist/datasource/fields.d.ts +1 -1
- package/dist/datasource/record.d.ts +6 -6
- package/dist/datasource/record.js +6 -5
- package/dist/fetch.js +21 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/logger.js +15 -0
- package/dist/package.js +1 -1
- package/package.json +1 -1
- package/skills/softr-sdk/SKILL.md +6 -2
- package/skills/softr-sdk/auth/SKILL.md +1 -1
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +3 -3
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/integrations/SKILL.md +38 -24
- package/skills/softr-sdk/record-mutations/SKILL.md +10 -10
- package/skills/softr-sdk/record-queries/SKILL.md +8 -8
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +116 -0
package/dist/action.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { SoftrUserGroupId } from "./auth/user.js";
|
|
|
2
2
|
|
|
3
3
|
//#region src/action.d.ts
|
|
4
4
|
type ActionOperation = "read" | "create" | "update" | "delete";
|
|
5
|
-
type
|
|
5
|
+
type ActionPermissions = {
|
|
6
6
|
userGroup: "ALL_USERS";
|
|
7
7
|
} | {
|
|
8
8
|
userGroup: "NON_LOGGED_IN_USERS";
|
|
@@ -10,6 +10,11 @@ type ActionPermission = {
|
|
|
10
10
|
userGroup: "LOGGED_IN_USERS";
|
|
11
11
|
userGroupIds?: Array<SoftrUserGroupId>;
|
|
12
12
|
};
|
|
13
|
+
type Action<TResource> = {
|
|
14
|
+
id: string;
|
|
15
|
+
resource: TResource;
|
|
16
|
+
permissions: ActionPermissions;
|
|
17
|
+
};
|
|
13
18
|
/**
|
|
14
19
|
* Describes the datasources available in the app and their read, create and update types.
|
|
15
20
|
*
|
|
@@ -42,7 +47,7 @@ type FieldIdOf<TDatasource$1, TOperation$1> = keyof SoftrDatasources extends nev
|
|
|
42
47
|
type ReadFieldIdOf<TDatasource$1> = keyof SoftrDatasources extends never ? string : keyof ReadOf<TDatasource$1> & string;
|
|
43
48
|
type ActionShape = {
|
|
44
49
|
id: string;
|
|
45
|
-
|
|
50
|
+
datasourceId: SoftrDatasourceId;
|
|
46
51
|
operation: ActionOperation;
|
|
47
52
|
fields?: Record<string, string>;
|
|
48
53
|
};
|
|
@@ -51,40 +56,48 @@ type ReadShape = ActionShape & {
|
|
|
51
56
|
fields: Record<string, string>;
|
|
52
57
|
};
|
|
53
58
|
type ActionData<TShape extends ActionShape> = keyof SoftrDatasources extends never ? Record<string, unknown> : TShape extends {
|
|
54
|
-
|
|
59
|
+
datasourceId: infer TDatasource;
|
|
55
60
|
operation: infer TOperation;
|
|
56
61
|
fields: infer TFields;
|
|
57
62
|
} ? { [Alias in keyof TFields]?: WriteOf<TDatasource, TOperation>[TFields[Alias] & keyof WriteOf<TDatasource, TOperation>] } : Record<string, unknown>;
|
|
58
63
|
type ActionRecord<TShape extends ActionShape> = keyof SoftrDatasources extends never ? Record<string, unknown> : TShape extends {
|
|
59
|
-
|
|
64
|
+
datasourceId: infer TDatasource;
|
|
60
65
|
fields: infer TFields;
|
|
61
66
|
} ? { [Alias in keyof TFields]: ReadOf<TDatasource>[TFields[Alias] & keyof ReadOf<TDatasource>] } : Record<string, unknown>;
|
|
62
67
|
declare const brand: unique symbol;
|
|
63
|
-
type
|
|
64
|
-
type: "
|
|
68
|
+
type DatasourceResource = {
|
|
69
|
+
type: "DATASOURCE";
|
|
70
|
+
datasourceId: SoftrDatasourceId;
|
|
71
|
+
operation: ActionOperation;
|
|
72
|
+
fields?: Record<string, string>;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* A datasource-backed action (query or mutation). The `TShape` brand carries field and
|
|
76
|
+
* operation type info for compile-time checking of `ds.*` calls.
|
|
77
|
+
*/
|
|
78
|
+
type DatasourceAction<TShape extends ActionShape = ActionShape> = Action<DatasourceResource> & {
|
|
65
79
|
id: TShape["id"];
|
|
66
|
-
} & {
|
|
67
80
|
readonly [brand]: TShape;
|
|
68
81
|
};
|
|
69
82
|
type WriteDeclaration<TId extends string, TDatasource$1, TOperation$1, TFields$1> = {
|
|
70
83
|
id: TId;
|
|
71
|
-
|
|
84
|
+
datasourceId: TDatasource$1;
|
|
72
85
|
operation: TOperation$1;
|
|
73
86
|
fields: TFields$1;
|
|
74
|
-
|
|
87
|
+
permissions: ActionPermissions;
|
|
75
88
|
};
|
|
76
89
|
type ReadDeclaration<TId extends string, TDatasource$1, TFields$1> = {
|
|
77
90
|
id: TId;
|
|
78
|
-
|
|
91
|
+
datasourceId: TDatasource$1;
|
|
79
92
|
fields: TFields$1;
|
|
80
|
-
|
|
93
|
+
permissions: ActionPermissions;
|
|
81
94
|
};
|
|
82
95
|
type DeleteDeclaration<TId extends string, TDatasource$1> = {
|
|
83
96
|
id: TId;
|
|
84
|
-
|
|
97
|
+
datasourceId: TDatasource$1;
|
|
85
98
|
operation: "delete";
|
|
86
99
|
fields?: never;
|
|
87
|
-
|
|
100
|
+
permissions: ActionPermissions;
|
|
88
101
|
};
|
|
89
102
|
/**
|
|
90
103
|
* Declares a read: which datasource, which fields participate, and who may run it.
|
|
@@ -94,9 +107,9 @@ type DeleteDeclaration<TId extends string, TDatasource$1> = {
|
|
|
94
107
|
* @example
|
|
95
108
|
* const publishedPosts = defineQuery({
|
|
96
109
|
* id: "publishedPosts",
|
|
97
|
-
*
|
|
110
|
+
* datasourceId: "8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77",
|
|
98
111
|
* fields: { title: "V1StGXR8", author: "Z7pQm4Kd", publishedAt: "Xp2dLh6v" },
|
|
99
|
-
*
|
|
112
|
+
* permissions: { userGroup: "ALL_USERS" },
|
|
100
113
|
* })
|
|
101
114
|
*
|
|
102
115
|
* const { records } = await ds
|
|
@@ -104,9 +117,9 @@ type DeleteDeclaration<TId extends string, TDatasource$1> = {
|
|
|
104
117
|
* .where((q) => q.publishedAt.lte(today))
|
|
105
118
|
* .orderBy((q) => q.publishedAt.desc)
|
|
106
119
|
*/
|
|
107
|
-
declare function defineQuery<const TId extends string, const TDatasource$1 extends SoftrDatasourceId, const TFields$1 extends Record<string, ReadFieldIdOf<TDatasource$1>>>(definition: ReadDeclaration<TId, TDatasource$1, TFields$1>):
|
|
120
|
+
declare function defineQuery<const TId extends string, const TDatasource$1 extends SoftrDatasourceId, const TFields$1 extends Record<string, ReadFieldIdOf<TDatasource$1>>>(definition: ReadDeclaration<TId, TDatasource$1, TFields$1>): DatasourceAction<{
|
|
108
121
|
id: TId;
|
|
109
|
-
|
|
122
|
+
datasourceId: TDatasource$1;
|
|
110
123
|
operation: "read";
|
|
111
124
|
fields: TFields$1;
|
|
112
125
|
}>;
|
|
@@ -119,10 +132,10 @@ declare function defineQuery<const TId extends string, const TDatasource$1 exten
|
|
|
119
132
|
* @example
|
|
120
133
|
* const closeOrder = defineMutation({
|
|
121
134
|
* id: "closeOrder",
|
|
122
|
-
*
|
|
135
|
+
* datasourceId: "8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77",
|
|
123
136
|
* operation: "update",
|
|
124
137
|
* fields: { status: "V1StGXR8", closedAt: "Z7pQm4Kd" },
|
|
125
|
-
*
|
|
138
|
+
* permissions: { userGroup: "LOGGED_IN_USERS", userGroupIds: ["grp_staff"] },
|
|
126
139
|
* })
|
|
127
140
|
*
|
|
128
141
|
* await ds.update(closeOrder, { id, data: { status: "closed" } })
|
|
@@ -130,23 +143,88 @@ declare function defineQuery<const TId extends string, const TDatasource$1 exten
|
|
|
130
143
|
* @example
|
|
131
144
|
* const removeOrder = defineMutation({
|
|
132
145
|
* id: "removeOrder",
|
|
133
|
-
*
|
|
146
|
+
* datasourceId: "8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77",
|
|
134
147
|
* operation: "delete",
|
|
135
|
-
*
|
|
148
|
+
* permissions: { userGroup: "LOGGED_IN_USERS", userGroupIds: ["grp_staff"] },
|
|
136
149
|
* })
|
|
137
150
|
*
|
|
138
151
|
* await ds.delete(removeOrder, { id })
|
|
139
152
|
*/
|
|
140
|
-
declare function defineMutation<const TId extends string, const TDatasource$1 extends SoftrDatasourceId, const TOperation$1 extends "create" | "update", const TFields$1 extends Record<string, FieldIdOf<TDatasource$1, TOperation$1>>>(definition: WriteDeclaration<TId, TDatasource$1, TOperation$1, TFields$1>):
|
|
153
|
+
declare function defineMutation<const TId extends string, const TDatasource$1 extends SoftrDatasourceId, const TOperation$1 extends "create" | "update", const TFields$1 extends Record<string, FieldIdOf<TDatasource$1, TOperation$1>>>(definition: WriteDeclaration<TId, TDatasource$1, TOperation$1, TFields$1>): DatasourceAction<{
|
|
141
154
|
id: TId;
|
|
142
|
-
|
|
155
|
+
datasourceId: TDatasource$1;
|
|
143
156
|
operation: TOperation$1;
|
|
144
157
|
fields: TFields$1;
|
|
145
158
|
}>;
|
|
146
|
-
declare function defineMutation<const TId extends string, const TDatasource$1 extends SoftrDatasourceId>(definition: DeleteDeclaration<TId, TDatasource$1>):
|
|
159
|
+
declare function defineMutation<const TId extends string, const TDatasource$1 extends SoftrDatasourceId>(definition: DeleteDeclaration<TId, TDatasource$1>): DatasourceAction<{
|
|
147
160
|
id: TId;
|
|
148
|
-
|
|
161
|
+
datasourceId: TDatasource$1;
|
|
149
162
|
operation: "delete";
|
|
150
163
|
}>;
|
|
164
|
+
type WorkflowTemplateValue = "{LOGGED_IN_USER:EMAIL}" | "{LOGGED_IN_USER:NAME}" | "{LOGGED_IN_USER:RECORD_ID}" | `{LOGGED_IN_USER:${string}}` | "{CURRENT_DATE_TIME}" | "{CURRENT_DATE}";
|
|
165
|
+
type WorkflowBodyValue = WorkflowTemplateValue | (string & {}) | number | boolean | null;
|
|
166
|
+
type WorkflowResource = {
|
|
167
|
+
type: "WORKFLOW";
|
|
168
|
+
workflowId: string;
|
|
169
|
+
triggerId: string;
|
|
170
|
+
body?: Record<string, WorkflowBodyValue>;
|
|
171
|
+
};
|
|
172
|
+
type WorkflowAction<TId extends string = string> = Action<WorkflowResource> & {
|
|
173
|
+
id: TId;
|
|
174
|
+
};
|
|
175
|
+
type WorkflowDefinition<TId extends string> = {
|
|
176
|
+
id: TId;
|
|
177
|
+
workflowId: string;
|
|
178
|
+
triggerId: string;
|
|
179
|
+
permissions: ActionPermissions;
|
|
180
|
+
body?: Record<string, WorkflowBodyValue>;
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Declares a workflow trigger: which workflow, which trigger, who may run it, and any
|
|
184
|
+
* hardcoded data resolved server-side (e.g. logged-in user fields, current date).
|
|
185
|
+
*
|
|
186
|
+
* Pass the returned object to `workflow.trigger()` at runtime — optionally with additional
|
|
187
|
+
* payload. Values in `body` are resolved by the server and cannot be overridden at runtime.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* const notifyTeam = defineWorkflow({
|
|
191
|
+
* id: "notifyTeam",
|
|
192
|
+
* workflowId: "wf_abc123",
|
|
193
|
+
* triggerId: "tr_xyz789",
|
|
194
|
+
* permissions: { userGroup: "LOGGED_IN_USERS" },
|
|
195
|
+
* body: { requestedBy: "{LOGGED_IN_USER:EMAIL}", at: "{CURRENT_DATE_TIME}" },
|
|
196
|
+
* });
|
|
197
|
+
*
|
|
198
|
+
* await workflow.trigger(notifyTeam, { subject: "Hello" });
|
|
199
|
+
*/
|
|
200
|
+
declare function defineWorkflow<const TId extends string>(definition: WorkflowDefinition<TId>): WorkflowAction<TId>;
|
|
201
|
+
type IntegrationResource = {
|
|
202
|
+
type: "INTEGRATION";
|
|
203
|
+
datasourceId: string;
|
|
204
|
+
};
|
|
205
|
+
type IntegrationAction<TId extends string = string> = Action<IntegrationResource> & {
|
|
206
|
+
id: TId;
|
|
207
|
+
};
|
|
208
|
+
type IntegrationDefinition<TId extends string> = {
|
|
209
|
+
id: TId;
|
|
210
|
+
datasourceId: string;
|
|
211
|
+
permissions: ActionPermissions;
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* Declares an integration: which connected data source to call through, and who may call it.
|
|
215
|
+
*
|
|
216
|
+
* Pass the returned object to `integration.createFetcher()` to get a bound fetcher.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* const googleCalendar = defineIntegration({
|
|
220
|
+
* id: "googleCalendar",
|
|
221
|
+
* datasourceId: "25d60213-ef4f-4fa0-b094-a46931b2d120",
|
|
222
|
+
* permissions: { userGroup: "LOGGED_IN_USERS" },
|
|
223
|
+
* })
|
|
224
|
+
*
|
|
225
|
+
* const calendar = integration.createFetcher(googleCalendar)
|
|
226
|
+
* const res = await calendar('https://www.googleapis.com/calendar/v3/calendars/primary/events')
|
|
227
|
+
*/
|
|
228
|
+
declare function defineIntegration<const TId extends string>(definition: IntegrationDefinition<TId>): IntegrationAction<TId>;
|
|
151
229
|
//#endregion
|
|
152
|
-
export {
|
|
230
|
+
export { ActionData, ActionPermissions, ActionRecord, ActionShape, DatasourceAction, FieldTypeOf, IntegrationAction, ReadOf, ReadShape, SoftrDatasources, WorkflowAction, defineIntegration, defineMutation, defineQuery, defineWorkflow };
|
package/dist/action.js
CHANGED
|
@@ -1,22 +1,59 @@
|
|
|
1
1
|
//#region src/action.ts
|
|
2
2
|
function defineQuery(definition) {
|
|
3
3
|
return {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
id: definition.id,
|
|
5
|
+
resource: {
|
|
6
|
+
type: "DATASOURCE",
|
|
7
|
+
datasourceId: definition.datasourceId,
|
|
8
|
+
operation: "read",
|
|
9
|
+
fields: definition.fields
|
|
10
|
+
},
|
|
11
|
+
permissions: definition.permissions
|
|
7
12
|
};
|
|
8
13
|
}
|
|
9
14
|
function defineMutation(definition) {
|
|
15
|
+
const { id, datasourceId, operation, fields, permissions } = definition;
|
|
10
16
|
return {
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
id,
|
|
18
|
+
resource: {
|
|
19
|
+
type: "DATASOURCE",
|
|
20
|
+
datasourceId,
|
|
21
|
+
operation,
|
|
22
|
+
fields
|
|
23
|
+
},
|
|
24
|
+
permissions
|
|
13
25
|
};
|
|
14
26
|
}
|
|
15
|
-
function
|
|
27
|
+
function defineWorkflow(definition) {
|
|
28
|
+
const { id, workflowId, triggerId, body, permissions } = definition;
|
|
29
|
+
return {
|
|
30
|
+
id,
|
|
31
|
+
resource: {
|
|
32
|
+
type: "WORKFLOW",
|
|
33
|
+
workflowId,
|
|
34
|
+
triggerId,
|
|
35
|
+
body
|
|
36
|
+
},
|
|
37
|
+
permissions
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function defineIntegration(definition) {
|
|
41
|
+
const { id, datasourceId, permissions } = definition;
|
|
42
|
+
return {
|
|
43
|
+
id,
|
|
44
|
+
resource: {
|
|
45
|
+
type: "INTEGRATION",
|
|
46
|
+
datasourceId
|
|
47
|
+
},
|
|
48
|
+
permissions
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function isDatasourceAction(value) {
|
|
16
52
|
if (typeof value !== "object" || value === null) return false;
|
|
17
53
|
const candidate = value;
|
|
18
|
-
|
|
54
|
+
const resource = candidate.resource;
|
|
55
|
+
return typeof candidate.id === "string" && typeof resource === "object" && resource !== null && resource.type === "DATASOURCE";
|
|
19
56
|
}
|
|
20
57
|
|
|
21
58
|
//#endregion
|
|
22
|
-
export { defineMutation, defineQuery,
|
|
59
|
+
export { defineIntegration, defineMutation, defineQuery, defineWorkflow, isDatasourceAction };
|
package/dist/auth/platform.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fetch } from "../fetch.js";
|
|
1
2
|
import { panic } from "../assertions.js";
|
|
2
3
|
import { sessionConfig } from "./config.js";
|
|
3
4
|
import { userSchema } from "./user.js";
|
|
@@ -278,4 +279,4 @@ function withoutTrailingSlash(url) {
|
|
|
278
279
|
}
|
|
279
280
|
|
|
280
281
|
//#endregion
|
|
281
|
-
export { makePlatform };
|
|
282
|
+
export { makePlatform, platformApiUrl };
|
|
@@ -1,24 +1,15 @@
|
|
|
1
|
+
import { IntegrationAction } from "../action.js";
|
|
2
|
+
|
|
1
3
|
//#region src/core/integration.d.ts
|
|
2
|
-
interface
|
|
3
|
-
dataSourceId: string;
|
|
4
|
-
url: string;
|
|
4
|
+
interface IntegrationFetchInit {
|
|
5
5
|
method?: string;
|
|
6
6
|
headers?: Record<string, string>;
|
|
7
7
|
body?: string;
|
|
8
|
-
queryParams?: Record<string, string>;
|
|
9
8
|
signal?: AbortSignal;
|
|
10
9
|
}
|
|
11
|
-
declare function
|
|
12
|
-
dataSourceId,
|
|
13
|
-
url: rawUrl,
|
|
14
|
-
method,
|
|
15
|
-
headers,
|
|
16
|
-
body,
|
|
17
|
-
queryParams,
|
|
18
|
-
signal
|
|
19
|
-
}: IntegrationFetchOptions): Promise<Response>;
|
|
10
|
+
declare function integrationCreateFetcher(action: IntegrationAction): (url: string, init?: IntegrationFetchInit) => Promise<Response>;
|
|
20
11
|
declare const integration: {
|
|
21
|
-
|
|
12
|
+
createFetcher: typeof integrationCreateFetcher;
|
|
22
13
|
};
|
|
23
14
|
//#endregion
|
|
24
15
|
export { integration };
|
package/dist/core/integration.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
+
import { fetch } from "../fetch.js";
|
|
1
2
|
import { readSessionToken } from "../auth/session.js";
|
|
2
3
|
import { dsUrl } from "../datasource/record.js";
|
|
3
4
|
|
|
4
5
|
//#region src/core/integration.ts
|
|
5
|
-
async function integrationFetch(
|
|
6
|
+
async function integrationFetch(action, url, { method = "GET", headers = {}, body, signal } = {}) {
|
|
6
7
|
if (signal?.aborted) throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
7
|
-
let url = rawUrl;
|
|
8
|
-
if (queryParams && Object.keys(queryParams).length > 0) {
|
|
9
|
-
const params = new URLSearchParams(queryParams);
|
|
10
|
-
url += (rawUrl.includes("?") ? "&" : "?") + params.toString();
|
|
11
|
-
}
|
|
12
8
|
const upperMethod = method.toUpperCase();
|
|
13
9
|
const payload = {
|
|
14
10
|
method: upperMethod,
|
|
@@ -16,7 +12,7 @@ async function integrationFetch({ dataSourceId, url: rawUrl, method = "GET", hea
|
|
|
16
12
|
headers
|
|
17
13
|
};
|
|
18
14
|
if (upperMethod !== "GET" && upperMethod !== "HEAD" && body !== void 0) payload.body = body;
|
|
19
|
-
return fetch(`${dsUrl(
|
|
15
|
+
return fetch(`${dsUrl(action.resource.datasourceId)}/proxy`, {
|
|
20
16
|
method: "POST",
|
|
21
17
|
headers: {
|
|
22
18
|
"Content-Type": "application/json",
|
|
@@ -26,7 +22,10 @@ async function integrationFetch({ dataSourceId, url: rawUrl, method = "GET", hea
|
|
|
26
22
|
signal
|
|
27
23
|
});
|
|
28
24
|
}
|
|
29
|
-
|
|
25
|
+
function integrationCreateFetcher(action) {
|
|
26
|
+
return (url, init) => integrationFetch(action, url, init);
|
|
27
|
+
}
|
|
28
|
+
const integration = { createFetcher: integrationCreateFetcher };
|
|
30
29
|
|
|
31
30
|
//#endregion
|
|
32
31
|
export { integration };
|
package/dist/core/workflow.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { WorkflowAction } from "../action.js";
|
|
2
|
+
|
|
1
3
|
//#region src/core/workflow.d.ts
|
|
2
|
-
|
|
4
|
+
type FlatValue = string | number | boolean | null;
|
|
5
|
+
declare function triggerWorkflow(action: WorkflowAction, payload?: Record<string, FlatValue>): Promise<void>;
|
|
3
6
|
declare const workflow: {
|
|
4
7
|
trigger: typeof triggerWorkflow;
|
|
5
8
|
};
|
package/dist/core/workflow.js
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
|
+
import { fetch } from "../fetch.js";
|
|
2
|
+
import { sessionConfig } from "../auth/config.js";
|
|
3
|
+
import { platformApiUrl } from "../auth/platform.js";
|
|
1
4
|
import { readSessionToken } from "../auth/session.js";
|
|
2
5
|
|
|
3
6
|
//#region src/core/workflow.ts
|
|
4
|
-
const
|
|
5
|
-
async function triggerWorkflow(
|
|
6
|
-
const
|
|
7
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
async function triggerWorkflow(action, payload) {
|
|
9
|
+
const baseUrl = `${await platformApiUrl()}/v1/vibe/applications/${sessionConfig().applicationId}/actions/${action.id}/workflow`;
|
|
10
|
+
const triggerRes = await fetch(baseUrl, {
|
|
7
11
|
method: "POST",
|
|
8
12
|
headers: {
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
Authorization: `Bearer ${readSessionToken()}`,
|
|
14
|
+
"Content-Type": "application/json"
|
|
11
15
|
},
|
|
12
|
-
|
|
16
|
+
body: JSON.stringify(payload ?? {})
|
|
13
17
|
});
|
|
14
|
-
if (!
|
|
18
|
+
if (!triggerRes.ok) throw new Error(`Workflow trigger failed: ${triggerRes.status}`);
|
|
19
|
+
const execution = await triggerRes.json();
|
|
20
|
+
if (execution.status === "FINISHED") return;
|
|
21
|
+
if (execution.status === "PENDING_INTERACTION") throw new Error("Workflow requires interaction");
|
|
22
|
+
while (true) {
|
|
23
|
+
await sleep(2e3);
|
|
24
|
+
const detailsRes = await fetch(`${baseUrl}/${execution.id}`, { headers: { Authorization: `Bearer ${readSessionToken()}` } });
|
|
25
|
+
if (!detailsRes.ok) throw new Error(`Workflow status check failed: ${detailsRes.status}`);
|
|
26
|
+
const details = await detailsRes.json();
|
|
27
|
+
if (details.status === "FINISHED") return;
|
|
28
|
+
if (details.status === "PENDING_INTERACTION") throw new Error("Workflow requires interaction");
|
|
29
|
+
}
|
|
15
30
|
}
|
|
16
31
|
const workflow = { trigger: triggerWorkflow };
|
|
17
32
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DatasourceAction, FieldTypeOf, ReadShape, SoftrDatasources } from "../action.js";
|
|
2
2
|
import { ConstructableOrderBy } from "./filter.js";
|
|
3
3
|
import { DateFieldType, NumberFieldType, SortSet } from "./fields.js";
|
|
4
4
|
import { WhereCallback } from "./record.js";
|
|
@@ -84,14 +84,14 @@ type Unusable = {
|
|
|
84
84
|
* field's value; a date field also carries bucket leaves. A multi-value field has no handle
|
|
85
85
|
* body, because exploding it would count each record once per value and inflate every metric.
|
|
86
86
|
*/
|
|
87
|
-
type DimensionSet<TShape extends ReadShape> = keyof SoftrDatasources extends never ? Record<string, AnyDimension> : { [Alias in keyof TShape["fields"]]: DimensionFor<FieldTypeOf<TShape["
|
|
87
|
+
type DimensionSet<TShape extends ReadShape> = keyof SoftrDatasources extends never ? Record<string, AnyDimension> : { [Alias in keyof TShape["fields"]]: DimensionFor<FieldTypeOf<TShape["datasourceId"], TShape["fields"][Alias]>> };
|
|
88
88
|
type DimensionFor<TFieldType> = [TFieldType] extends [never] ? AnyDimension : TFieldType extends `${string}[]` | "ATTACHMENT" | "DATE_RANGE" ? Unusable : TFieldType extends "DATE" ? DateDimension : TFieldType extends DateFieldType ? DatetimeDimension : TFieldType extends NumberFieldType ? Dimension<number | null> : TFieldType extends "CHECKBOX" ? Dimension<boolean | null> : Dimension<string | null>;
|
|
89
89
|
/**
|
|
90
90
|
* The handles a `metrics` callback is handed: aggregation leaves per alias, gated by the
|
|
91
91
|
* field's declared type, plus `$count`. A field whose aggregate would come back as a
|
|
92
92
|
* coerced non-number has no leaves at all.
|
|
93
93
|
*/
|
|
94
|
-
type MeasureSet<TShape extends ReadShape> = (keyof SoftrDatasources extends never ? Record<string, AnyMeasures> : { [Alias in keyof TShape["fields"]]: MeasureFor<FieldTypeOf<TShape["
|
|
94
|
+
type MeasureSet<TShape extends ReadShape> = (keyof SoftrDatasources extends never ? Record<string, AnyMeasures> : { [Alias in keyof TShape["fields"]]: MeasureFor<FieldTypeOf<TShape["datasourceId"], TShape["fields"][Alias]>> }) & {
|
|
95
95
|
/** Counts the rows in each group. Takes no field: every row counts once. */
|
|
96
96
|
readonly $count: Measure<number>;
|
|
97
97
|
};
|
|
@@ -181,7 +181,7 @@ type MetricBuilder<TShape extends ReadShape, TValue$1> = {
|
|
|
181
181
|
* .orderBy((q) => q.month.asc)
|
|
182
182
|
* // data: Array<{ month: string | null; revenue: number | null; orders: number }>
|
|
183
183
|
*/
|
|
184
|
-
declare function chart<TShape extends ReadShape>(query:
|
|
184
|
+
declare function chart<TShape extends ReadShape>(query: DatasourceAction<TShape>): ChartSeed<TShape>;
|
|
185
185
|
/**
|
|
186
186
|
* Aggregates records into one value, with no grouping.
|
|
187
187
|
*
|
|
@@ -191,6 +191,6 @@ declare function chart<TShape extends ReadShape>(query: Action<TShape>): ChartSe
|
|
|
191
191
|
* .where((q) => q.status.is("paid"))
|
|
192
192
|
* // value: number | null
|
|
193
193
|
*/
|
|
194
|
-
declare function metric<TShape extends ReadShape, TMeasure extends Measure<unknown>>(query:
|
|
194
|
+
declare function metric<TShape extends ReadShape, TMeasure extends Measure<unknown>>(query: DatasourceAction<TShape>, pick: (q: MeasureSet<TShape>) => TMeasure): MetricBuilder<TShape, ColumnValue<TMeasure>>;
|
|
195
195
|
//#endregion
|
|
196
196
|
export { chart, metric };
|
package/dist/datasource/chart.js
CHANGED
|
@@ -112,7 +112,7 @@ type Combinators = {
|
|
|
112
112
|
* the action never declared has to resolve too, so that case is open by name and permissive
|
|
113
113
|
* by operator.
|
|
114
114
|
*/
|
|
115
|
-
type FieldSet<TShape extends ReadShape> = keyof SoftrDatasources extends never ? Record<string, AnyChain> & Combinators : { [Alias in keyof TShape['fields']]: ChainFor<FieldTypeOf<TShape['
|
|
115
|
+
type FieldSet<TShape extends ReadShape> = keyof SoftrDatasources extends never ? Record<string, AnyChain> & Combinators : { [Alias in keyof TShape['fields']]: ChainFor<FieldTypeOf<TShape['datasourceId'], TShape['fields'][Alias]>, NonNullable<ReadOf<TShape['datasourceId']>[TShape['fields'][Alias] & keyof ReadOf<TShape['datasourceId']>]>> } & Combinators;
|
|
116
116
|
/** What an `orderBy` callback is handed: two handles per alias and nothing else. */
|
|
117
117
|
type SortSet<TRecord> = { [K in keyof TRecord]: {
|
|
118
118
|
readonly asc: ConstructableOrderBy;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ActionData, ActionRecord, ActionShape, DatasourceAction, ReadShape } from "../action.js";
|
|
2
2
|
import { AnyConstructable, ConstructableOrderBy } from "./filter.js";
|
|
3
3
|
import { FieldSet, SortSet } from "./fields.js";
|
|
4
4
|
|
|
@@ -78,7 +78,7 @@ interface ListResult<TRecord extends SoftrRecord = SoftrRecord> {
|
|
|
78
78
|
* .orderBy((q) => q.publishedAt.desc)
|
|
79
79
|
* .limit(50)
|
|
80
80
|
*/
|
|
81
|
-
declare function listRecords<TShape extends ReadShape>(query:
|
|
81
|
+
declare function listRecords<TShape extends ReadShape>(query: DatasourceAction<TShape>): ListBuilder<TShape>;
|
|
82
82
|
/**
|
|
83
83
|
* Gets a single record by ID.
|
|
84
84
|
*
|
|
@@ -94,7 +94,7 @@ declare function listRecords<TShape extends ReadShape>(query: Action<TShape>): L
|
|
|
94
94
|
* .get(posts, { id: "record-id" })
|
|
95
95
|
* .where((q) => q.publishedAt.lte(today))
|
|
96
96
|
*/
|
|
97
|
-
declare function getRecord<TShape extends ReadShape>(query:
|
|
97
|
+
declare function getRecord<TShape extends ReadShape>(query: DatasourceAction<TShape>, options: GetOptions): GetBuilder<TShape>;
|
|
98
98
|
/**
|
|
99
99
|
* Creates a record and returns it.
|
|
100
100
|
*
|
|
@@ -105,7 +105,7 @@ declare function getRecord<TShape extends ReadShape>(query: Action<TShape>, opti
|
|
|
105
105
|
*/
|
|
106
106
|
declare function createRecord<TShape extends ActionShape & {
|
|
107
107
|
operation: "create";
|
|
108
|
-
}>(action:
|
|
108
|
+
}>(action: DatasourceAction<TShape>, options: {
|
|
109
109
|
data: ActionData<TShape>;
|
|
110
110
|
}): Promise<SoftrRecord<ActionRecord<TShape>>>;
|
|
111
111
|
/**
|
|
@@ -119,7 +119,7 @@ declare function createRecord<TShape extends ActionShape & {
|
|
|
119
119
|
*/
|
|
120
120
|
declare function updateRecord<TShape extends ActionShape & {
|
|
121
121
|
operation: "update";
|
|
122
|
-
}>(action:
|
|
122
|
+
}>(action: DatasourceAction<TShape>, options: {
|
|
123
123
|
id: string;
|
|
124
124
|
data: ActionData<TShape>;
|
|
125
125
|
}): Promise<SoftrRecord<ActionRecord<TShape>>>;
|
|
@@ -131,7 +131,7 @@ declare function updateRecord<TShape extends ActionShape & {
|
|
|
131
131
|
*/
|
|
132
132
|
declare function deleteRecord<TShape extends ActionShape & {
|
|
133
133
|
operation: "delete";
|
|
134
|
-
}>(action:
|
|
134
|
+
}>(action: DatasourceAction<TShape>, options: {
|
|
135
135
|
id: string;
|
|
136
136
|
}): Promise<void>;
|
|
137
137
|
//#endregion
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { fetch } from "../fetch.js";
|
|
1
2
|
import { assert, panic } from "../assertions.js";
|
|
2
|
-
import {
|
|
3
|
+
import { isDatasourceAction } from "../action.js";
|
|
3
4
|
import { readUserTimezone } from "../cookies.js";
|
|
4
5
|
import { readSessionToken } from "../auth/session.js";
|
|
5
6
|
import { buildFilterCriteria, buildOrderBy, dealiasFilterCriteria, dealiasOrderBy } from "./filter.js";
|
|
@@ -233,12 +234,12 @@ async function deleteRecord(action, options) {
|
|
|
233
234
|
}
|
|
234
235
|
function resolveTarget(subject, operation) {
|
|
235
236
|
const declaredBy = operation === "read" ? "defineQuery" : "defineMutation";
|
|
236
|
-
assert(
|
|
237
|
+
assert(isDatasourceAction(subject), () => `Expected an action from ${declaredBy} but got ${JSON.stringify(subject)}`);
|
|
237
238
|
const action = subject;
|
|
238
|
-
assert(action.operation === operation, () => `action "${action.id}" is declared as ${action.operation}, not ${operation}`);
|
|
239
|
+
assert(action.resource.operation === operation, () => `action "${action.id}" is declared as ${action.resource.operation}, not ${operation}`);
|
|
239
240
|
return {
|
|
240
|
-
datasourceId: action.
|
|
241
|
-
select: action.fields ?? {}
|
|
241
|
+
datasourceId: action.resource.datasourceId,
|
|
242
|
+
select: action.resource.fields ?? {}
|
|
242
243
|
};
|
|
243
244
|
}
|
|
244
245
|
function aliasFields(fields, aliases) {
|
package/dist/fetch.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { logger } from "./logger.js";
|
|
2
|
+
|
|
3
|
+
//#region src/fetch.ts
|
|
4
|
+
const fetch = async (input, init) => {
|
|
5
|
+
if (process.env.NODE_ENV === "production") return globalThis.fetch(input, init);
|
|
6
|
+
const url = input instanceof Request ? input.url : String(input);
|
|
7
|
+
const method = init?.method ?? (input instanceof Request ? input.method : "GET");
|
|
8
|
+
const started = Date.now();
|
|
9
|
+
try {
|
|
10
|
+
const response = await globalThis.fetch(input, init);
|
|
11
|
+
logger.info(`${method} ${response.status} ${url} ${Date.now() - started}ms`);
|
|
12
|
+
return response;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
const label = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
15
|
+
logger.info(`${method} ERR ${url} ${label} in ${Date.now() - started}ms`);
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
export { fetch };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
|
|
2
|
-
import { SoftrDatasources, defineMutation, defineQuery } from "./action.js";
|
|
2
|
+
import { ActionPermissions, SoftrDatasources, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
3
3
|
import { SoftrContext } from "./auth/context.js";
|
|
4
4
|
import { defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
|
|
5
5
|
import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions } from "./auth/isomorphic.js";
|
|
6
6
|
import { useUserTimezone } from "./timezone.js";
|
|
7
7
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
8
|
-
export { DefaultErrorOverlay, type SoftrContext, type SoftrDatasources, type SoftrUserGroupId, type SoftrUserGroups, type User, defineMutation, defineQuery, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
|
|
8
|
+
export { type ActionPermissions, DefaultErrorOverlay, type SoftrContext, type SoftrDatasources, type SoftrUserGroupId, type SoftrUserGroups, type User, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { defineMutation, defineQuery } from "./action.js";
|
|
1
|
+
import { defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
2
2
|
import { defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
|
|
3
3
|
import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions } from "./auth/isomorphic.js";
|
|
4
4
|
import { useUserTimezone } from "./timezone.js";
|
|
5
5
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
6
6
|
|
|
7
|
-
export { DefaultErrorOverlay, defineMutation, defineQuery, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
|
|
7
|
+
export { DefaultErrorOverlay, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/logger.ts
|
|
2
|
+
const PREFIX = "[@softr/sdk]";
|
|
3
|
+
function emit(method, args) {
|
|
4
|
+
if (process.env.NODE_ENV === "production") return;
|
|
5
|
+
console[method](PREFIX, ...args);
|
|
6
|
+
}
|
|
7
|
+
const logger = {
|
|
8
|
+
info: (...args) => emit("info", args),
|
|
9
|
+
log: (...args) => emit("log", args),
|
|
10
|
+
warn: (...args) => emit("warn", args),
|
|
11
|
+
error: (...args) => emit("error", args)
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { logger };
|
package/dist/package.js
CHANGED
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
metadata:
|
|
12
12
|
type: core
|
|
13
13
|
library: softr-sdk
|
|
14
|
-
library_version: 0.0.
|
|
14
|
+
library_version: 0.0.32
|
|
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` lives on `@softr/sdk/server` and only runs inside a `createServerFn` handler. Use `defineWorkflow` from `@softr/sdk` to declare the action, then pass it to `workflow.trigger`. Load the `softr-sdk/workflows` sub-skill for the full pattern.
|
|
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 `record-queries` plus `routes-and-visibility`.
|
|
27
27
|
|
|
@@ -43,6 +43,7 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
|
|
|
43
43
|
| Fetch records from a datasource | [record-queries/SKILL.md](./record-queries/SKILL.md) |
|
|
44
44
|
| Chart, aggregate or count records | [charts-and-metrics/SKILL.md](./charts-and-metrics/SKILL.md) |
|
|
45
45
|
| Create, update or delete a record | [record-mutations/SKILL.md](./record-mutations/SKILL.md) |
|
|
46
|
+
| Trigger a Softr workflow | [workflows/SKILL.md](./workflows/SKILL.md) |
|
|
46
47
|
| Sign a visitor in, out, or up | [auth/SKILL.md](./auth/SKILL.md) |
|
|
47
48
|
| Call a connected third-party API (Slack, Stripe, Gmail, …) | [integrations/SKILL.md](./integrations/SKILL.md) |
|
|
48
49
|
|
|
@@ -64,6 +65,9 @@ Drawing a chart, a stat tile, or any aggregated number?
|
|
|
64
65
|
Submitting a form, editing a record, or deleting one?
|
|
65
66
|
load softr-sdk/record-mutations
|
|
66
67
|
|
|
68
|
+
Triggering a Softr workflow from a button or form?
|
|
69
|
+
load softr-sdk/workflows
|
|
70
|
+
|
|
67
71
|
Wiring a login, signup, or sign-out?
|
|
68
72
|
load softr-sdk/auth
|
|
69
73
|
|
|
@@ -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.32
|
|
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.
|
|
@@ -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.32
|
|
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. 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.
|
|
@@ -30,7 +30,7 @@ import { ds } from '@softr/sdk/server'
|
|
|
30
30
|
|
|
31
31
|
const sales = defineQuery({
|
|
32
32
|
id: 'sales',
|
|
33
|
-
|
|
33
|
+
datasourceId: '00000000-0000-0000-0000-000000000000',
|
|
34
34
|
fields: {
|
|
35
35
|
amount: 'Vk9cXm2pSz6Ld4', // CURRENCY
|
|
36
36
|
region: 'fZk3Qm9pLw2Xa1', // SELECT
|
|
@@ -38,7 +38,7 @@ const sales = defineQuery({
|
|
|
38
38
|
closedAt: 'Xp2dLh6vKz9Fa3', // DATETIME
|
|
39
39
|
rep: 'Gq5wNt8rJy3Hb1', // LINKED_RECORD
|
|
40
40
|
},
|
|
41
|
-
|
|
41
|
+
permissions: { userGroup: 'LOGGED_IN_USERS' },
|
|
42
42
|
})
|
|
43
43
|
```
|
|
44
44
|
|
|
@@ -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.32
|
|
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,27 +2,27 @@
|
|
|
2
2
|
name: integrations
|
|
3
3
|
description: >-
|
|
4
4
|
Call a third-party service connected to the app as an integration:
|
|
5
|
-
|
|
6
|
-
@softr/sdk/server
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
query params as an object. Check res.ok before res.json().
|
|
5
|
+
defineIntegration from @softr/sdk declares the action with its datasourceId
|
|
6
|
+
and permissions; integration.createFetcher from @softr/sdk/server binds it and
|
|
7
|
+
returns a fetcher function you call with url, optional method, headers, and
|
|
8
|
+
body. Check res.ok before res.json().
|
|
10
9
|
metadata:
|
|
11
10
|
type: sub-skill
|
|
12
11
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.32
|
|
14
13
|
---
|
|
15
14
|
|
|
16
|
-
**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.
|
|
15
|
+
**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. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
17
16
|
|
|
18
17
|
# Integrations
|
|
19
18
|
|
|
20
|
-
`integration.
|
|
19
|
+
`integration.createFetcher` calls a third-party API through Softr's authenticated proxy. It handles authorization automatically — you never set API keys or auth headers yourself.
|
|
21
20
|
|
|
22
21
|
## Setup
|
|
23
22
|
|
|
24
23
|
```ts
|
|
25
24
|
import { createServerFn } from '@tanstack/react-start'
|
|
25
|
+
import { defineIntegration } from '@softr/sdk'
|
|
26
26
|
import { integration } from '@softr/sdk/server'
|
|
27
27
|
```
|
|
28
28
|
|
|
@@ -34,38 +34,52 @@ When the user attaches integrations to the chat, they appear in the `## Attached
|
|
|
34
34
|
- "Gmail" — database `<databaseId>`, integration `<integrationId>`, type `GMAIL`
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Use `databaseId` as `
|
|
37
|
+
Use `databaseId` as `datasourceId` in `defineIntegration` — that is the `database` coordinate, not the `integration` one. The `integration` value is the OAuth connection identifier; do not pass it anywhere. Use `integrationType` (e.g. `GMAIL`) to look up the base URL from the reference table below — then construct the full URL path, including any query parameters, directly in the `url` string.
|
|
38
|
+
|
|
39
|
+
## Declare the integration action
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const googleCalendar = defineIntegration({
|
|
43
|
+
id: 'googleCalendar',
|
|
44
|
+
datasourceId: '25d60213-ef4f-4fa0-b094-a46931b2d120',
|
|
45
|
+
permissions: { userGroup: 'LOGGED_IN_USERS' },
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Write the object inline in `defineIntegration`. Passing it through a variable widens the types.
|
|
38
50
|
|
|
39
51
|
## Core pattern
|
|
40
52
|
|
|
53
|
+
`integration.createFetcher(action)` binds the action and returns a fetcher function. Call that function with a `url` and optional `method`, `headers`, `body`, and `signal`.
|
|
54
|
+
|
|
41
55
|
```ts
|
|
42
56
|
const getCalendarEvents = createServerFn().handler(async () => {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
singleEvents: 'true',
|
|
49
|
-
orderBy: 'startTime',
|
|
50
|
-
maxResults: '10',
|
|
51
|
-
},
|
|
52
|
-
})
|
|
57
|
+
const calendar = integration.createFetcher(googleCalendar)
|
|
58
|
+
const now = new Date().toISOString()
|
|
59
|
+
const res = await calendar(
|
|
60
|
+
`https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=${now}&singleEvents=true&orderBy=startTime&maxResults=10`,
|
|
61
|
+
)
|
|
53
62
|
if (!res.ok) throw new Error(`Integration request failed: ${res.status} ${await res.text()}`)
|
|
54
63
|
return res.json()
|
|
55
64
|
})
|
|
56
65
|
```
|
|
57
66
|
|
|
58
|
-
|
|
67
|
+
Include query parameters directly in the `url` string — there is no separate `queryParams` option.
|
|
59
68
|
|
|
60
69
|
## POST request
|
|
61
70
|
|
|
62
71
|
```ts
|
|
72
|
+
const slackDs = defineIntegration({
|
|
73
|
+
id: 'slack',
|
|
74
|
+
datasourceId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
|
75
|
+
permissions: { userGroup: 'LOGGED_IN_USERS' },
|
|
76
|
+
})
|
|
77
|
+
|
|
63
78
|
const sendSlackMessage = createServerFn({ method: 'POST' })
|
|
64
79
|
.validator((data: { channel: string; text: string }) => data)
|
|
65
80
|
.handler(async ({ data }) => {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
url: 'https://slack.com/api/chat.postMessage',
|
|
81
|
+
const slack = integration.createFetcher(slackDs)
|
|
82
|
+
const res = await slack('https://slack.com/api/chat.postMessage', {
|
|
69
83
|
method: 'POST',
|
|
70
84
|
headers: { 'Content-Type': 'application/json' },
|
|
71
85
|
body: JSON.stringify({ channel: data.channel, text: data.text }),
|
|
@@ -141,4 +155,4 @@ If the API the user needs is not in the table above, call it directly from a ser
|
|
|
141
155
|
|
|
142
156
|
**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.
|
|
143
157
|
|
|
144
|
-
**Not checking `res.ok`.**
|
|
158
|
+
**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()`.
|
|
@@ -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.32
|
|
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. 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.
|
|
@@ -38,18 +38,18 @@ A mutation declares exactly one operation, and each of the three needs its own d
|
|
|
38
38
|
```ts
|
|
39
39
|
const createProject = defineMutation({
|
|
40
40
|
id: 'createProject',
|
|
41
|
-
|
|
41
|
+
datasourceId: '00000000-0000-0000-0000-000000000000',
|
|
42
42
|
operation: 'create',
|
|
43
43
|
fields: {
|
|
44
44
|
name: 'fZk3Qm9pLw2Xa1', // SINGLE_LINE_TEXT
|
|
45
45
|
status: 'Ru7bVx2cNq0Ed9', // SELECT, choice ids 'draft' | 'active' | 'archived'
|
|
46
46
|
dueDate: 'Xp2dLh6vKz9Fa3', // DATETIME
|
|
47
47
|
},
|
|
48
|
-
|
|
48
|
+
permissions: { userGroup: 'LOGGED_IN_USERS' },
|
|
49
49
|
})
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
The declaration follows the same rules as a read, spelled out in [record-queries](../record-queries/SKILL.md): `id` reads like a name and is unique across the app, queries counted alongside mutations; `
|
|
52
|
+
The declaration follows the same rules as a read, spelled out in [record-queries](../record-queries/SKILL.md): `id` reads like a name and is unique across the app, queries counted alongside mutations; `datasourceId` 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; `permissions` declares who may run the mutation, from the same three choices, and the SDK enforces it; and the object is written inline, because passing it through a variable or a spread widens the field ids and the types stop checking.
|
|
53
53
|
|
|
54
54
|
The mutation's `fields` bound the write: `data` can only name them, and the returned record only carries them. A delete mutation declares no `fields`, and giving it some is a type error.
|
|
55
55
|
|
|
@@ -76,10 +76,10 @@ Returns the created record as `{ id, fields }`, with `fields` keyed by the mutat
|
|
|
76
76
|
```ts
|
|
77
77
|
const updateProject = defineMutation({
|
|
78
78
|
id: 'updateProject',
|
|
79
|
-
|
|
79
|
+
datasourceId: '00000000-0000-0000-0000-000000000000',
|
|
80
80
|
operation: 'update',
|
|
81
81
|
fields: { status: 'Ru7bVx2cNq0Ed9', dueDate: 'Xp2dLh6vKz9Fa3' },
|
|
82
|
-
|
|
82
|
+
permissions: { userGroup: 'LOGGED_IN_USERS' },
|
|
83
83
|
})
|
|
84
84
|
|
|
85
85
|
const archiveProject = createServerFn({ method: 'POST' })
|
|
@@ -98,9 +98,9 @@ The record id is its own option; keep it out of `data`. An empty `id` throws.
|
|
|
98
98
|
```ts
|
|
99
99
|
const deleteProject = defineMutation({
|
|
100
100
|
id: 'deleteProject',
|
|
101
|
-
|
|
101
|
+
datasourceId: '00000000-0000-0000-0000-000000000000',
|
|
102
102
|
operation: 'delete',
|
|
103
|
-
|
|
103
|
+
permissions: { userGroup: 'LOGGED_IN_USERS' },
|
|
104
104
|
})
|
|
105
105
|
|
|
106
106
|
const removeProject = createServerFn({ method: 'POST' })
|
|
@@ -119,9 +119,9 @@ An edit form needs both halves: a `defineQuery` to load the record and a `define
|
|
|
119
119
|
```ts
|
|
120
120
|
const projectDetail = defineQuery({
|
|
121
121
|
id: 'projectDetail',
|
|
122
|
-
|
|
122
|
+
datasourceId: '00000000-0000-0000-0000-000000000000',
|
|
123
123
|
fields: { status: 'Ru7bVx2cNq0Ed9', dueDate: 'Xp2dLh6vKz9Fa3' },
|
|
124
|
-
|
|
124
|
+
permissions: { userGroup: 'LOGGED_IN_USERS' },
|
|
125
125
|
})
|
|
126
126
|
|
|
127
127
|
const project = await ds.get(projectDetail, { id })
|
|
@@ -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.32
|
|
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. 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.
|
|
@@ -38,21 +38,21 @@ Every read takes a query: which datasource, which fields, who it is for.
|
|
|
38
38
|
```ts
|
|
39
39
|
const publishedPosts = defineQuery({
|
|
40
40
|
id: 'publishedPosts',
|
|
41
|
-
|
|
41
|
+
datasourceId: '00000000-0000-0000-0000-000000000000',
|
|
42
42
|
fields: {
|
|
43
43
|
title: 'fZk3Qm9pLw2Xa1', // SINGLE_LINE_TEXT
|
|
44
44
|
published: 'Ru7bVx2cNq0Ed9', // CHECKBOX
|
|
45
45
|
publishedAt: 'Xp2dLh6vKz9Fa3',// DATETIME
|
|
46
46
|
views: 'mB4kQ8yTn1sWc7', // NUMBER
|
|
47
47
|
},
|
|
48
|
-
|
|
48
|
+
permissions: { userGroup: 'ALL_USERS' },
|
|
49
49
|
})
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
- `id` is the name the person building the app sees, so `publishedPosts`, not `q1`. It must be unique across the app: no two declarations may share one, queries and mutations counted together.
|
|
53
|
-
- `
|
|
53
|
+
- `datasourceId` is the 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 is never derived from the table's name.
|
|
54
54
|
- The keys of `fields` are names you choose. The values are field ids, opaque strings the table's schema gives you: `get_table` returns them for a Softr table, `list_data_source_table_fields` for a table on a connected database. Everything downstream speaks your names: the returned records, `where`, `orderBy`.
|
|
55
|
-
- `
|
|
55
|
+
- `permissions` declares who may run the query, and the SDK enforces it:
|
|
56
56
|
- `{ userGroup: 'ALL_USERS' }`: everyone, signed out included.
|
|
57
57
|
- `{ userGroup: 'NON_LOGGED_IN_USERS' }`: signed-out visitors only.
|
|
58
58
|
- `{ userGroup: 'LOGGED_IN_USERS' }`: any signed-in visitor, narrowed to particular groups with `userGroupIds: ['00000000-0000-0000-0000-00000000000a']`.
|
|
@@ -132,9 +132,9 @@ if (first.nextOffset !== null) {
|
|
|
132
132
|
```ts
|
|
133
133
|
const myInvoices = defineQuery({
|
|
134
134
|
id: 'myInvoices',
|
|
135
|
-
|
|
135
|
+
datasourceId: '00000000-0000-0000-0000-000000000001',
|
|
136
136
|
fields: { owner: 'Gq5wNt8rJy3Hb1', amount: 'Vk9cXm2pSz6Ld4' },
|
|
137
|
-
|
|
137
|
+
permissions: { userGroup: 'LOGGED_IN_USERS' },
|
|
138
138
|
})
|
|
139
139
|
|
|
140
140
|
const listMyInvoices = createServerFn().handler(async ({ context }) => {
|
|
@@ -144,7 +144,7 @@ const listMyInvoices = createServerFn().handler(async ({ context }) => {
|
|
|
144
144
|
})
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
The SDK enforces `
|
|
147
|
+
The SDK enforces `permissions`, so a visitor outside the declared audience never reaches the records. Which of those records belong to one visitor is a separate question, and the `where` above answers it: a group says nothing about who owns a row, so a per-visitor read filters on `context.user`. See [current-user](../current-user/SKILL.md) for how the visitor gets onto `context`.
|
|
148
148
|
|
|
149
149
|
A read the datasource turns down throws rather than coming back empty, and it throws a plain `Error` carrying the status. There is no typed predicate to narrow it with. Handle it where you would handle any other failed fetch: an `errorComponent` on the route, or a check in the handler.
|
|
150
150
|
|
|
@@ -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.32
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
**Boundary.** `defineVisibility`, the error predicates 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.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workflows
|
|
3
|
+
description: >-
|
|
4
|
+
Trigger a Softr workflow from a server function: defineWorkflow from
|
|
5
|
+
@softr/sdk declares the action with its workflowId, triggerId, permissions,
|
|
6
|
+
and optional hardcoded body; workflow.trigger from @softr/sdk/server fires it
|
|
7
|
+
with an optional flat runtime payload and resolves to void when the workflow
|
|
8
|
+
finishes.
|
|
9
|
+
metadata:
|
|
10
|
+
type: sub-skill
|
|
11
|
+
library: softr-sdk
|
|
12
|
+
library_version: 0.0.32
|
|
13
|
+
---
|
|
14
|
+
|
|
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. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
16
|
+
|
|
17
|
+
# Triggering workflows
|
|
18
|
+
|
|
19
|
+
`workflow.trigger` fires a Softr workflow and waits for it to finish. It resolves to `void`.
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
25
|
+
import { defineWorkflow } from '@softr/sdk'
|
|
26
|
+
import { workflow } from '@softr/sdk/server'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Declare the workflow action
|
|
30
|
+
|
|
31
|
+
Every trigger needs a workflow action: which workflow, which trigger step, who may run it, and any data that is always sent.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const sendWelcomeEmail = defineWorkflow({
|
|
35
|
+
id: 'sendWelcomeEmail',
|
|
36
|
+
workflowId: 'c9d8e7f6-a5b4-4c3d-8e2f-1a0b9c8d7e6f',
|
|
37
|
+
triggerId: '926da496-749f-4006-999c-197d8a556122',
|
|
38
|
+
permissions: { userGroup: 'LOGGED_IN_USERS' },
|
|
39
|
+
body: {
|
|
40
|
+
email: '{LOGGED_IN_USER:EMAIL}', // resolved server-side from the signed-in user
|
|
41
|
+
name: '{LOGGED_IN_USER:NAME}',
|
|
42
|
+
plan: 'enterprise', // any hardcoded value — always sent, never overridden at runtime
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- `id` is a name you choose, unique across the app.
|
|
48
|
+
- `workflowId` and `triggerId` are opaque UUIDs from the workflow definition in the Softr UI. Pass them as inline string literals — never derive them at runtime.
|
|
49
|
+
- `permissions` declares who may run the trigger. Uses the same format as queries and mutations.
|
|
50
|
+
- `body` is hardcoded data the server resolves before calling the workflow. It is **not** passed by the caller at runtime — the workflow always receives it. Template values draw from the signed-in user and the current time (see table below).
|
|
51
|
+
|
|
52
|
+
Write the object inline in `defineWorkflow`. Passing it through a variable widens the types.
|
|
53
|
+
|
|
54
|
+
## Template values in `body`
|
|
55
|
+
|
|
56
|
+
These placeholders are resolved server-side when the workflow runs:
|
|
57
|
+
|
|
58
|
+
| Value | Resolves to |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `{LOGGED_IN_USER:EMAIL}` | Signed-in user's email |
|
|
61
|
+
| `{LOGGED_IN_USER:NAME}` | Signed-in user's name |
|
|
62
|
+
| `{LOGGED_IN_USER:RECORD_ID}` | Signed-in user's record id |
|
|
63
|
+
| `{LOGGED_IN_USER:<field>}` | Any other field from the user record |
|
|
64
|
+
| `{CURRENT_DATE_TIME}` | Current date and time |
|
|
65
|
+
| `{CURRENT_DATE}` | Current date |
|
|
66
|
+
|
|
67
|
+
## Core pattern
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const runWorkflow = createServerFn({ method: 'POST' })
|
|
71
|
+
.validator(z.object({ message: z.string() }))
|
|
72
|
+
.handler(async ({ data }) => {
|
|
73
|
+
await workflow.trigger(sendWelcomeEmail, { message: data.message })
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`workflow.trigger` takes the action object and an optional flat runtime payload. The payload only accepts primitive values — `string`, `number`, `boolean`, `null`. Use the action's `body` for anything that is always the same or comes from the user session.
|
|
78
|
+
|
|
79
|
+
`workflow.trigger` resolves only when the workflow has finished.
|
|
80
|
+
|
|
81
|
+
## Triggering without a runtime payload
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const run = createServerFn({ method: 'POST' }).handler(async () => {
|
|
85
|
+
await workflow.trigger(sendWelcomeEmail)
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Omit the second argument when the action's `body` already carries everything the workflow needs.
|
|
90
|
+
|
|
91
|
+
## Failure modes
|
|
92
|
+
|
|
93
|
+
**Calling `workflow.trigger` from a component.** `workflow` lives on `@softr/sdk/server`. Wrap it in a `createServerFn`.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// Wrong: calling from a click handler
|
|
97
|
+
<button onClick={() => workflow.trigger(sendWelcomeEmail)}>Run</button>
|
|
98
|
+
|
|
99
|
+
// Correct: server function, called from the button
|
|
100
|
+
const run = createServerFn({ method: 'POST' }).handler(() =>
|
|
101
|
+
workflow.trigger(sendWelcomeEmail),
|
|
102
|
+
)
|
|
103
|
+
// …
|
|
104
|
+
const submit = useServerFn(run)
|
|
105
|
+
<button onClick={() => submit()}>Run</button>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Computing `workflowId` or `triggerId` at runtime.** Both are static UUIDs from the workflow definition. Derive them from user input and the call fails at request time.
|
|
109
|
+
|
|
110
|
+
**Passing nested objects in the runtime payload.** The second argument to `workflow.trigger` only accepts flat values — `string`, `number`, `boolean`, or `null`. Put anything that needs server-side resolution in the action's `body` instead.
|
|
111
|
+
|
|
112
|
+
**Not awaiting the result.** `workflow.trigger` is async. Fire-and-forget loses the result and swallows any error.
|
|
113
|
+
|
|
114
|
+
## References
|
|
115
|
+
|
|
116
|
+
- [current-user/SKILL.md](../current-user/SKILL.md): reading the visitor off `context` to include them in the payload
|