@softr/sdk 0.0.98 → 0.0.99-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/action.d.ts +44 -1
- package/dist/action.js +37 -1
- package/dist/core/email.d.ts +48 -0
- package/dist/core/email.js +100 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/package.js +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.js +2 -1
- package/package.json +1 -1
- package/skills/softr-sdk/SKILL.md +10 -4
- package/skills/softr-sdk/ai/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/email/SKILL.md +147 -0
- 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 +3 -1
- 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 +10 -8
package/dist/action.d.ts
CHANGED
|
@@ -234,6 +234,49 @@ type IntegrationDefinition<TId extends string> = {
|
|
|
234
234
|
* const res = await listEvents({ query: { timeMin: now, singleEvents: "true" } })
|
|
235
235
|
*/
|
|
236
236
|
declare function defineIntegration<const TId extends string>(definition: IntegrationDefinition<TId>): IntegrationAction<TId>;
|
|
237
|
+
type EmailFields = {
|
|
238
|
+
senderId?: string;
|
|
239
|
+
to?: ReadonlyArray<string>;
|
|
240
|
+
cc?: ReadonlyArray<string>;
|
|
241
|
+
bcc?: ReadonlyArray<string>;
|
|
242
|
+
replyTo?: string;
|
|
243
|
+
};
|
|
244
|
+
type EmailResource = EmailFields & {
|
|
245
|
+
type: "SOFTR_EMAIL";
|
|
246
|
+
};
|
|
247
|
+
type EmailAction<TId extends string = string> = Action<EmailResource> & {
|
|
248
|
+
id: TId;
|
|
249
|
+
};
|
|
250
|
+
type EmailDefinition<TId extends string> = EmailFields & {
|
|
251
|
+
id: TId;
|
|
252
|
+
permissions: ActionPermissions;
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* Declares an email action: who may send it, and the fields every email it sends keeps whatever the
|
|
256
|
+
* call says. Softr sends it from the app's email sender and bills the workspace's workflow credits.
|
|
257
|
+
*
|
|
258
|
+
* - `to`, `cc`, `bcc`: addresses. An element may be a whole `{LOGGED_IN_USER:EMAIL}` or
|
|
259
|
+
* `{LOGGED_IN_USER:<field>}` placeholder, filled in for the signed-in sender.
|
|
260
|
+
* - `replyTo`: an address or one such placeholder.
|
|
261
|
+
* - `senderId`: one of the workspace's email senders, for an email that must come from that address.
|
|
262
|
+
* Left out, the app's sender is used. The call can never choose the sender.
|
|
263
|
+
*
|
|
264
|
+
* A field declared here wins over the call's, and declaring `to` also ignores the call's `cc` and
|
|
265
|
+
* `bcc`. Fix the recipients whenever the use case allows,
|
|
266
|
+
* because anyone in the audience can otherwise send any email to anyone.
|
|
267
|
+
*
|
|
268
|
+
* Pass the returned object to `email.send()` at runtime.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* const notifyOwner = defineEmailAction({
|
|
272
|
+
* id: "notifyOwner",
|
|
273
|
+
* to: ["owner@acme.com"],
|
|
274
|
+
* permissions: { userGroupTypes: ["NON_LOGGED_IN_USERS", "LOGGED_IN_USERS"] },
|
|
275
|
+
* })
|
|
276
|
+
*
|
|
277
|
+
* await email.send(notifyOwner, { subject: "New submission", body: html, replyTo: data.email })
|
|
278
|
+
*/
|
|
279
|
+
declare function defineEmailAction<const TId extends string>(definition: EmailDefinition<TId>): EmailAction<TId>;
|
|
237
280
|
type AiResource = {
|
|
238
281
|
type: "SOFTR_AI";
|
|
239
282
|
model: string;
|
|
@@ -268,4 +311,4 @@ type AiDefinition<TId extends string> = {
|
|
|
268
311
|
*/
|
|
269
312
|
declare function defineAiAction<const TId extends string>(definition: AiDefinition<TId>): AiAction<TId>;
|
|
270
313
|
//#endregion
|
|
271
|
-
export { ActionData, ActionPermissions, ActionRecord, ActionShape, AiAction, DatasourceAction, FieldTypeOf, IntegrationAction, ReadOf, ReadShape, SoftrDatasources, WorkflowAction, defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow };
|
|
314
|
+
export { ActionData, ActionPermissions, ActionRecord, ActionShape, AiAction, DatasourceAction, EmailAction, FieldTypeOf, IntegrationAction, ReadOf, ReadShape, SoftrDatasources, WorkflowAction, defineAiAction, defineEmailAction, defineIntegration, defineMutation, defineQuery, defineWorkflow };
|
package/dist/action.js
CHANGED
|
@@ -50,6 +50,42 @@ function defineIntegration(definition) {
|
|
|
50
50
|
permissions
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Declares an email action: who may send it, and the fields every email it sends keeps whatever the
|
|
55
|
+
* call says. Softr sends it from the app's email sender and bills the workspace's workflow credits.
|
|
56
|
+
*
|
|
57
|
+
* - `to`, `cc`, `bcc`: addresses. An element may be a whole `{LOGGED_IN_USER:EMAIL}` or
|
|
58
|
+
* `{LOGGED_IN_USER:<field>}` placeholder, filled in for the signed-in sender.
|
|
59
|
+
* - `replyTo`: an address or one such placeholder.
|
|
60
|
+
* - `senderId`: one of the workspace's email senders, for an email that must come from that address.
|
|
61
|
+
* Left out, the app's sender is used. The call can never choose the sender.
|
|
62
|
+
*
|
|
63
|
+
* A field declared here wins over the call's, and declaring `to` also ignores the call's `cc` and
|
|
64
|
+
* `bcc`. Fix the recipients whenever the use case allows,
|
|
65
|
+
* because anyone in the audience can otherwise send any email to anyone.
|
|
66
|
+
*
|
|
67
|
+
* Pass the returned object to `email.send()` at runtime.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* const notifyOwner = defineEmailAction({
|
|
71
|
+
* id: "notifyOwner",
|
|
72
|
+
* to: ["owner@acme.com"],
|
|
73
|
+
* permissions: { userGroupTypes: ["NON_LOGGED_IN_USERS", "LOGGED_IN_USERS"] },
|
|
74
|
+
* })
|
|
75
|
+
*
|
|
76
|
+
* await email.send(notifyOwner, { subject: "New submission", body: html, replyTo: data.email })
|
|
77
|
+
*/
|
|
78
|
+
function defineEmailAction(definition) {
|
|
79
|
+
const { id, permissions,...fields } = definition;
|
|
80
|
+
return {
|
|
81
|
+
id,
|
|
82
|
+
resource: {
|
|
83
|
+
type: "SOFTR_EMAIL",
|
|
84
|
+
...fields
|
|
85
|
+
},
|
|
86
|
+
permissions
|
|
87
|
+
};
|
|
88
|
+
}
|
|
53
89
|
function isDatasourceAction(value) {
|
|
54
90
|
if (typeof value !== "object" || value === null) return false;
|
|
55
91
|
const candidate = value;
|
|
@@ -69,4 +105,4 @@ function defineAiAction(definition) {
|
|
|
69
105
|
}
|
|
70
106
|
|
|
71
107
|
//#endregion
|
|
72
|
-
export { defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow, isDatasourceAction };
|
|
108
|
+
export { defineAiAction, defineEmailAction, defineIntegration, defineMutation, defineQuery, defineWorkflow, isDatasourceAction };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { EmailAction } from "../action.js";
|
|
2
|
+
|
|
3
|
+
//#region src/core/email.d.ts
|
|
4
|
+
declare const EMAIL_ERROR_CODES: readonly ["NOT_FOUND", "INVALID_INPUT", "PLAN_RESTRICTED", "CREDITS_EXHAUSTED", "RATE_LIMITED", "DAILY_LIMIT_REACHED", "SENDER_UNAVAILABLE", "PROVIDER_REJECTED", "UNAVAILABLE", "TIMEOUT"];
|
|
5
|
+
type EmailErrorCode = (typeof EMAIL_ERROR_CODES)[number];
|
|
6
|
+
interface EmailSendInit {
|
|
7
|
+
/** Ignored when the action declares its own. */
|
|
8
|
+
to?: ReadonlyArray<string>;
|
|
9
|
+
/** Ignored when the action declares its own `cc` or `to`. */
|
|
10
|
+
cc?: ReadonlyArray<string>;
|
|
11
|
+
/** Ignored when the action declares its own `bcc` or `to`. */
|
|
12
|
+
bcc?: ReadonlyArray<string>;
|
|
13
|
+
/** Ignored when the action declares its own. */
|
|
14
|
+
replyTo?: string;
|
|
15
|
+
subject: string;
|
|
16
|
+
/** Plain text or HTML. */
|
|
17
|
+
body: string;
|
|
18
|
+
}
|
|
19
|
+
interface EmailSent {
|
|
20
|
+
/** Null when the mail provider did not report one; the email was still sent. */
|
|
21
|
+
messageId: string | null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Thrown when Softr did not send the email. `code` says why:
|
|
25
|
+
*
|
|
26
|
+
* - `NOT_FOUND`: no such action, or the caller is not in its audience.
|
|
27
|
+
* - `INVALID_INPUT`: the call's inputs were refused; the message says which.
|
|
28
|
+
* - `PLAN_RESTRICTED`: the workspace's plan does not include sending this email.
|
|
29
|
+
* - `CREDITS_EXHAUSTED`: the workspace has no workflow credits left.
|
|
30
|
+
* - `RATE_LIMITED`: the app sent too many emails lately; try again later.
|
|
31
|
+
* - `DAILY_LIMIT_REACHED`: Softr's own address sent the workspace's emails for today.
|
|
32
|
+
* - `SENDER_UNAVAILABLE`: the chosen sender is missing or not confirmed, or the action leaves `to` open and would send from Softr's address.
|
|
33
|
+
* - `PROVIDER_REJECTED`: the mail provider refused the email.
|
|
34
|
+
* - `UNAVAILABLE`: Softr could not send it right now, or could not be reached.
|
|
35
|
+
* - `TIMEOUT`: no answer in time. It may still have been sent.
|
|
36
|
+
*/
|
|
37
|
+
declare class EmailError extends Error {
|
|
38
|
+
readonly status: number;
|
|
39
|
+
readonly code: EmailErrorCode;
|
|
40
|
+
constructor(status: number, code: EmailErrorCode, message: string);
|
|
41
|
+
}
|
|
42
|
+
/** Sends one email through the action. Never retries by itself. */
|
|
43
|
+
declare function sendEmail(action: EmailAction, init: EmailSendInit): Promise<EmailSent>;
|
|
44
|
+
declare const email: {
|
|
45
|
+
send: typeof sendEmail;
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
export { EmailError, EmailErrorCode, EmailSendInit, EmailSent, email };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { assert } from "../assertions.js";
|
|
2
|
+
import { softrApiUrl } from "../softr-api.js";
|
|
3
|
+
import { fetch } from "../fetch.js";
|
|
4
|
+
import { sessionConfig } from "../auth/config.js";
|
|
5
|
+
import { readSessionToken } from "../auth/session.js";
|
|
6
|
+
|
|
7
|
+
//#region src/core/email.ts
|
|
8
|
+
/** Longer than Softr waits for the send itself, so its own timeout answers first. */
|
|
9
|
+
const TIMEOUT_MS = 12e4;
|
|
10
|
+
const EMAIL_ERROR_CODES = [
|
|
11
|
+
"NOT_FOUND",
|
|
12
|
+
"INVALID_INPUT",
|
|
13
|
+
"PLAN_RESTRICTED",
|
|
14
|
+
"CREDITS_EXHAUSTED",
|
|
15
|
+
"RATE_LIMITED",
|
|
16
|
+
"DAILY_LIMIT_REACHED",
|
|
17
|
+
"SENDER_UNAVAILABLE",
|
|
18
|
+
"PROVIDER_REJECTED",
|
|
19
|
+
"UNAVAILABLE",
|
|
20
|
+
"TIMEOUT"
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Thrown when Softr did not send the email. `code` says why:
|
|
24
|
+
*
|
|
25
|
+
* - `NOT_FOUND`: no such action, or the caller is not in its audience.
|
|
26
|
+
* - `INVALID_INPUT`: the call's inputs were refused; the message says which.
|
|
27
|
+
* - `PLAN_RESTRICTED`: the workspace's plan does not include sending this email.
|
|
28
|
+
* - `CREDITS_EXHAUSTED`: the workspace has no workflow credits left.
|
|
29
|
+
* - `RATE_LIMITED`: the app sent too many emails lately; try again later.
|
|
30
|
+
* - `DAILY_LIMIT_REACHED`: Softr's own address sent the workspace's emails for today.
|
|
31
|
+
* - `SENDER_UNAVAILABLE`: the chosen sender is missing or not confirmed, or the action leaves `to` open and would send from Softr's address.
|
|
32
|
+
* - `PROVIDER_REJECTED`: the mail provider refused the email.
|
|
33
|
+
* - `UNAVAILABLE`: Softr could not send it right now, or could not be reached.
|
|
34
|
+
* - `TIMEOUT`: no answer in time. It may still have been sent.
|
|
35
|
+
*/
|
|
36
|
+
var EmailError = class extends Error {
|
|
37
|
+
status;
|
|
38
|
+
code;
|
|
39
|
+
constructor(status, code, message) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "EmailError";
|
|
42
|
+
this.status = status;
|
|
43
|
+
this.code = code;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/** Sends one email through the action. Never retries by itself. */
|
|
47
|
+
async function sendEmail(action, init) {
|
|
48
|
+
const { to, cc, bcc, replyTo, subject, body } = init;
|
|
49
|
+
const sessionToken = readSessionToken();
|
|
50
|
+
let res;
|
|
51
|
+
try {
|
|
52
|
+
res = await fetch(`${softrApiUrl("SOFTR_PLATFORM_API_URL")}/v1/vibe/applications/${sessionConfig().applicationId}/actions/${action.id}/email`, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: {
|
|
55
|
+
...sessionToken ? { Authorization: `Bearer ${sessionToken}` } : {},
|
|
56
|
+
"Content-Type": "application/json"
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify({ input: {
|
|
59
|
+
to,
|
|
60
|
+
cc,
|
|
61
|
+
bcc,
|
|
62
|
+
replyTo,
|
|
63
|
+
subject,
|
|
64
|
+
body
|
|
65
|
+
} }),
|
|
66
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof DOMException && error.name === "TimeoutError") throw new EmailError(504, "TIMEOUT", `Email "${action.id}" got no answer in time. It may still have been sent.`);
|
|
70
|
+
throw new EmailError(502, "UNAVAILABLE", `Email "${action.id}" could not reach Softr: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
|
+
}
|
|
72
|
+
if (res.ok) {
|
|
73
|
+
const sent = await res.json();
|
|
74
|
+
assert(typeof sent === "object" && sent !== null && "messageId" in sent && (typeof sent.messageId === "string" || sent.messageId === null), "the send answered without a messageId");
|
|
75
|
+
return { messageId: sent.messageId };
|
|
76
|
+
}
|
|
77
|
+
const { code, message } = await readFailure(res, `Email "${action.id}" failed with HTTP ${res.status}`);
|
|
78
|
+
throw new EmailError(res.status, code, message);
|
|
79
|
+
}
|
|
80
|
+
/** The code and message of a send Softr refused, read from its answer; `fallbackMessage` when it sent none. */
|
|
81
|
+
async function readFailure(res, fallbackMessage) {
|
|
82
|
+
const failure = await res.json().catch(() => null);
|
|
83
|
+
const known = typeof failure === "object" && failure !== null ? failure : {};
|
|
84
|
+
return {
|
|
85
|
+
code: ("code" in known ? EMAIL_ERROR_CODES.find((errorCode) => errorCode === known.code) : void 0) ?? codeOfStatus(res.status),
|
|
86
|
+
message: "message" in known && typeof known.message === "string" && known.message !== "" ? known.message : fallbackMessage
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function codeOfStatus(status) {
|
|
90
|
+
switch (status) {
|
|
91
|
+
case 400: return "INVALID_INPUT";
|
|
92
|
+
case 404: return "NOT_FOUND";
|
|
93
|
+
case 429: return "RATE_LIMITED";
|
|
94
|
+
default: return "UNAVAILABLE";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const email = { send: sendEmail };
|
|
98
|
+
|
|
99
|
+
//#endregion
|
|
100
|
+
export { EmailError, email };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { SoftrUserGroupId, SoftrUserGroups, SoftrUserProperties, User } from "./auth/user.js";
|
|
2
|
-
import { ActionPermissions, SoftrDatasources, defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
2
|
+
import { ActionPermissions, SoftrDatasources, defineAiAction, defineEmailAction, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
3
3
|
import { FileAttachment } from "./core/file-upload.js";
|
|
4
4
|
import { FileItem, useFileUpload } from "./core/use-file-upload.js";
|
|
5
5
|
import { AiAnswer, AiFile, AiGenerateInit, AiMessage, AiUsage } from "./core/ai.js";
|
|
6
|
+
import { EmailSendInit, EmailSent } from "./core/email.js";
|
|
6
7
|
import { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting } from "./formatting.js";
|
|
7
8
|
import { SoftrContext } from "./auth/context.js";
|
|
8
9
|
import { AuthSettings } from "./auth/platform.js";
|
|
@@ -18,4 +19,4 @@ import { useUserTimezone } from "./timezone.js";
|
|
|
18
19
|
import { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall } from "./pwa.js";
|
|
19
20
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
20
21
|
import { Audience } from "./route/audience.js";
|
|
21
|
-
export { type ActionPermissions, type AiAnswer, type AiFile, type AiGenerateInit, type AiMessage, type AiUsage, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type Page, type PageSettings, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type SoftrUserProperties, type User, type WeekDay, assertRouteAccess, checkAccess, defineAiAction, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
|
22
|
+
export { type ActionPermissions, type AiAnswer, type AiFile, type AiGenerateInit, type AiMessage, type AiUsage, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type EmailSendInit, type EmailSent, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type Page, type PageSettings, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type SoftrUserProperties, type User, type WeekDay, assertRouteAccess, checkAccess, defineAiAction, defineEmailAction, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
1
|
+
import { defineAiAction, defineEmailAction, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
2
2
|
import { useFileUpload } from "./core/use-file-upload.js";
|
|
3
3
|
import { checkAccess, defineVisibility, isAuthorized } from "./route/visibility.js";
|
|
4
4
|
import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./route/route.js";
|
|
@@ -14,4 +14,4 @@ import { useUserTimezone } from "./timezone.js";
|
|
|
14
14
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
15
15
|
import { Audience } from "./route/audience.js";
|
|
16
16
|
|
|
17
|
-
export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineAiAction, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
|
17
|
+
export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineAiAction, defineEmailAction, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
package/dist/package.js
CHANGED
package/dist/server.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { upload } from "./core/file-upload.js";
|
|
2
2
|
import { AiError, ai } from "./core/ai.js";
|
|
3
|
+
import { EmailError, EmailErrorCode, email } from "./core/email.js";
|
|
3
4
|
import { auth } from "./auth/server.js";
|
|
4
5
|
import { FieldChoice, RecordOption, RecordOptions } from "./core/options.js";
|
|
5
6
|
import { ds } from "./datasource/datasource.js";
|
|
6
7
|
import { workflow } from "./core/workflow.js";
|
|
7
8
|
import { integration } from "./core/integration.js";
|
|
8
9
|
import "@tanstack/react-start/server-only";
|
|
9
|
-
export { AiError, type FieldChoice, type RecordOption, type RecordOptions, ai, auth, ds, integration, upload, workflow };
|
|
10
|
+
export { AiError, EmailError, type EmailErrorCode, type FieldChoice, type RecordOption, type RecordOptions, ai, auth, ds, email, integration, upload, workflow };
|
package/dist/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import { auth } from "./auth/server.js";
|
|
|
4
4
|
import { workflow } from "./core/workflow.js";
|
|
5
5
|
import { integration } from "./core/integration.js";
|
|
6
6
|
import { AiError, ai } from "./core/ai.js";
|
|
7
|
+
import { EmailError, email } from "./core/email.js";
|
|
7
8
|
import "@tanstack/react-start/server-only";
|
|
8
9
|
|
|
9
|
-
export { AiError, ai, auth, ds, integration, upload, workflow };
|
|
10
|
+
export { AiError, EmailError, ai, auth, ds, email, integration, upload, workflow };
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: core
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.99-alpha.0
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
# Softr SDK core
|
|
@@ -22,6 +22,8 @@ metadata:
|
|
|
22
22
|
>
|
|
23
23
|
> **CRITICAL**: The app is already wired. `src/start.ts`, `src/router.tsx` and `src/routes/__root.tsx` register `softrAuthMiddleware` and resolve the session. Do not add a second registration of it. `softrMcpMiddleware` is the one thing an app adds to `src/start.ts` itself — load the `softr-sdk/mcp` sub-skill before doing so.
|
|
24
24
|
>
|
|
25
|
+
> **CRITICAL**: `email.send` lives on `@softr/sdk/server` and only runs inside a `createServerFn` handler. Use `defineEmailAction` from `@softr/sdk` to declare who may send it and the recipients it fixes. An email the app sends itself needs no workflow and no connected mail service. Load the `softr-sdk/email` sub-skill for the full pattern.
|
|
26
|
+
>
|
|
25
27
|
> **CRITICAL**: `ai.generate` and `ai.stream` live on `@softr/sdk/server` and only run inside a `createServerFn` handler. Use `defineAiAction` from `@softr/sdk` to declare the action with the model it runs on, an id copied from the `ai_list_models` tool. Softr runs the model on its own keys; never ask the user for an API key for the app's own AI feature. Load the `softr-sdk/ai` sub-skill for the full pattern.
|
|
26
28
|
>
|
|
27
29
|
> **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.
|
|
@@ -32,8 +34,8 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
|
|
|
32
34
|
|
|
33
35
|
| Import | Runs | Holds |
|
|
34
36
|
| --- | --- | --- |
|
|
35
|
-
| `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineAiAction`, `definePage`, `pageHead`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `FormattedDate`, `FormattingScope`, `useFormatting`, `formatDate`, `formatDateTime`, `formatNumber`, `formatValue`, `formatField`, `fieldShape`, `registerSoftrFields`, `useOverrides`, `SoftrPwaHead`, `usePwaInstall`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
|
|
36
|
-
| `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration`, `ai` |
|
|
37
|
+
| `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineEmailAction`, `defineAiAction`, `definePage`, `pageHead`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `FormattedDate`, `FormattingScope`, `useFormatting`, `formatDate`, `formatDateTime`, `formatNumber`, `formatValue`, `formatField`, `fieldShape`, `registerSoftrFields`, `useOverrides`, `SoftrPwaHead`, `usePwaInstall`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
|
|
38
|
+
| `@softr/sdk/server` | Server only | `ds`, `auth`, `email`, `EmailError`, `workflow`, `integration`, `ai` |
|
|
37
39
|
| `@softr/sdk/client` | Browser only | `useFileUpload` |
|
|
38
40
|
| `@softr/sdk/vite` | Build | `softr()`, already in `vite.config.ts` |
|
|
39
41
|
|
|
@@ -47,10 +49,11 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
|
|
|
47
49
|
| Fetch records from a datasource | [record-queries/SKILL.md](./record-queries/SKILL.md) |
|
|
48
50
|
| Chart, aggregate or count records | [charts-and-metrics/SKILL.md](./charts-and-metrics/SKILL.md) |
|
|
49
51
|
| Create, update or delete a record | [record-mutations/SKILL.md](./record-mutations/SKILL.md) |
|
|
52
|
+
| Send an email from the app: a form that emails the owner, a confirmation, staff emailing a client | [email/SKILL.md](./email/SKILL.md) |
|
|
50
53
|
| Trigger a Softr workflow | [workflows/SKILL.md](./workflows/SKILL.md) |
|
|
51
54
|
| Sign a visitor in, out, or up | [auth/SKILL.md](./auth/SKILL.md) |
|
|
52
55
|
| Ask an AI model: a summary, an extraction from a document, a classification, a draft, a chat | [ai/SKILL.md](./ai/SKILL.md) |
|
|
53
|
-
| Call a connected third-party API (Slack, Stripe, Gmail, …), or an AI model on the user's own provider account | [integrations/SKILL.md](./integrations/SKILL.md) |
|
|
56
|
+
| Call a connected third-party API (Slack, Stripe, Gmail from the user's own mailbox, …), or an AI model on the user's own provider account | [integrations/SKILL.md](./integrations/SKILL.md) |
|
|
54
57
|
| Open the app to AI assistants over MCP | [mcp/SKILL.md](./mcp/SKILL.md) |
|
|
55
58
|
| Let visitors install the app on their home screen | [pwa/SKILL.md](./pwa/SKILL.md) |
|
|
56
59
|
| Show a date, a time or a number | [formatting/SKILL.md](./formatting/SKILL.md) |
|
|
@@ -76,6 +79,9 @@ Drawing a chart, a stat tile, or any aggregated number?
|
|
|
76
79
|
Submitting a form, editing a record, or deleting one?
|
|
77
80
|
load softr-sdk/record-mutations
|
|
78
81
|
|
|
82
|
+
Sending an email when a form is submitted or a button is pressed?
|
|
83
|
+
load softr-sdk/email — no workflow, no connected mail service
|
|
84
|
+
|
|
79
85
|
Triggering a Softr workflow from a button or form?
|
|
80
86
|
load softr-sdk/workflows
|
|
81
87
|
|
|
@@ -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.99-alpha.0
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
**Boundary.** `ai` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineAiAction` and every type come from `@softr/sdk` and can be imported 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.99-alpha.0
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
|
|
@@ -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.99-alpha.0
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**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.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: email
|
|
3
|
+
description: >-
|
|
4
|
+
Send an email from the app itself, with nothing to connect and no workflow:
|
|
5
|
+
defineEmailAction from @softr/sdk declares who may send it and the fields it
|
|
6
|
+
fixes (to, cc, bcc, replyTo, and an optional senderId); email.send from
|
|
7
|
+
@softr/sdk/server sends one with a subject and a text or HTML body, and throws
|
|
8
|
+
an EmailError with a code when Softr does not send it. Load this for a contact
|
|
9
|
+
form that emails the owner, a confirmation to the signed-in user, staff
|
|
10
|
+
emailing a client, or any email the app sends while handling what a person
|
|
11
|
+
does in it.
|
|
12
|
+
metadata:
|
|
13
|
+
type: sub-skill
|
|
14
|
+
library: softr-sdk
|
|
15
|
+
library_version: 0.0.99-alpha.0
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
**Boundary.** `email` and `EmailError` live on `@softr/sdk/server` and run only inside a `createServerFn` handler — importing them into a component fails the build. `defineEmailAction` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
19
|
+
|
|
20
|
+
# Email
|
|
21
|
+
|
|
22
|
+
An app sends an email through an email action. Softr sends it and bills the workspace's **workflow credits**, so there is no mail service to connect and no workflow to build. Use it for every email the app sends while it handles something a person does in it: a form submitted, a button pressed, a record saved through the app.
|
|
23
|
+
|
|
24
|
+
A workflow is still the answer for an email on a schedule ("every Monday") or on a change made outside the app (a record edited in the table): load `automate-with-workflows` for those. A connected Gmail or Outlook is the answer only when the user wants the email to come from their own mailbox, through the `integrations` sub-skill.
|
|
25
|
+
|
|
26
|
+
`defineEmailAction` missing from `@softr/sdk`? The app was built on an older version. Upgrade `@softr/sdk` to the latest version first, then write the action.
|
|
27
|
+
|
|
28
|
+
## Who can send it, and to whom
|
|
29
|
+
|
|
30
|
+
The action's `permissions` is its audience: everyone in it can send an email through it, directly, without the app's own code. The app's code is not a guard. Only what the declaration fixes holds whatever the caller sends.
|
|
31
|
+
|
|
32
|
+
- **Fix the recipients whenever the use case allows.** A contact form that emails the owner fixes `to` to the owner. "Email me this report" fixes `to` to `{LOGGED_IN_USER:EMAIL}`. Then nobody can send the app's email to anyone else.
|
|
33
|
+
- **When people pick the recipients, narrow the audience.** "Staff email a client" leaves `to` open and keeps the action to the staff group with `customGroups`. An action with open recipients for all signed-in users lets anyone who signs up send any email to anyone from the app's address, up to the workspace's credits. Build that only when the user asks for it, and tell them so in one line. Such an action also needs a sender of the workspace's own (see below). An action open to visitors must fix `to`: one that fixes only `cc` or `bcc` is not stored.
|
|
34
|
+
|
|
35
|
+
## The sender
|
|
36
|
+
|
|
37
|
+
An email comes from the first of these that is set:
|
|
38
|
+
|
|
39
|
+
1. the action's `senderId`;
|
|
40
|
+
2. the app's email sender, which also sends its sign-in emails (Users → Notifications in Studio);
|
|
41
|
+
3. Softr's testing address, at most 30 emails a day per workspace. The next one fails with `DAILY_LIMIT_REACHED`.
|
|
42
|
+
|
|
43
|
+
A chosen sender that is missing or not confirmed fails every send with `SENDER_UNAVAILABLE`: Softr never falls back. An action that leaves `to` open needs 1 or 2, and fails the same way from Softr's address.
|
|
44
|
+
|
|
45
|
+
**Look before you write the action.** Call `application_get` for the app's sender and `workspace_list_email_senders` for the workspace's. Everything you say about the sender comes from those two answers.
|
|
46
|
+
|
|
47
|
+
**Ask, never pick.**
|
|
48
|
+
|
|
49
|
+
- The app has its own sender: the emails come from it. Say so.
|
|
50
|
+
- Otherwise, with confirmed senders in the workspace: name them and ask whether the emails should come from one, and if so for **the whole app** (sign-in emails too: `application_configure_email_sender` with the email only) or **only these emails** (the action's `senderId`, copied exactly; nothing else changes).
|
|
51
|
+
- With none: the emails come from Softr's testing address. Offer to send from the user's own address. Custom senders need the Professional or Business plan.
|
|
52
|
+
|
|
53
|
+
**Adding a new sender.** `application_configure_email_sender` with an email and a name adds one, and also makes it the app's sender.
|
|
54
|
+
|
|
55
|
+
- For the whole app, call it once the user approves. Softr emails a confirmation link to that address; ask the user to click it, then check it with `workspace_list_email_senders`. Until then the app keeps its current sender. Give the user the DKIM and Return-Path records it returns: without them the emails are more likely to land in spam.
|
|
56
|
+
- For only these emails, the user adds and confirms it in Studio: Users → Notifications → Email sender → Manage email senders, leaving the app's sender as it is. Then set it as the action's `senderId`.
|
|
57
|
+
|
|
58
|
+
**In your closing message**, say where the emails come from: the sender's address, or "Softr's testing address, up to 30 emails a day". Never its raw address.
|
|
59
|
+
|
|
60
|
+
## What it costs
|
|
61
|
+
|
|
62
|
+
Every email costs **5 workflow credits per recipient**, from the workspace's monthly workflow credits, which its workflows spend too. You do not know the workspace's plan, so never name one or count how many emails it covers. The plan and the closing message give the cost and where to watch it, for example: "Each submission emails you, at 5 workflow credits per email. Track them in Settings → Billing → Workflow usage, where this app has its own row."
|
|
63
|
+
|
|
64
|
+
Emails sent from the preview are real and charged, and the actions in the code reach the live app as soon as they are saved. Send at most one test email, and only when the user asks for one. Never send emails in a loop to try something out.
|
|
65
|
+
|
|
66
|
+
## Declare the action
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const notifyOwner = defineEmailAction({
|
|
70
|
+
id: 'notifyOwner',
|
|
71
|
+
to: ['owner@acme.com'],
|
|
72
|
+
permissions: { userGroupTypes: ['NON_LOGGED_IN_USERS', 'LOGGED_IN_USERS'] },
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const emailClient = defineEmailAction({
|
|
76
|
+
id: 'emailClient',
|
|
77
|
+
replyTo: '{LOGGED_IN_USER:EMAIL}',
|
|
78
|
+
permissions: { userGroupTypes: ['LOGGED_IN_USERS'], customGroups: ['<the staff group id>'] },
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- `id` is a name you choose, unique across the app.
|
|
83
|
+
- `to`, `cc`, `bcc`: arrays of addresses. Each one declared here replaces the call's, and declaring `to` also ignores the call's `cc` and `bcc`. An element may be a whole placeholder for the signed-in sender: `{LOGGED_IN_USER:EMAIL}`, or `{LOGGED_IN_USER:<field>}` for an email field of their user record. Never part of a string, never in the call.
|
|
84
|
+
- `replyTo`: an address or one such placeholder.
|
|
85
|
+
- `senderId`: one of the workspace's senders, as above. The call can never set the sender.
|
|
86
|
+
- `permissions`: who may send it, as for queries and mutations.
|
|
87
|
+
|
|
88
|
+
Write the object inline in `defineEmailAction`, every value a literal. A declaration that cannot be read is not stored, and the check reports it.
|
|
89
|
+
|
|
90
|
+
Declare one action per kind of email: the owner's notification and the client's confirmation are two actions, each with its own recipients and audience.
|
|
91
|
+
|
|
92
|
+
## Core pattern
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const submit = createServerFn({ method: 'POST' })
|
|
96
|
+
.validator(contactSchema)
|
|
97
|
+
.handler(async ({ data }) => {
|
|
98
|
+
const saved = await ds.create(saveSubmission, { data })
|
|
99
|
+
try {
|
|
100
|
+
await email.send(notifyOwner, {
|
|
101
|
+
subject: `New message from ${data.name}`,
|
|
102
|
+
body: renderSubmission(data), // the app's own HTML, with what the visitor typed escaped
|
|
103
|
+
replyTo: data.email,
|
|
104
|
+
})
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof EmailError) return { saved: true, emailed: false, reason: error.message }
|
|
107
|
+
throw error
|
|
108
|
+
}
|
|
109
|
+
return { saved: true, emailed: true }
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`email.send(action, init)` takes:
|
|
114
|
+
|
|
115
|
+
- `subject` and `body`. Required. `body` is plain text or HTML. Escape what visitors typed before you put it into HTML.
|
|
116
|
+
- `to`, `cc`, `bcc`, `replyTo`: each used only where the action does not fix it, and `cc` and `bcc` never when it fixes `to`. Each entry is one address. At most 50 recipients per email.
|
|
117
|
+
|
|
118
|
+
It answers `{ messageId }` once the email is sent. The answer never lists the recipients. An email carries no attachments: link to the file instead.
|
|
119
|
+
|
|
120
|
+
Every call is a new email. Disable the button while the request runs, so a double click does not send twice.
|
|
121
|
+
|
|
122
|
+
## Failure modes
|
|
123
|
+
|
|
124
|
+
**Not handling `EmailError`.** `email.send` throws an `EmailError` from `@softr/sdk/server`, with `status`, `code` and `message` when the email was not sent, and never retries by itself. Catch it, keep what the request already saved, and show the person a line they can act on: "Saved, but the email could not be sent." The codes:
|
|
125
|
+
|
|
126
|
+
| `code` | Meaning |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `NOT_FOUND` | No such action, or the caller is not in its audience |
|
|
129
|
+
| `INVALID_INPUT` | No subject or body, an entry or placeholder that is not exactly one address, or more than 50 recipients |
|
|
130
|
+
| `PLAN_RESTRICTED` | The workspace's plan does not allow this email |
|
|
131
|
+
| `CREDITS_EXHAUSTED` | The workspace has no workflow credits left |
|
|
132
|
+
| `RATE_LIMITED` | The app sent too often lately. Tell the person to try again later |
|
|
133
|
+
| `DAILY_LIMIT_REACHED` | Softr's own address sent its 30 emails for today. Tell the person the email could not be sent today; the owner fixes it by choosing their own sender |
|
|
134
|
+
| `SENDER_UNAVAILABLE` | The chosen sender is missing or not confirmed, or the action leaves `to` open and has no sender of the workspace's own. Tell the person the email could not be sent; the owner fixes the sender |
|
|
135
|
+
| `PROVIDER_REJECTED` | The mail provider refused the email |
|
|
136
|
+
| `UNAVAILABLE` | Softr could not send it right now or could not be reached |
|
|
137
|
+
| `TIMEOUT` | No answer in time. It may still have been sent, so never send it again by yourself |
|
|
138
|
+
|
|
139
|
+
**Letting a mistyped address reach Softr.** An address the person typed (the reply-to of a contact form, a client's email) that is not one address is refused with `INVALID_INPUT`, which reads as a vague failure. Check its format in the validator and tell them which field is wrong.
|
|
140
|
+
|
|
141
|
+
**Calling from client code.** `email` is server-only. Wrap the send in a `createServerFn` and call that from the form or the button.
|
|
142
|
+
|
|
143
|
+
**Taking recipients from the request when they could be fixed.** A `to` the browser sends is a `to` any caller can change. Fix it in the declaration, or read it on the server from the record the email is about.
|
|
144
|
+
|
|
145
|
+
**Placeholders in the call.** `{LOGGED_IN_USER:EMAIL}` is filled only in the declaration. In the call it is sent as written and fails.
|
|
146
|
+
|
|
147
|
+
**Building a workflow for an email the app sends itself.** A "Run custom workflow" trigger plus a "Send email" step does the same at more cost to the user: a workflow to keep published. Use the email action; keep workflows for schedules and table events.
|
|
@@ -7,7 +7,7 @@ description: >-
|
|
|
7
7
|
metadata:
|
|
8
8
|
type: sub-skill
|
|
9
9
|
library: softr-sdk
|
|
10
|
-
library_version: 0.0.
|
|
10
|
+
library_version: 0.0.99-alpha.0
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
**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.
|
|
@@ -7,7 +7,7 @@ description: >-
|
|
|
7
7
|
metadata:
|
|
8
8
|
type: sub-skill
|
|
9
9
|
library: softr-sdk
|
|
10
|
-
library_version: 0.0.
|
|
10
|
+
library_version: 0.0.99-alpha.0
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
**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.
|
|
@@ -16,6 +16,8 @@ metadata:
|
|
|
16
16
|
|
|
17
17
|
`integration.createFetcher` calls a third-party API through Softr's authenticated proxy. It adds the connected account's credentials automatically — you never set API keys or auth headers yourself. **Every call acts as that one account, whoever the visitor is**, so take what a call reaches, such as a customer or a channel, from the session or a record the server read, never from the arguments the browser sends.
|
|
18
18
|
|
|
19
|
+
An email the app sends is the `email` sub-skill, with nothing to connect. Send through a connected Gmail or Outlook only when the user wants the email to come from their own mailbox.
|
|
20
|
+
|
|
19
21
|
## Setup
|
|
20
22
|
|
|
21
23
|
```ts
|
|
@@ -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.99-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.
|
|
@@ -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.99-alpha.0
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**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.
|
|
@@ -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.99-alpha.0
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**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.
|
|
@@ -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.99-alpha.0
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `definePage`, `pageHead`, 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.99-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.
|
|
@@ -18,6 +18,8 @@ metadata:
|
|
|
18
18
|
|
|
19
19
|
`workflow.trigger` fires a Softr workflow and waits for it to finish. It resolves to `void`.
|
|
20
20
|
|
|
21
|
+
An email the app sends itself, when a form is submitted or a button is pressed, needs no workflow: load the `email` sub-skill. Fire a workflow for what the workflow's own steps do, such as updating a CRM or posting to a connected service.
|
|
22
|
+
|
|
21
23
|
## Setup
|
|
22
24
|
|
|
23
25
|
```ts
|
|
@@ -31,15 +33,15 @@ import { workflow } from '@softr/sdk/server'
|
|
|
31
33
|
Every trigger needs a workflow action: which workflow, which trigger step, who may run it, and any data that is always sent. The workflow must have a **Run custom workflow** trigger — no other trigger type can be fired from app code.
|
|
32
34
|
|
|
33
35
|
```ts
|
|
34
|
-
const
|
|
35
|
-
id: '
|
|
36
|
+
const syncNewLead = defineWorkflow({
|
|
37
|
+
id: 'syncNewLead',
|
|
36
38
|
workflowId: 'c9d8e7f6-a5b4-4c3d-8e2f-1a0b9c8d7e6f',
|
|
37
39
|
triggerId: '926da496-749f-4006-999c-197d8a556122',
|
|
38
40
|
permissions: { userGroupTypes: ['LOGGED_IN_USERS'] },
|
|
39
41
|
body: {
|
|
40
42
|
email: '{LOGGED_IN_USER:EMAIL}', // resolved server-side from the signed-in user
|
|
41
43
|
name: '{LOGGED_IN_USER:NAME}',
|
|
42
|
-
|
|
44
|
+
source: 'website', // any hardcoded value — always sent, never overridden at runtime
|
|
43
45
|
},
|
|
44
46
|
})
|
|
45
47
|
```
|
|
@@ -70,7 +72,7 @@ These placeholders are resolved server-side when the workflow runs:
|
|
|
70
72
|
const runWorkflow = createServerFn({ method: 'POST' })
|
|
71
73
|
.validator(z.object({ message: z.string() }))
|
|
72
74
|
.handler(async ({ data }) => {
|
|
73
|
-
await workflow.trigger(
|
|
75
|
+
await workflow.trigger(syncNewLead, { message: data.message })
|
|
74
76
|
})
|
|
75
77
|
```
|
|
76
78
|
|
|
@@ -82,7 +84,7 @@ const runWorkflow = createServerFn({ method: 'POST' })
|
|
|
82
84
|
|
|
83
85
|
```ts
|
|
84
86
|
const run = createServerFn({ method: 'POST' }).handler(async () => {
|
|
85
|
-
await workflow.trigger(
|
|
87
|
+
await workflow.trigger(syncNewLead)
|
|
86
88
|
})
|
|
87
89
|
```
|
|
88
90
|
|
|
@@ -94,11 +96,11 @@ Omit the second argument when the action's `body` already carries everything the
|
|
|
94
96
|
|
|
95
97
|
```ts
|
|
96
98
|
// Wrong: calling from a click handler
|
|
97
|
-
<button onClick={() => workflow.trigger(
|
|
99
|
+
<button onClick={() => workflow.trigger(syncNewLead)}>Run</button>
|
|
98
100
|
|
|
99
101
|
// Correct: server function, called from the button
|
|
100
102
|
const run = createServerFn({ method: 'POST' }).handler(() =>
|
|
101
|
-
workflow.trigger(
|
|
103
|
+
workflow.trigger(syncNewLead),
|
|
102
104
|
)
|
|
103
105
|
// …
|
|
104
106
|
const submit = useServerFn(run)
|