@softr/sdk 0.0.11 → 0.0.13
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/assertions.js +68 -0
- package/dist/auth/config.js +27 -0
- package/dist/auth/context.d.ts +18 -0
- package/dist/auth/isomorphic.d.ts +73 -0
- package/dist/auth/isomorphic.js +113 -0
- package/dist/auth/methods/password.d.ts +22 -0
- package/dist/auth/methods/password.js +51 -0
- package/dist/auth/methods/signup.d.ts +37 -0
- package/dist/auth/methods/signup.js +89 -0
- package/dist/auth/methods/utils.d.ts +15 -0
- package/dist/auth/methods/utils.js +13 -0
- package/dist/auth/permissions.d.ts +17 -0
- package/dist/auth/permissions.js +13 -0
- package/dist/auth/platform.js +138 -0
- package/dist/auth/server.d.ts +17 -0
- package/dist/auth/server.js +17 -0
- package/dist/auth/session.js +167 -0
- package/dist/{user-D23hwTsf.d.ts → auth/user.d.ts} +1 -1
- package/dist/client/timezone.d.ts +4 -0
- package/dist/client/timezone.js +11 -0
- package/dist/client.d.ts +1 -4
- package/dist/client.js +1 -9
- package/dist/cookies.js +47 -0
- package/dist/core/datasource.d.ts +21 -0
- package/dist/core/datasource.js +196 -0
- package/dist/core/integration.d.ts +7 -0
- package/dist/core/integration.js +32 -0
- package/dist/core/query.d.ts +354 -0
- package/dist/core/query.js +528 -0
- package/dist/core/workflow.d.ts +7 -0
- package/dist/core/workflow.js +19 -0
- package/dist/index.d.ts +8 -685
- package/dist/index.js +4 -1019
- package/dist/routes/discover.d.ts +9 -0
- package/dist/routes/discover.js +198 -0
- package/dist/routes/manifest.d.ts +17 -0
- package/dist/routes/manifest.js +148 -0
- package/dist/routes/mem-fs.js +71 -0
- package/dist/routes/visibility.d.ts +142 -0
- package/dist/routes/visibility.js +125 -0
- package/dist/server.d.ts +5 -84
- package/dist/server.js +5 -194
- package/dist/types.d.ts +105 -0
- package/dist/vite.d.ts +1 -21
- package/dist/vite.js +2 -405
- package/package.json +4 -4
- package/dist/session-GUEI96C8.js +0 -360
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { panic } from "../assertions.js";
|
|
2
|
+
import { sessionConfig } from "./config.js";
|
|
3
|
+
import { permissionsSchema } from "./permissions.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
//#region src/auth/platform.ts
|
|
7
|
+
function makePlatform(applicationId) {
|
|
8
|
+
return {
|
|
9
|
+
session: async (token) => {
|
|
10
|
+
const result = await request(`/v1/vibe/applications/${applicationId}/users/session`, {
|
|
11
|
+
method: "GET",
|
|
12
|
+
token
|
|
13
|
+
}, z.object({
|
|
14
|
+
user: z.object({
|
|
15
|
+
id: z.string(),
|
|
16
|
+
email: z.string().nullish(),
|
|
17
|
+
fullName: z.string().nullish(),
|
|
18
|
+
userGroupIds: z.array(z.string()).nullish()
|
|
19
|
+
}).nullish(),
|
|
20
|
+
permissions: permissionsSchema
|
|
21
|
+
}).transform(({ user, permissions }) => ({
|
|
22
|
+
user: user ? {
|
|
23
|
+
id: user.id,
|
|
24
|
+
email: user.email ?? "",
|
|
25
|
+
name: user.fullName ?? "",
|
|
26
|
+
userGroupIds: user.userGroupIds ?? []
|
|
27
|
+
} : null,
|
|
28
|
+
permissions
|
|
29
|
+
})));
|
|
30
|
+
if (result.status === 404) panic(`Application ${applicationId} not found. Check SOFTR_APPLICATION_ID.`);
|
|
31
|
+
return result;
|
|
32
|
+
},
|
|
33
|
+
tokens: (credentials) => request(`/v1/vibe/applications/${applicationId}/users/tokens`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
json: credentials
|
|
36
|
+
}, z.object({
|
|
37
|
+
jwt: z.string(),
|
|
38
|
+
user: z.object({
|
|
39
|
+
id: z.string(),
|
|
40
|
+
email: z.string().nullish(),
|
|
41
|
+
fullName: z.string().nullish(),
|
|
42
|
+
userGroupIds: z.array(z.string()).nullish()
|
|
43
|
+
}).nullish(),
|
|
44
|
+
permissions: permissionsSchema
|
|
45
|
+
}).transform(({ jwt, user, permissions }) => ({
|
|
46
|
+
token: jwt,
|
|
47
|
+
session: {
|
|
48
|
+
user: user ? {
|
|
49
|
+
id: user.id,
|
|
50
|
+
email: user.email ?? "",
|
|
51
|
+
name: user.fullName ?? "",
|
|
52
|
+
userGroupIds: user.userGroupIds ?? []
|
|
53
|
+
} : null,
|
|
54
|
+
permissions
|
|
55
|
+
}
|
|
56
|
+
}))),
|
|
57
|
+
signIn: (credentials) => request(`/v1/applications/${applicationId}/users/sign-in`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
json: credentials
|
|
60
|
+
}, z.object({
|
|
61
|
+
jwt: z.string().nullish(),
|
|
62
|
+
authenticatorAppAction: z.string().nullish()
|
|
63
|
+
}).nullable()),
|
|
64
|
+
signUp: (body) => request(`/v1/applications/${applicationId}/users/sign-up`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
json: body
|
|
67
|
+
}, z.object({ jwt: z.string().nullish() }).nullable()),
|
|
68
|
+
new: (body) => request(`/v1/applications/${applicationId}/users/new`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
json: body
|
|
71
|
+
}, z.object({ jwt: z.string().nullish() }).nullable()),
|
|
72
|
+
signOut: (token) => request(`/v1/applications/${applicationId}/users/sign-out`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
token
|
|
75
|
+
}, z.null())
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const errorSchema = z.object({ message: z.string() });
|
|
79
|
+
async function request(path, init, schema, parse = (response$1) => response$1.json()) {
|
|
80
|
+
const response = await fetch(`${await platformApiUrl()}${path}`, {
|
|
81
|
+
method: init.method,
|
|
82
|
+
headers: {
|
|
83
|
+
accept: "application/json",
|
|
84
|
+
...init.json === void 0 ? {} : { "content-type": "application/json" },
|
|
85
|
+
...init.token ? { authorization: `Bearer ${init.token}` } : {}
|
|
86
|
+
},
|
|
87
|
+
...init.json === void 0 ? {} : { body: JSON.stringify(init.json) }
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const error = errorSchema.safeParse(await response.json().catch(() => null));
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
status: response.status,
|
|
94
|
+
message: error.success && error.data.message ? error.data.message : response.statusText
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const parsed = schema.safeParse(await parse(response).catch(() => null));
|
|
98
|
+
if (!parsed.success) throw new Error(`Unexpected ${path.split("/").pop()} response: ` + z.prettifyError(parsed.error));
|
|
99
|
+
return {
|
|
100
|
+
ok: true,
|
|
101
|
+
status: response.status,
|
|
102
|
+
data: parsed.data
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
let cached;
|
|
106
|
+
async function platformApiUrl() {
|
|
107
|
+
cached ??= resolvePlatformApiUrl().catch((error) => {
|
|
108
|
+
cached = void 0;
|
|
109
|
+
throw error;
|
|
110
|
+
});
|
|
111
|
+
return cached;
|
|
112
|
+
}
|
|
113
|
+
const contextSchema = z.object({ platformApiUrl: z.string() });
|
|
114
|
+
async function resolvePlatformApiUrl() {
|
|
115
|
+
const pinned = process.env.SOFTR_PLATFORM_API_URL;
|
|
116
|
+
if (pinned) return withoutTrailingSlash(pinned);
|
|
117
|
+
const studioApiUrl = process.env.SOFTR_STUDIO_API_URL;
|
|
118
|
+
if (!studioApiUrl || process.env.NODE_ENV === "production") panic("SOFTR_PLATFORM_API_URL is not set.");
|
|
119
|
+
const { applicationId } = sessionConfig();
|
|
120
|
+
const url = `${withoutTrailingSlash(studioApiUrl)}/v1/api/applications/${applicationId}/context/public`;
|
|
121
|
+
let response;
|
|
122
|
+
try {
|
|
123
|
+
response = await fetch(url, { headers: { accept: "application/json" } });
|
|
124
|
+
} catch {
|
|
125
|
+
panic(`Could not reach ${url}`);
|
|
126
|
+
}
|
|
127
|
+
if (response.status === 404) panic(`Unknown application ${applicationId}`);
|
|
128
|
+
if (!response.ok) panic(`${url} answered ${response.status}`);
|
|
129
|
+
const body = contextSchema.safeParse(await response.json().catch(() => null));
|
|
130
|
+
if (!body.success) panic(`No platformApiUrl at ${url}`);
|
|
131
|
+
return withoutTrailingSlash(body.data.platformApiUrl);
|
|
132
|
+
}
|
|
133
|
+
function withoutTrailingSlash(url) {
|
|
134
|
+
return url.replace(/\/+$/, "");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
//#endregion
|
|
138
|
+
export { makePlatform };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { signIn, signOut } from "./methods/password.js";
|
|
2
|
+
import { confirmSignUp, signUp } from "./methods/signup.js";
|
|
3
|
+
import "@tanstack/react-start/server-only";
|
|
4
|
+
|
|
5
|
+
//#region src/auth/server.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Server-only auth methods, should be called from the app's own `createServerFn` handlers.
|
|
9
|
+
*/
|
|
10
|
+
declare const auth: {
|
|
11
|
+
signIn: typeof signIn;
|
|
12
|
+
signOut: typeof signOut;
|
|
13
|
+
signUp: typeof signUp;
|
|
14
|
+
confirmSignUp: typeof confirmSignUp;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { auth };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { signIn, signOut } from "./methods/password.js";
|
|
2
|
+
import { confirmSignUp, signUp } from "./methods/signup.js";
|
|
3
|
+
import "@tanstack/react-start/server-only";
|
|
4
|
+
|
|
5
|
+
//#region src/auth/server.ts
|
|
6
|
+
/**
|
|
7
|
+
* Server-only auth methods, should be called from the app's own `createServerFn` handlers.
|
|
8
|
+
*/
|
|
9
|
+
const auth = {
|
|
10
|
+
signIn,
|
|
11
|
+
signOut,
|
|
12
|
+
signUp,
|
|
13
|
+
confirmSignUp
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
export { auth };
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { readRequestCookies, serializeCookie, writeCookies } from "../cookies.js";
|
|
2
|
+
import { sessionConfig } from "./config.js";
|
|
3
|
+
import { permissionsSchema } from "./permissions.js";
|
|
4
|
+
import { getRequestProtocol } from "@tanstack/react-start/server";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
//#region src/auth/session.ts
|
|
9
|
+
const SESSION_TOKEN = "session_token";
|
|
10
|
+
const SESSION_DATA = "session_data";
|
|
11
|
+
const SESSION_DATA_TTL_SECONDS = 300;
|
|
12
|
+
/**
|
|
13
|
+
* A browser refuses a `__Host-` cookie unless it is `Secure`, `Path=/` and free
|
|
14
|
+
* of `Domain` — that last clause is the one worth having, since it stops a
|
|
15
|
+
* sibling host planting a cookie of this name for us to honour.
|
|
16
|
+
*/
|
|
17
|
+
const HOST_PREFIX = "__Host-";
|
|
18
|
+
function requestIsSecure() {
|
|
19
|
+
return getRequestProtocol() === "https";
|
|
20
|
+
}
|
|
21
|
+
function sessionCookie(base) {
|
|
22
|
+
const secure = requestIsSecure();
|
|
23
|
+
return {
|
|
24
|
+
name: secure ? HOST_PREFIX + base : base,
|
|
25
|
+
secure
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** The platform token this request carries, or `null`. */
|
|
29
|
+
function readSessionToken() {
|
|
30
|
+
const { name } = sessionCookie(SESSION_TOKEN);
|
|
31
|
+
return openEnvelope(readRequestCookies()[name], SESSION_TOKEN);
|
|
32
|
+
}
|
|
33
|
+
/** The cached session for `sessionToken`, or `null` on any kind of miss. */
|
|
34
|
+
function readSessionData(sessionToken) {
|
|
35
|
+
const { name } = sessionCookie(SESSION_DATA);
|
|
36
|
+
const payload = parseSessionData(openEnvelope(readRequestCookies()[name], SESSION_DATA));
|
|
37
|
+
if (payload === null) return null;
|
|
38
|
+
if (payload.exp <= Math.floor(Date.now() / 1e3)) return null;
|
|
39
|
+
if (payload.tb !== tokenBinding(sessionToken)) return null;
|
|
40
|
+
return {
|
|
41
|
+
user: payload.user,
|
|
42
|
+
permissions: payload.permissions
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function makeSessionTokenCookie(token) {
|
|
46
|
+
const { name, secure } = sessionCookie(SESSION_TOKEN);
|
|
47
|
+
return serializeCookie(name, sealEnvelope(token, SESSION_TOKEN), {
|
|
48
|
+
maxAge: sessionConfig().sessionMaxAge,
|
|
49
|
+
secure
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function makeSessionDataCookie(session, sessionToken) {
|
|
53
|
+
const { name, secure } = sessionCookie(SESSION_DATA);
|
|
54
|
+
const payload = {
|
|
55
|
+
user: session.user,
|
|
56
|
+
permissions: session.permissions,
|
|
57
|
+
exp: Math.floor(Date.now() / 1e3) + SESSION_DATA_TTL_SECONDS,
|
|
58
|
+
tb: tokenBinding(sessionToken)
|
|
59
|
+
};
|
|
60
|
+
return serializeCookie(name, sealEnvelope(JSON.stringify(payload), SESSION_DATA), {
|
|
61
|
+
maxAge: SESSION_DATA_TTL_SECONDS,
|
|
62
|
+
secure
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Writes rather than returns: a forgotten list would leave the session live.
|
|
67
|
+
*/
|
|
68
|
+
function clearSession() {
|
|
69
|
+
clearCookies([SESSION_TOKEN, SESSION_DATA]);
|
|
70
|
+
}
|
|
71
|
+
const sessionDataSchema = z.object({
|
|
72
|
+
user: z.object({
|
|
73
|
+
id: z.string(),
|
|
74
|
+
email: z.string(),
|
|
75
|
+
name: z.string(),
|
|
76
|
+
userGroupIds: z.array(z.string())
|
|
77
|
+
}).nullable(),
|
|
78
|
+
permissions: permissionsSchema,
|
|
79
|
+
exp: z.number(),
|
|
80
|
+
tb: z.string()
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* Both spellings on https: a jar can hold both pairs across a move between
|
|
84
|
+
* schemes, and the one left behind would revive if the scheme changed back.
|
|
85
|
+
*/
|
|
86
|
+
function clearCookies(bases) {
|
|
87
|
+
const secure = requestIsSecure();
|
|
88
|
+
writeCookies(bases.flatMap((base) => secure ? [HOST_PREFIX + base, base] : [base]).map((name) => serializeCookie(name, "", {
|
|
89
|
+
maxAge: 0,
|
|
90
|
+
secure
|
|
91
|
+
})));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Over the plaintext token, never the cookie value: that is resealed with a
|
|
95
|
+
* fresh iv on every write, so a binding over its bytes would be a permanent
|
|
96
|
+
* cache miss.
|
|
97
|
+
*/
|
|
98
|
+
function tokenBinding(sessionToken) {
|
|
99
|
+
if (sessionToken === null) return "anonymous";
|
|
100
|
+
return createHash("sha256").update(sessionToken).digest("base64url");
|
|
101
|
+
}
|
|
102
|
+
function parseSessionData(json) {
|
|
103
|
+
if (json === null) return null;
|
|
104
|
+
let payload;
|
|
105
|
+
try {
|
|
106
|
+
payload = JSON.parse(json);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const data = sessionDataSchema.safeParse(payload);
|
|
111
|
+
return data.success ? data.data : null;
|
|
112
|
+
}
|
|
113
|
+
const IV_BYTES = 12;
|
|
114
|
+
const TAG_BYTES = 16;
|
|
115
|
+
/** Iv + one byte of ciphertext + tag. Anything shorter is not ours. */
|
|
116
|
+
const MIN_ENVELOPE_BYTES = IV_BYTES + TAG_BYTES + 1;
|
|
117
|
+
/**
|
|
118
|
+
* `Buffer.from(…, "base64url")` skips stray characters rather than throwing.
|
|
119
|
+
*/
|
|
120
|
+
const BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
121
|
+
/**
|
|
122
|
+
* One key for both cookies. The application id goes in with the secret so a
|
|
123
|
+
* cookie from staging is inert in production even when the two share a secret.
|
|
124
|
+
*/
|
|
125
|
+
function sessionKey() {
|
|
126
|
+
const { applicationId, sessionSecret } = sessionConfig();
|
|
127
|
+
return createHash("sha256").update(Buffer.from(sessionSecret, "hex")).update(applicationId).digest();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* `base64url(iv ‖ ct ‖ tag)`, sealed with AES-256-GCM — an AEAD, so opening
|
|
131
|
+
* throws on a single flipped bit and needs no separate signature. The iv must
|
|
132
|
+
* be fresh per seal: nonce reuse under one key leaks the payloads and the
|
|
133
|
+
* authentication subkey.
|
|
134
|
+
*
|
|
135
|
+
* The purpose is bound in as additional authenticated data rather than by
|
|
136
|
+
* giving each cookie a key of its own: one key, and a `session_data` value
|
|
137
|
+
* moved into the token slot then fails to open at all instead of opening into
|
|
138
|
+
* a later field check.
|
|
139
|
+
*/
|
|
140
|
+
function sealEnvelope(payload, purpose) {
|
|
141
|
+
const iv = randomBytes(IV_BYTES);
|
|
142
|
+
const cipher = createCipheriv("aes-256-gcm", sessionKey(), iv);
|
|
143
|
+
cipher.setAAD(Buffer.from(purpose));
|
|
144
|
+
const sealed = Buffer.concat([cipher.update(payload, "utf8"), cipher.final()]);
|
|
145
|
+
return Buffer.concat([
|
|
146
|
+
iv,
|
|
147
|
+
sealed,
|
|
148
|
+
cipher.getAuthTag()
|
|
149
|
+
]).toString("base64url");
|
|
150
|
+
}
|
|
151
|
+
/** `null` for anything that is not one of ours, with no reason attached. */
|
|
152
|
+
function openEnvelope(raw, purpose) {
|
|
153
|
+
if (!raw || !BASE64URL.test(raw)) return null;
|
|
154
|
+
try {
|
|
155
|
+
const bytes = Buffer.from(raw, "base64url");
|
|
156
|
+
if (bytes.length < MIN_ENVELOPE_BYTES) return null;
|
|
157
|
+
const decipher = createDecipheriv("aes-256-gcm", sessionKey(), bytes.subarray(0, IV_BYTES));
|
|
158
|
+
decipher.setAAD(Buffer.from(purpose));
|
|
159
|
+
decipher.setAuthTag(bytes.subarray(bytes.length - TAG_BYTES));
|
|
160
|
+
return Buffer.concat([decipher.update(bytes.subarray(IV_BYTES, bytes.length - TAG_BYTES)), decipher.final()]).toString("utf8");
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
export { clearSession, makeSessionDataCookie, makeSessionTokenCookie, readSessionData, readSessionToken };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/client/timezone.ts
|
|
4
|
+
function useUserTimezone() {
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
document.cookie = `user-timezone=${Intl.DateTimeFormat().resolvedOptions().timeZone}; path=/`;
|
|
7
|
+
}, []);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
export { useUserTimezone };
|
package/dist/client.d.ts
CHANGED
package/dist/client.js
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
|
+
import { useUserTimezone } from "./client/timezone.js";
|
|
1
2
|
import "@tanstack/react-start/client-only";
|
|
2
|
-
import { useEffect } from "react";
|
|
3
3
|
|
|
4
|
-
//#region src/client/timezone.ts
|
|
5
|
-
function useUserTimezone() {
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
document.cookie = `user-timezone=${Intl.DateTimeFormat().resolvedOptions().timeZone}; path=/`;
|
|
8
|
-
}, []);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
//#endregion
|
|
12
4
|
export { useUserTimezone };
|
package/dist/cookies.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getRequestHeader, getResponseHeaders, setResponseHeader } from "@tanstack/react-start/server";
|
|
2
|
+
|
|
3
|
+
//#region src/cookies.ts
|
|
4
|
+
function parseCookieHeader(header) {
|
|
5
|
+
const cookies = {};
|
|
6
|
+
if (!header) return cookies;
|
|
7
|
+
for (const pair of header.split(";")) {
|
|
8
|
+
const separator = pair.indexOf("=");
|
|
9
|
+
if (separator === -1) continue;
|
|
10
|
+
const name = pair.slice(0, separator).trim();
|
|
11
|
+
if (name.length === 0) continue;
|
|
12
|
+
cookies[name] = decode(pair.slice(separator + 1).trim());
|
|
13
|
+
}
|
|
14
|
+
return cookies;
|
|
15
|
+
}
|
|
16
|
+
function decode(value) {
|
|
17
|
+
try {
|
|
18
|
+
return decodeURIComponent(value);
|
|
19
|
+
} catch {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function readRequestCookies() {
|
|
24
|
+
return parseCookieHeader(getRequestHeader("cookie"));
|
|
25
|
+
}
|
|
26
|
+
function serializeCookie(name, value, { maxAge, secure }) {
|
|
27
|
+
if (name.startsWith("__Host-") && !secure) throw new Error(`Refusing to write ${name} without Secure: a __Host- cookie is only valid when Secure, Path=/ and free of Domain, and a browser silently discards one that is not.`);
|
|
28
|
+
const parts = [
|
|
29
|
+
`${name}=${encodeURIComponent(value)}`,
|
|
30
|
+
"Path=/",
|
|
31
|
+
"HttpOnly"
|
|
32
|
+
];
|
|
33
|
+
if (secure) parts.push("Secure");
|
|
34
|
+
parts.push("SameSite=Lax", `Max-Age=${maxAge}`);
|
|
35
|
+
return parts.join("; ");
|
|
36
|
+
}
|
|
37
|
+
const USER_TIMEZONE_COOKIE = "user-timezone";
|
|
38
|
+
function readUserTimezone() {
|
|
39
|
+
return readRequestCookies()[USER_TIMEZONE_COOKIE] ?? null;
|
|
40
|
+
}
|
|
41
|
+
function writeCookies(cookies) {
|
|
42
|
+
if (cookies.length === 0) return;
|
|
43
|
+
setResponseHeader("Set-Cookie", [...getResponseHeaders().getSetCookie(), ...cookies]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { readRequestCookies, readUserTimezone, serializeCookie, writeCookies };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ChartDataOptions, ChartDataResult, CreateOptions, GetOptions, ListOptions, ListResult, MetricOptions, MetricResult, SoftrRecord, UpdateOptions } from "../types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/core/datasource.d.ts
|
|
4
|
+
declare function listRecords(datasourceId: string, options?: ListOptions): Promise<ListResult>;
|
|
5
|
+
declare function getRecord(datasourceId: string, options: GetOptions): Promise<SoftrRecord | null>;
|
|
6
|
+
declare function createRecord(datasourceId: string, options: CreateOptions): Promise<SoftrRecord>;
|
|
7
|
+
declare function updateRecord(datasourceId: string, options: UpdateOptions): Promise<SoftrRecord>;
|
|
8
|
+
declare function deleteRecord(datasourceId: string, id: string): Promise<void>;
|
|
9
|
+
declare function getMetric(datasourceId: string, options: MetricOptions): Promise<MetricResult>;
|
|
10
|
+
declare function getChartData(datasourceId: string, options: ChartDataOptions): Promise<ChartDataResult>;
|
|
11
|
+
declare const ds: {
|
|
12
|
+
list: typeof listRecords;
|
|
13
|
+
get: typeof getRecord;
|
|
14
|
+
create: typeof createRecord;
|
|
15
|
+
update: typeof updateRecord;
|
|
16
|
+
delete: typeof deleteRecord;
|
|
17
|
+
metric: typeof getMetric;
|
|
18
|
+
chartData: typeof getChartData;
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
export { ds };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { buildFilterCriteria, buildOrderBy, dealiasFilterCriteria, dealiasOrderBy } from "./query.js";
|
|
2
|
+
import { readUserTimezone } from "../cookies.js";
|
|
3
|
+
import { readSessionToken } from "../auth/session.js";
|
|
4
|
+
|
|
5
|
+
//#region src/core/datasource.ts
|
|
6
|
+
const MAX_RECORD_COUNT = 100;
|
|
7
|
+
const COUNT_CNT = "COUNT_CNT";
|
|
8
|
+
const BASE_URL = () => (import.meta.env.VITE_SOFTR_BASE_URL ?? "").replace(/\/$/, "");
|
|
9
|
+
async function listRecords(datasourceId, options = {}) {
|
|
10
|
+
const { select, where, orderBy } = options;
|
|
11
|
+
const count = Math.min(options.limit ?? 20, MAX_RECORD_COUNT);
|
|
12
|
+
const offset = options.offset ?? null;
|
|
13
|
+
const filterCriteria = where ? dealiasFilterCriteria(buildFilterCriteria(where), select ?? {}) : {};
|
|
14
|
+
const sortingOption = orderBy ? dealiasOrderBy(buildOrderBy(orderBy), select ?? {}) : void 0;
|
|
15
|
+
const res = await fetch(`${BASE_URL()}/api/datasources/${datasourceId}/records`, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
"X-User-Timezone": options.timeZone ?? readUserTimezone() ?? "UTC",
|
|
20
|
+
"Authorization": `Bearer ${readSessionToken()}`
|
|
21
|
+
},
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
options: { userLocale: "en-US" },
|
|
24
|
+
filterCriteria,
|
|
25
|
+
sortingOption,
|
|
26
|
+
pagingOption: {
|
|
27
|
+
offset,
|
|
28
|
+
count
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) throw new Error(`Softr datasource list failed for "${datasourceId}": ${res.status}`);
|
|
33
|
+
const payload = await res.json();
|
|
34
|
+
const nextOffset = payload.offset < payload.total ? payload.offset : null;
|
|
35
|
+
return {
|
|
36
|
+
records: payload.items.map((item) => ({
|
|
37
|
+
id: item.id,
|
|
38
|
+
fields: select ? aliasFields(item.fields, select) : item.fields
|
|
39
|
+
})),
|
|
40
|
+
total: payload.total,
|
|
41
|
+
nextOffset
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function getRecord(datasourceId, options) {
|
|
45
|
+
if (options.id !== void 0) {
|
|
46
|
+
if (!options.id) throw new Error(`Softr datasource get: id must not be empty`);
|
|
47
|
+
const res = await fetch(`${BASE_URL()}/api/datasources/${datasourceId}/records/${options.id}`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: {
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
"X-User-Timezone": options.timeZone ?? readUserTimezone() ?? "UTC",
|
|
52
|
+
"Authorization": `Bearer ${readSessionToken()}`
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({ options: { userLocale: "en-US" } })
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) throw new Error(`Softr datasource get failed for "${datasourceId}": ${res.status}`);
|
|
57
|
+
const contentLength = res.headers.get("Content-Length");
|
|
58
|
+
if (contentLength && parseInt(contentLength, 10) === 0) return null;
|
|
59
|
+
return res.json();
|
|
60
|
+
}
|
|
61
|
+
return (await listRecords(datasourceId, {
|
|
62
|
+
where: options.where,
|
|
63
|
+
limit: 1,
|
|
64
|
+
timeZone: options.timeZone
|
|
65
|
+
})).records[0] ?? null;
|
|
66
|
+
}
|
|
67
|
+
async function createRecord(datasourceId, options) {
|
|
68
|
+
const res = await fetch(`${BASE_URL()}/api/datasources/${datasourceId}/records`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
"Authorization": `Bearer ${readSessionToken()}`
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({ fields: options.fields })
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok) throw new Error(`Softr datasource create failed for "${datasourceId}": ${res.status}`);
|
|
77
|
+
return (await res.json()).record;
|
|
78
|
+
}
|
|
79
|
+
async function updateRecord(datasourceId, options) {
|
|
80
|
+
if (!options.id) throw new Error(`Softr datasource update: id must not be empty`);
|
|
81
|
+
const res = await fetch(`${BASE_URL()}/api/datasources/${datasourceId}/records/${options.id}`, {
|
|
82
|
+
method: "PATCH",
|
|
83
|
+
headers: {
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
"Authorization": `Bearer ${readSessionToken()}`
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify({ fields: options.fields })
|
|
88
|
+
});
|
|
89
|
+
if (!res.ok) throw new Error(`Softr datasource update failed for "${datasourceId}": ${res.status}`);
|
|
90
|
+
return (await res.json()).record;
|
|
91
|
+
}
|
|
92
|
+
async function deleteRecord(datasourceId, id) {
|
|
93
|
+
const res = await fetch(`${BASE_URL()}/api/datasources/${datasourceId}/records/${id}`, {
|
|
94
|
+
method: "DELETE",
|
|
95
|
+
headers: { "Authorization": `Bearer ${readSessionToken()}` }
|
|
96
|
+
});
|
|
97
|
+
if (!res.ok) throw new Error(`Softr datasource delete failed for "${datasourceId}": ${res.status}`);
|
|
98
|
+
}
|
|
99
|
+
async function getMetric(datasourceId, options) {
|
|
100
|
+
const { select, where, aggregation, field } = options;
|
|
101
|
+
const fieldId = field ? select?.[field] ?? field : void 0;
|
|
102
|
+
const filterCriteria = where ? dealiasFilterCriteria(buildFilterCriteria(where), select ?? {}) : {};
|
|
103
|
+
const res = await fetch(`${BASE_URL()}/api/datasources/${datasourceId}/chartMetricData`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
"X-User-Timezone": options.timeZone ?? readUserTimezone() ?? "UTC",
|
|
108
|
+
"Authorization": `Bearer ${readSessionToken()}`
|
|
109
|
+
},
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
options: { userLocale: "en-US" },
|
|
112
|
+
calculationField: {
|
|
113
|
+
field: fieldId,
|
|
114
|
+
aggregation
|
|
115
|
+
},
|
|
116
|
+
filterCriteria
|
|
117
|
+
})
|
|
118
|
+
});
|
|
119
|
+
if (!res.ok) throw new Error(`Softr datasource metric failed for "${datasourceId}": ${res.status}`);
|
|
120
|
+
const payload = await res.json();
|
|
121
|
+
const executionTimestamp = /* @__PURE__ */ new Date(payload.executionTimestamp * 1e3);
|
|
122
|
+
let value;
|
|
123
|
+
if (aggregation === "COUNT") value = payload.data.find((item) => COUNT_CNT in item)?.[COUNT_CNT] ?? null;
|
|
124
|
+
else {
|
|
125
|
+
if (!fieldId) throw new Error(`field is required for aggregation "${aggregation}"`);
|
|
126
|
+
value = payload.data.find((item) => fieldId in item)?.[fieldId] ?? null;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
value,
|
|
130
|
+
executionTimestamp
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function getChartData(datasourceId, options) {
|
|
134
|
+
const { select, where, groupBy, metrics, orderBy } = options;
|
|
135
|
+
const filterCriteria = where ? dealiasFilterCriteria(buildFilterCriteria(where), select ?? {}) : {};
|
|
136
|
+
const sortingOption = orderBy ? dealiasOrderBy(buildOrderBy(orderBy), select ?? {})[0] : void 0;
|
|
137
|
+
const groupByFields = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((field) => ({ field: select?.[field] ?? field }));
|
|
138
|
+
const yAxisFields = Object.values(metrics).map(({ field, aggregation }) => ({
|
|
139
|
+
field: field ? select?.[field] ?? field : void 0,
|
|
140
|
+
aggregation
|
|
141
|
+
}));
|
|
142
|
+
const res = await fetch(`${BASE_URL()}/api/datasources/${datasourceId}/chartData`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: {
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
"X-User-Timezone": options.timeZone ?? readUserTimezone() ?? "UTC",
|
|
147
|
+
"Authorization": `Bearer ${readSessionToken()}`
|
|
148
|
+
},
|
|
149
|
+
body: JSON.stringify({
|
|
150
|
+
options: { userLocale: "en-US" },
|
|
151
|
+
filterCriteria,
|
|
152
|
+
sortingOption,
|
|
153
|
+
xAxisFields: groupByFields,
|
|
154
|
+
yAxisFields
|
|
155
|
+
})
|
|
156
|
+
});
|
|
157
|
+
if (!res.ok) throw new Error(`Softr datasource chartData failed for "${datasourceId}": ${res.status}`);
|
|
158
|
+
const payload = await res.json();
|
|
159
|
+
const executionTimestamp = /* @__PURE__ */ new Date(payload.executionTimestamp * 1e3);
|
|
160
|
+
const aliases = {};
|
|
161
|
+
const multipleMetrics = Object.keys(metrics).length > 1;
|
|
162
|
+
for (const [alias, { field, aggregation }] of Object.entries(metrics)) if (aggregation === "COUNT") aliases[COUNT_CNT] = alias;
|
|
163
|
+
else {
|
|
164
|
+
const fieldId = field ? select?.[field] ?? field : field;
|
|
165
|
+
if (fieldId) {
|
|
166
|
+
const suffix = multipleMetrics ? `_${aggregation.toLowerCase()}` : "";
|
|
167
|
+
aliases[fieldId + suffix] = alias;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const field of Array.isArray(groupBy) ? groupBy : [groupBy]) aliases[select?.[field] ?? field] = field;
|
|
171
|
+
return {
|
|
172
|
+
data: payload.data.map((item) => {
|
|
173
|
+
const row = {};
|
|
174
|
+
for (const [rawKey, alias] of Object.entries(aliases)) if (rawKey in item) row[alias] = item[rawKey];
|
|
175
|
+
return row;
|
|
176
|
+
}),
|
|
177
|
+
executionTimestamp
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function aliasFields(fields, aliases) {
|
|
181
|
+
const result = {};
|
|
182
|
+
for (const [alias, fieldId] of Object.entries(aliases)) result[alias] = fields[fieldId];
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
const ds = {
|
|
186
|
+
list: listRecords,
|
|
187
|
+
get: getRecord,
|
|
188
|
+
create: createRecord,
|
|
189
|
+
update: updateRecord,
|
|
190
|
+
delete: deleteRecord,
|
|
191
|
+
metric: getMetric,
|
|
192
|
+
chartData: getChartData
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
//#endregion
|
|
196
|
+
export { ds };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/core/integration.d.ts
|
|
2
|
+
declare function integrationFetch(integrationId: string, input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
3
|
+
declare const integration: {
|
|
4
|
+
fetch: typeof integrationFetch;
|
|
5
|
+
};
|
|
6
|
+
//#endregion
|
|
7
|
+
export { integration };
|