@softr/sdk 0.0.31 → 0.0.33
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/auth/isomorphic.js +2 -2
- package/dist/auth/methods/authenticator.js +1 -1
- package/dist/auth/methods/code.js +1 -1
- package/dist/auth/methods/magicLink.js +1 -1
- package/dist/auth/methods/password.js +1 -1
- package/dist/auth/methods/signup.js +1 -1
- package/dist/auth/methods/social.js +1 -1
- package/dist/auth/platform.js +2 -1
- package/dist/auth/session.js +2 -2
- package/dist/boot-error.js +1 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.js +2 -1
- package/dist/codegen/fetch-metadata.js +1 -0
- package/dist/core/file-upload.d.ts +16 -0
- package/dist/core/file-upload.js +34 -0
- package/dist/core/integration.js +1 -0
- package/dist/core/upload-fn.js +16 -0
- package/dist/core/use-file-upload.d.ts +28 -0
- package/dist/core/use-file-upload.js +105 -0
- package/dist/core/workflow.js +2 -1
- package/dist/datasource/chart.js +1 -0
- package/dist/datasource/record.js +2 -1
- package/dist/fetch.js +21 -0
- package/dist/index.d.ts +2 -1
- package/dist/logger.js +15 -0
- package/dist/package.js +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.js +2 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +1 -1
- package/package.json +1 -1
- package/skills/softr-sdk/SKILL.md +1 -1
- package/skills/softr-sdk/auth/SKILL.md +1 -1
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/file-uploads/SKILL.md +153 -0
- package/skills/softr-sdk/integrations/SKILL.md +4 -4
- package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
- package/skills/softr-sdk/record-queries/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +6 -5
package/dist/auth/isomorphic.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { assert, panic, requireDefined } from "../assertions.js";
|
|
2
2
|
import { writeCookies } from "../cookies.js";
|
|
3
3
|
import { sessionConfig } from "./config.js";
|
|
4
|
+
import { clearSession, makeSessionDataCookie, readSessionData, readSessionToken } from "./session.js";
|
|
4
5
|
import { makePlatform } from "./platform.js";
|
|
5
6
|
import { bootErrorMiddleware } from "../boot-error.js";
|
|
6
|
-
import { clearSession, makeSessionDataCookie, readSessionData, readSessionToken } from "./session.js";
|
|
7
|
-
import { rootRouteId, useRouteContext } from "@tanstack/react-router";
|
|
8
7
|
import { createMiddleware, createServerFn, getGlobalStartContext } from "@tanstack/react-start";
|
|
8
|
+
import { rootRouteId, useRouteContext } from "@tanstack/react-router";
|
|
9
9
|
|
|
10
10
|
//#region src/auth/isomorphic.ts
|
|
11
11
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { assert, panic } from "../../assertions.js";
|
|
2
2
|
import { writeCookies } from "../../cookies.js";
|
|
3
3
|
import { sessionConfig } from "../config.js";
|
|
4
|
-
import { makePlatform } from "../platform.js";
|
|
5
4
|
import { clearPendingAuth, makeSessionDataCookie, makeSessionTokenCookie, readPendingAuthToken, readSessionToken } from "../session.js";
|
|
5
|
+
import { makePlatform } from "../platform.js";
|
|
6
6
|
import { authError } from "./utils.js";
|
|
7
7
|
|
|
8
8
|
//#region src/auth/methods/authenticator.ts
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { assert, panic } from "../../assertions.js";
|
|
2
2
|
import { writeCookies } from "../../cookies.js";
|
|
3
3
|
import { sessionConfig } from "../config.js";
|
|
4
|
-
import { makePlatform } from "../platform.js";
|
|
5
4
|
import { clearPendingAuth, makePendingAuthCookie, makeSessionDataCookie, makeSessionTokenCookie } from "../session.js";
|
|
5
|
+
import { makePlatform } from "../platform.js";
|
|
6
6
|
import { authError } from "./utils.js";
|
|
7
7
|
|
|
8
8
|
//#region src/auth/methods/code.ts
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { assert, panic } from "../../assertions.js";
|
|
2
2
|
import { writeCookies } from "../../cookies.js";
|
|
3
3
|
import { sessionConfig } from "../config.js";
|
|
4
|
-
import { makePlatform } from "../platform.js";
|
|
5
4
|
import { makePendingAuthCookie, makeSessionDataCookie, makeSessionTokenCookie } from "../session.js";
|
|
5
|
+
import { makePlatform } from "../platform.js";
|
|
6
6
|
import { authError } from "./utils.js";
|
|
7
7
|
|
|
8
8
|
//#region src/auth/methods/magicLink.ts
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { assert, panic, softAssert } from "../../assertions.js";
|
|
2
2
|
import { writeCookies } from "../../cookies.js";
|
|
3
3
|
import { sessionConfig } from "../config.js";
|
|
4
|
-
import { makePlatform } from "../platform.js";
|
|
5
4
|
import { clearPendingAuth, clearSession, makePendingAuthCookie, makeSessionDataCookie, makeSessionTokenCookie, readSessionToken } from "../session.js";
|
|
5
|
+
import { makePlatform } from "../platform.js";
|
|
6
6
|
import { authError } from "./utils.js";
|
|
7
7
|
|
|
8
8
|
//#region src/auth/methods/password.ts
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { assert, panic } from "../../assertions.js";
|
|
2
2
|
import { writeCookies } from "../../cookies.js";
|
|
3
3
|
import { sessionConfig } from "../config.js";
|
|
4
|
-
import { makePlatform } from "../platform.js";
|
|
5
4
|
import { clearPendingAuth, makePendingAuthCookie, makeSessionDataCookie, makeSessionTokenCookie } from "../session.js";
|
|
5
|
+
import { makePlatform } from "../platform.js";
|
|
6
6
|
import { authError } from "./utils.js";
|
|
7
7
|
|
|
8
8
|
//#region src/auth/methods/signup.ts
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { assert, panic } from "../../assertions.js";
|
|
2
2
|
import { writeCookies } from "../../cookies.js";
|
|
3
3
|
import { sessionConfig } from "../config.js";
|
|
4
|
-
import { makePlatform } from "../platform.js";
|
|
5
4
|
import { clearPendingAuth, makePendingAuthCookie, makeSessionDataCookie, makeSessionTokenCookie } from "../session.js";
|
|
5
|
+
import { makePlatform } from "../platform.js";
|
|
6
6
|
import { authError } from "./utils.js";
|
|
7
7
|
|
|
8
8
|
//#region src/auth/methods/social.ts
|
package/dist/auth/platform.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { fetch } from "../fetch.js";
|
|
1
2
|
import { panic } from "../assertions.js";
|
|
3
|
+
import { permissionsSchema } from "./permissions.js";
|
|
2
4
|
import { sessionConfig } from "./config.js";
|
|
3
5
|
import { userSchema } from "./user.js";
|
|
4
|
-
import { permissionsSchema } from "./permissions.js";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
|
|
7
8
|
//#region src/auth/platform.ts
|
package/dist/auth/session.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readRequestCookies, serializeCookie, writeCookies } from "../cookies.js";
|
|
2
|
+
import { permissionsSchema } from "./permissions.js";
|
|
2
3
|
import { sessionConfig } from "./config.js";
|
|
3
4
|
import { userSchema } from "./user.js";
|
|
4
|
-
import {
|
|
5
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
5
6
|
import { getRequestProtocol } from "@tanstack/react-start/server";
|
|
6
7
|
import { z } from "zod";
|
|
7
|
-
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
8
8
|
|
|
9
9
|
//#region src/auth/session.ts
|
|
10
10
|
const SESSION_TOKEN = "session_token";
|
package/dist/boot-error.js
CHANGED
package/dist/client.d.ts
CHANGED
package/dist/client.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DatasourceAction } from "../action.js";
|
|
2
|
+
|
|
3
|
+
//#region src/core/file-upload.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The shape of a single uploaded file as it is stored in a record field.
|
|
7
|
+
* Pre-load existing attachments from the record into this shape for edit forms.
|
|
8
|
+
*/
|
|
9
|
+
type FileAttachment = {
|
|
10
|
+
url: string;
|
|
11
|
+
filename: string;
|
|
12
|
+
id: string;
|
|
13
|
+
};
|
|
14
|
+
declare function upload(action: DatasourceAction, file: File): Promise<FileAttachment>;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { FileAttachment, upload };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { fetch } from "../fetch.js";
|
|
2
|
+
import { sessionConfig } from "../auth/config.js";
|
|
3
|
+
import { readSessionToken } from "../auth/session.js";
|
|
4
|
+
import { platformApiUrl } from "../auth/platform.js";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
//#region src/core/file-upload.ts
|
|
8
|
+
async function upload(action, file) {
|
|
9
|
+
const { applicationId } = sessionConfig();
|
|
10
|
+
const baseUrl = await platformApiUrl();
|
|
11
|
+
const uploadUrl = new URL(`${baseUrl}/v1/vibe/applications/${applicationId}/actions/${action.id}/uploads`);
|
|
12
|
+
uploadUrl.searchParams.set("fileName", file.name);
|
|
13
|
+
const formData = new FormData();
|
|
14
|
+
formData.append("file", file);
|
|
15
|
+
const token = readSessionToken();
|
|
16
|
+
const res = await fetch(uploadUrl.toString(), {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
19
|
+
body: formData
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok) throw new Error(`File upload failed: ${res.status}`);
|
|
22
|
+
const rawUrl = await res.json();
|
|
23
|
+
const parsed = new URL(rawUrl);
|
|
24
|
+
const fileId = parsed.searchParams.get("fileId") || randomUUID();
|
|
25
|
+
parsed.searchParams.delete("fileId");
|
|
26
|
+
return {
|
|
27
|
+
url: parsed.toString(),
|
|
28
|
+
filename: file.name || "Untitled file",
|
|
29
|
+
id: fileId
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
export { upload };
|
package/dist/core/integration.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { assert } from "../assertions.js";
|
|
2
|
+
import { upload } from "./file-upload.js";
|
|
3
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
4
|
+
|
|
5
|
+
//#region src/core/upload-fn.ts
|
|
6
|
+
const uploadServerFn = createServerFn({ method: "POST" }).validator((data) => data).handler(async ({ data }) => {
|
|
7
|
+
const actionJson = data.get("action");
|
|
8
|
+
const file = data.get("file");
|
|
9
|
+
assert(typeof actionJson === "string", "upload: action is required");
|
|
10
|
+
assert(file instanceof File, "upload: file is required");
|
|
11
|
+
const attachment = await upload(JSON.parse(actionJson), file);
|
|
12
|
+
return new Response(JSON.stringify(attachment), { headers: { "Content-Type": "application/json" } });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { uploadServerFn };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DatasourceAction } from "../action.js";
|
|
2
|
+
import { FileAttachment } from "./file-upload.js";
|
|
3
|
+
|
|
4
|
+
//#region src/core/use-file-upload.d.ts
|
|
5
|
+
type FileItem = {
|
|
6
|
+
status: 'uploading';
|
|
7
|
+
id: string;
|
|
8
|
+
filename: string;
|
|
9
|
+
progress: number;
|
|
10
|
+
cancel: () => void;
|
|
11
|
+
} | (FileAttachment & {
|
|
12
|
+
status: 'done';
|
|
13
|
+
}) | {
|
|
14
|
+
status: 'error';
|
|
15
|
+
id: string;
|
|
16
|
+
filename: string;
|
|
17
|
+
error: Error;
|
|
18
|
+
};
|
|
19
|
+
declare function useFileUpload(action: DatasourceAction, options?: {
|
|
20
|
+
initialAttachments?: FileAttachment[];
|
|
21
|
+
}): {
|
|
22
|
+
files: FileItem[];
|
|
23
|
+
isUploading: boolean;
|
|
24
|
+
upload: (file: File) => void;
|
|
25
|
+
remove: (id: string) => void;
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
export { FileItem, useFileUpload };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { uploadServerFn } from "./upload-fn.js";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/core/use-file-upload.ts
|
|
5
|
+
/**
|
|
6
|
+
* The state machine behind useFileUpload, decoupled from React so it can be
|
|
7
|
+
* tested directly with a plain dispatch function.
|
|
8
|
+
*/
|
|
9
|
+
function createFileUploadStore(uploadFn, dispatch) {
|
|
10
|
+
let nextId = 0;
|
|
11
|
+
return {
|
|
12
|
+
upload(file) {
|
|
13
|
+
const id = String(++nextId);
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const cancel = () => {
|
|
16
|
+
controller.abort();
|
|
17
|
+
dispatch((prev) => prev.filter((item) => item.id !== id));
|
|
18
|
+
};
|
|
19
|
+
const onProgress = (progress) => {
|
|
20
|
+
dispatch((prev) => prev.map((item) => item.id === id && item.status === "uploading" ? {
|
|
21
|
+
...item,
|
|
22
|
+
progress
|
|
23
|
+
} : item));
|
|
24
|
+
};
|
|
25
|
+
dispatch((prev) => [...prev, {
|
|
26
|
+
status: "uploading",
|
|
27
|
+
id,
|
|
28
|
+
filename: file.name,
|
|
29
|
+
progress: 0,
|
|
30
|
+
cancel
|
|
31
|
+
}]);
|
|
32
|
+
uploadFn(file, {
|
|
33
|
+
onProgress,
|
|
34
|
+
signal: controller.signal
|
|
35
|
+
}).then((attachment) => {
|
|
36
|
+
dispatch((prev) => prev.map((item) => item.id === id ? {
|
|
37
|
+
...attachment,
|
|
38
|
+
status: "done"
|
|
39
|
+
} : item));
|
|
40
|
+
}).catch((error) => {
|
|
41
|
+
if (controller.signal.aborted) return;
|
|
42
|
+
dispatch((prev) => prev.map((item) => item.id === id ? {
|
|
43
|
+
status: "error",
|
|
44
|
+
id,
|
|
45
|
+
filename: file.name,
|
|
46
|
+
error: error instanceof Error ? error : /* @__PURE__ */ new Error("Upload failed")
|
|
47
|
+
} : item));
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
remove(id) {
|
|
51
|
+
dispatch((prev) => prev.filter((item) => item.id !== id));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function xhrUpload(action, file, { onProgress, signal }) {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const xhr = new XMLHttpRequest();
|
|
58
|
+
const fd = new FormData();
|
|
59
|
+
fd.append("file", file);
|
|
60
|
+
fd.append("action", JSON.stringify(action));
|
|
61
|
+
xhr.upload.onprogress = (e) => {
|
|
62
|
+
if (e.lengthComputable) onProgress(Math.round(e.loaded / e.total * 100));
|
|
63
|
+
};
|
|
64
|
+
signal.addEventListener("abort", () => {
|
|
65
|
+
xhr.abort();
|
|
66
|
+
reject(new DOMException("Upload cancelled", "AbortError"));
|
|
67
|
+
});
|
|
68
|
+
xhr.onload = () => {
|
|
69
|
+
if (xhr.status < 200 || xhr.status >= 300) {
|
|
70
|
+
reject(/* @__PURE__ */ new Error(`File upload failed: ${xhr.status}`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
resolve(JSON.parse(xhr.responseText));
|
|
75
|
+
} catch {
|
|
76
|
+
reject(/* @__PURE__ */ new Error("File upload: unexpected response format"));
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
xhr.onerror = () => reject(/* @__PURE__ */ new Error("File upload failed: network error"));
|
|
80
|
+
xhr.open("POST", uploadServerFn.url);
|
|
81
|
+
xhr.send(fd);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function useFileUpload(action, options) {
|
|
85
|
+
const [files, dispatch] = useState(() => (options?.initialAttachments ?? []).map((a) => ({
|
|
86
|
+
...a,
|
|
87
|
+
status: "done"
|
|
88
|
+
})));
|
|
89
|
+
const actionRef = useRef(action);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
actionRef.current = action;
|
|
92
|
+
});
|
|
93
|
+
const store = useRef(createFileUploadStore((file, opts) => xhrUpload(actionRef.current, file, opts), dispatch));
|
|
94
|
+
const upload = useCallback((file) => store.current.upload(file), []);
|
|
95
|
+
const remove = useCallback((id) => store.current.remove(id), []);
|
|
96
|
+
return {
|
|
97
|
+
files,
|
|
98
|
+
isUploading: files.some((f) => f.status === "uploading"),
|
|
99
|
+
upload,
|
|
100
|
+
remove
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
export { useFileUpload };
|
package/dist/core/workflow.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { fetch } from "../fetch.js";
|
|
1
2
|
import { sessionConfig } from "../auth/config.js";
|
|
2
|
-
import { platformApiUrl } from "../auth/platform.js";
|
|
3
3
|
import { readSessionToken } from "../auth/session.js";
|
|
4
|
+
import { platformApiUrl } from "../auth/platform.js";
|
|
4
5
|
|
|
5
6
|
//#region src/core/workflow.ts
|
|
6
7
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
package/dist/datasource/chart.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { fetch } from "../fetch.js";
|
|
1
2
|
import { assert, panic } from "../assertions.js";
|
|
2
|
-
import { isDatasourceAction } from "../action.js";
|
|
3
3
|
import { readUserTimezone } from "../cookies.js";
|
|
4
4
|
import { readSessionToken } from "../auth/session.js";
|
|
5
|
+
import { isDatasourceAction } from "../action.js";
|
|
5
6
|
import { buildFilterCriteria, buildOrderBy, dealiasFilterCriteria, dealiasOrderBy } from "./filter.js";
|
|
6
7
|
import { andAll, makeFieldSet, makeSortSet } from "./fields.js";
|
|
7
8
|
|
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,9 @@
|
|
|
1
1
|
import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
|
|
2
2
|
import { ActionPermissions, SoftrDatasources, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
3
|
+
import { FileAttachment } from "./core/file-upload.js";
|
|
3
4
|
import { SoftrContext } from "./auth/context.js";
|
|
4
5
|
import { defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
|
|
5
6
|
import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions } from "./auth/isomorphic.js";
|
|
6
7
|
import { useUserTimezone } from "./timezone.js";
|
|
7
8
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
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 };
|
|
9
|
+
export { type ActionPermissions, DefaultErrorOverlay, type FileAttachment, 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/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/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { upload } from "./core/file-upload.js";
|
|
1
2
|
import { auth } from "./auth/server.js";
|
|
2
3
|
import { ds } from "./datasource/datasource.js";
|
|
3
4
|
import { workflow } from "./core/workflow.js";
|
|
4
5
|
import { integration } from "./core/integration.js";
|
|
5
6
|
import "@tanstack/react-start/server-only";
|
|
6
|
-
export { auth, ds, integration, workflow };
|
|
7
|
+
export { auth, ds, integration, upload, workflow };
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { upload } from "./core/file-upload.js";
|
|
1
2
|
import { auth } from "./auth/server.js";
|
|
2
3
|
import { ds } from "./datasource/datasource.js";
|
|
3
4
|
import { workflow } from "./core/workflow.js";
|
|
4
5
|
import { integration } from "./core/integration.js";
|
|
5
6
|
import "@tanstack/react-start/server-only";
|
|
6
7
|
|
|
7
|
-
export { auth, ds, integration, workflow };
|
|
8
|
+
export { auth, ds, integration, upload, workflow };
|
package/dist/vite.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PluginOption } from "vite";
|
|
2
2
|
|
|
3
3
|
//#region src/vite.d.ts
|
|
4
|
-
/** Everything Softr adds to an app's Vite config: the studio edit engine, when
|
|
4
|
+
/** Everything Softr adds to an app's Vite config: the studio edit engine, when there is a pod. */
|
|
5
5
|
declare function softr(): PluginOption[];
|
|
6
6
|
//#endregion
|
|
7
7
|
export { softr };
|
package/dist/vite.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
|
|
4
4
|
//#region src/vite.ts
|
|
5
|
-
/** Everything Softr adds to an app's Vite config: the studio edit engine, when
|
|
5
|
+
/** Everything Softr adds to an app's Vite config: the studio edit engine, when there is a pod. */
|
|
6
6
|
function softr() {
|
|
7
7
|
return [editEngine()];
|
|
8
8
|
}
|
package/package.json
CHANGED
|
@@ -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.33
|
|
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.33
|
|
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.
|
|
@@ -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.33
|
|
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.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: file-uploads
|
|
3
|
+
description: >-
|
|
4
|
+
Upload files from a form or edit view: useFileUpload(action) from @softr/sdk/client does
|
|
5
|
+
everything — XHR upload with real progress, per-upload cancel, and isUploading to block
|
|
6
|
+
form submission. No route registration needed; the SDK ships its own server function.
|
|
7
|
+
For server-side uploads or plain forms, use upload(actionId, file) from @softr/sdk/server.
|
|
8
|
+
For edit forms pass initialAttachments to pre-load existing attachments.
|
|
9
|
+
metadata:
|
|
10
|
+
type: sub-skill
|
|
11
|
+
library: softr-sdk
|
|
12
|
+
library_version: 0.0.33
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
**Boundary.** `upload` lives on `@softr/sdk/server` and runs inside server functions. `useFileUpload` and `FileItem` live on `@softr/sdk/client` and run only in components. `FileAttachment` comes from `@softr/sdk` and can be imported anywhere.
|
|
16
|
+
|
|
17
|
+
# File uploads
|
|
18
|
+
|
|
19
|
+
`useFileUpload` handles the full upload cycle — XHR with real progress, per-upload cancel, and state management. It uses a server function the SDK registers itself, so no route file is needed in the app.
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { defineMutation, type FileAttachment } from '@softr/sdk'
|
|
25
|
+
import { ds } from '@softr/sdk/server'
|
|
26
|
+
import { useFileUpload, type FileItem } from '@softr/sdk/client'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Core pattern
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const createApplication = defineMutation({
|
|
33
|
+
id: 'createApplication',
|
|
34
|
+
datasourceId: '8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77',
|
|
35
|
+
operation: 'create',
|
|
36
|
+
fields: { name: 'V1StGXR8', resume: 'Z7pQm4Kd' },
|
|
37
|
+
permissions: { userGroup: 'LOGGED_IN_USERS' },
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const submitApplication = createServerFn({ method: 'POST' })
|
|
41
|
+
.validator((data: { name: string; resume: FileAttachment[] }) => data)
|
|
42
|
+
.handler(async ({ data }) => {
|
|
43
|
+
await ds.create(createApplication, { data })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function ApplicationForm() {
|
|
47
|
+
const { files, isUploading, upload, remove } = useFileUpload(createApplication)
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<form onSubmit={async (e) => {
|
|
51
|
+
e.preventDefault()
|
|
52
|
+
if (isUploading) return
|
|
53
|
+
const resume = files.filter(f => f.status === 'done') as FileAttachment[]
|
|
54
|
+
await submitApplication({ data: { name: e.currentTarget.name.value, resume } })
|
|
55
|
+
}}>
|
|
56
|
+
<input name="name" />
|
|
57
|
+
<input type="file" onChange={(e) => { const f = e.target.files?.[0]; if (f) upload(f) }} />
|
|
58
|
+
{files.map((f) => (
|
|
59
|
+
<div key={f.id}>
|
|
60
|
+
{f.filename}
|
|
61
|
+
{f.status === 'uploading' && <> — {f.progress}% <button type="button" onClick={f.cancel}>Cancel</button></>}
|
|
62
|
+
{f.status === 'done' && <button type="button" onClick={() => remove(f.id)}>Remove</button>}
|
|
63
|
+
{f.status === 'error' && <span> — Failed: {f.error.message}</span>}
|
|
64
|
+
</div>
|
|
65
|
+
))}
|
|
66
|
+
<button type="submit" disabled={isUploading}>Submit</button>
|
|
67
|
+
</form>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`useFileUpload` takes the same `DatasourceAction` you pass to `ds.create` / `ds.update` — no separate upload action to declare.
|
|
73
|
+
|
|
74
|
+
## What `FileAttachment` looks like
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
type FileAttachment = {
|
|
78
|
+
url: string // clean URL, no internal query params
|
|
79
|
+
filename: string // original filename
|
|
80
|
+
id: string // file id extracted from the upload response
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Pass `attachments` as the field value in `ds.create` / `ds.update`.
|
|
85
|
+
|
|
86
|
+
## FileItem shape
|
|
87
|
+
|
|
88
|
+
`files` is a single ordered array. Each entry is a discriminated union on `status`:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
type FileItem =
|
|
92
|
+
| { status: 'uploading'; id: string; filename: string; progress: number; cancel: () => void }
|
|
93
|
+
| { status: 'done'; id: string; filename: string; url: string } // FileAttachment + status
|
|
94
|
+
| { status: 'error'; id: string; filename: string; error: Error }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Items appear in insertion order and transition in place — a completed upload stays at its original position rather than moving to a separate list.
|
|
98
|
+
|
|
99
|
+
For form submission, filter to `done` items: `files.filter(f => f.status === 'done') as FileAttachment[]`.
|
|
100
|
+
|
|
101
|
+
## Blocking form submission
|
|
102
|
+
|
|
103
|
+
`isUploading` is `true` while any item has `status: 'uploading'`. Disable the submit button and guard the submit handler.
|
|
104
|
+
|
|
105
|
+
## Edit forms: pre-loading existing attachments
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const { files, isUploading, upload, remove } = useFileUpload(
|
|
109
|
+
updateApplication,
|
|
110
|
+
{ initialAttachments: record.fields.resume ?? [] },
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Pre-loaded attachments appear as `status: 'done'` items at the start of `files`. The existing attachments already have the `FileAttachment` shape from the datasource record.
|
|
115
|
+
|
|
116
|
+
## Server-side uploads and plain forms
|
|
117
|
+
|
|
118
|
+
For server-side use (no progress tracking needed) or plain HTML forms, use `upload` directly:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { upload, ds } from '@softr/sdk/server'
|
|
122
|
+
|
|
123
|
+
const submitResume = createServerFn({ method: 'POST' })
|
|
124
|
+
.validator((fields: FormData) => fields)
|
|
125
|
+
.handler(async ({ data }) => {
|
|
126
|
+
const uploaded = await upload(createApplication, data.get('cv') as File)
|
|
127
|
+
await ds.create(createApplication, {
|
|
128
|
+
data: { fullName: data.get('fullName') as string, cv: [uploaded] },
|
|
129
|
+
})
|
|
130
|
+
return new Response(null, { status: 303, headers: { Location: '/thank-you' } })
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
function ApplyForm() {
|
|
134
|
+
return (
|
|
135
|
+
<form action={submitResume.url} method="POST">
|
|
136
|
+
<input type="text" name="fullName" />
|
|
137
|
+
<input type="file" name="cv" />
|
|
138
|
+
<button type="submit">Apply for this role</button>
|
|
139
|
+
</form>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Failure modes
|
|
145
|
+
|
|
146
|
+
**Submitting while `isUploading` is true.** Only `done` items have a `url` and should be submitted. In-progress uploads finish after the record is saved and are never included. Always filter to `status === 'done'` before passing to `ds.create` / `ds.update`.
|
|
147
|
+
|
|
148
|
+
**Not passing `initialAttachments` on an edit form.** The hook starts with an empty list. Without pre-loading, the first save erases existing attachments.
|
|
149
|
+
|
|
150
|
+
## References
|
|
151
|
+
|
|
152
|
+
- [record-mutations/SKILL.md](../record-mutations/SKILL.md): creating and updating records with the collected attachments
|
|
153
|
+
- [current-user/SKILL.md](../current-user/SKILL.md): gating uploads to the signed-in user
|
|
@@ -3,13 +3,13 @@ name: integrations
|
|
|
3
3
|
description: >-
|
|
4
4
|
Call a third-party service connected to the app as an integration:
|
|
5
5
|
defineIntegration from @softr/sdk declares the action with its datasourceId
|
|
6
|
-
and permissions; integration.createFetcher from @softr/sdk/server binds it
|
|
7
|
-
|
|
8
|
-
|
|
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().
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.33
|
|
13
13
|
---
|
|
14
14
|
|
|
15
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.
|
|
@@ -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.33
|
|
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.
|
|
@@ -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.33
|
|
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.
|
|
@@ -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.33
|
|
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.
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: workflows
|
|
3
3
|
description: >-
|
|
4
|
-
Trigger a Softr workflow from a server function: defineWorkflow from
|
|
5
|
-
declares the action with its workflowId, triggerId, permissions,
|
|
6
|
-
hardcoded body; workflow.trigger from @softr/sdk/server fires it
|
|
7
|
-
flat runtime payload and resolves to void when the workflow
|
|
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.
|
|
8
9
|
metadata:
|
|
9
10
|
type: sub-skill
|
|
10
11
|
library: softr-sdk
|
|
11
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.33
|
|
12
13
|
---
|
|
13
14
|
|
|
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.
|