railcode 0.1.20 → 0.1.21
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/README.md +1 -1
- package/dist/db.js +1 -1
- package/dist/index.js +209 -10
- package/package.json +1 -1
- package/static/sdk.js +135 -27
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ railcode agent <list|show|create|update|delete|run|schedule> ...
|
|
|
58
58
|
`railcode db list` (aliases `ls`, `connections`) prints each connector's
|
|
59
59
|
`name` + `engine` (`--json` for the raw array). `railcode db query "<sql>"`
|
|
60
60
|
(alias `sql`) runs SQL against `--connection` (default `default`) and prints a
|
|
61
|
-
table + row count; `--engine <postgres|bigquery|
|
|
61
|
+
table + row count; `--engine <postgres|bigquery|turso>` is inferred from the connector
|
|
62
62
|
list when omitted, `--params '<json-array>'` binds `$1, $2, …` placeholders
|
|
63
63
|
(SQL is never interpolated), `--file <path>` reads SQL from a file, and
|
|
64
64
|
`--json` prints the raw `{ columns, rows, rowcount, truncated }` envelope.
|
package/dist/db.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the command layer in index.ts does the HTTP, file reads, and printing.
|
|
4
4
|
export class DbError extends Error {
|
|
5
5
|
}
|
|
6
|
-
export const DB_ENGINES = ["postgres", "bigquery", "
|
|
6
|
+
export const DB_ENGINES = ["postgres", "bigquery", "turso"];
|
|
7
7
|
// `--params '<json-array>'` → the positional $1,$2,… values. Empty when omitted.
|
|
8
8
|
export function parseSqlParams(raw) {
|
|
9
9
|
if (raw === undefined)
|
package/dist/index.js
CHANGED
|
@@ -826,7 +826,7 @@ export default defineConfig({
|
|
|
826
826
|
}
|
|
827
827
|
function reactRailcodeTypes() {
|
|
828
828
|
return `type Me = {
|
|
829
|
-
user: { uuid: string; name: string; email: string };
|
|
829
|
+
user: { uuid: string; name: string; email: string; is_admin: boolean; roles: RoleRef[] };
|
|
830
830
|
app: { uuid: string; slug: string; name: string };
|
|
831
831
|
org: { uuid: string; slug: string; name: string };
|
|
832
832
|
};
|
|
@@ -835,9 +835,12 @@ type AppUser = {
|
|
|
835
835
|
uuid: string;
|
|
836
836
|
name: string;
|
|
837
837
|
email: string;
|
|
838
|
-
|
|
838
|
+
is_admin: boolean;
|
|
839
839
|
};
|
|
840
840
|
|
|
841
|
+
type RoleRef = { uuid: string; name: string };
|
|
842
|
+
type OrgRole = RoleRef & { description: string; is_member: boolean };
|
|
843
|
+
|
|
841
844
|
type KvRecord<T = unknown> = {
|
|
842
845
|
key: string;
|
|
843
846
|
value: T;
|
|
@@ -881,7 +884,7 @@ type SqlRows = Array<Record<string, unknown>> & {
|
|
|
881
884
|
};
|
|
882
885
|
|
|
883
886
|
type DataConnectorInfo = {
|
|
884
|
-
engine: "postgres" | "bigquery" | "
|
|
887
|
+
engine: "postgres" | "bigquery" | "turso" | string;
|
|
885
888
|
name: string;
|
|
886
889
|
};
|
|
887
890
|
|
|
@@ -960,8 +963,25 @@ type LlmModelInfo = { model: string; default: boolean };
|
|
|
960
963
|
|
|
961
964
|
type LlmProviderInfo = { provider: string; default: boolean; models: LlmModelInfo[] };
|
|
962
965
|
|
|
966
|
+
type EmailSendOptions = {
|
|
967
|
+
to: string | string[];
|
|
968
|
+
subject: string;
|
|
969
|
+
html?: string;
|
|
970
|
+
text?: string;
|
|
971
|
+
cc?: string | string[];
|
|
972
|
+
bcc?: string | string[];
|
|
973
|
+
replyTo?: string;
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
type EmailSendResult = {
|
|
977
|
+
id: string;
|
|
978
|
+
status: string;
|
|
979
|
+
requestId: string;
|
|
980
|
+
};
|
|
981
|
+
|
|
963
982
|
declare const me: () => Promise<Me>;
|
|
964
983
|
declare const appUsers: () => Promise<AppUser[]>;
|
|
984
|
+
declare const roles: () => Promise<OrgRole[]>;
|
|
965
985
|
declare const designSystem: () => Promise<string>;
|
|
966
986
|
declare const db: { collection<T = unknown>(name: string): Collection<T> };
|
|
967
987
|
declare const files: {
|
|
@@ -973,7 +993,6 @@ declare const files: {
|
|
|
973
993
|
declare const data: DatabaseNamespace;
|
|
974
994
|
declare const postgres: DatabaseNamespace;
|
|
975
995
|
declare const bigquery: DatabaseNamespace;
|
|
976
|
-
declare const snowflake: DatabaseNamespace;
|
|
977
996
|
declare const dataConnectors: () => Promise<DataConnectorInfo[]>;
|
|
978
997
|
declare const savedQueries: () => Promise<SavedQueryInfo[]>;
|
|
979
998
|
declare const query: (name: string, params?: Record<string, unknown>) => Promise<SqlRows>;
|
|
@@ -986,6 +1005,7 @@ declare const llm: {
|
|
|
986
1005
|
stream(input: string | LlmMessage[], opts?: LlmOptions): AsyncIterable<LlmStreamEvent>;
|
|
987
1006
|
};
|
|
988
1007
|
declare const llmProviders: () => Promise<LlmProviderInfo[]>;
|
|
1008
|
+
declare const email: { send(opts: EmailSendOptions): Promise<EmailSendResult> };
|
|
989
1009
|
`;
|
|
990
1010
|
}
|
|
991
1011
|
function reactViteEnvTypes() {
|
|
@@ -1060,6 +1080,14 @@ function message(error: unknown): string {
|
|
|
1060
1080
|
return error instanceof Error ? error.message : String(error);
|
|
1061
1081
|
}
|
|
1062
1082
|
|
|
1083
|
+
function httpStatus(error: unknown): number | null {
|
|
1084
|
+
if (typeof error === "object" && error !== null && "status" in error) {
|
|
1085
|
+
const status = (error as { status?: unknown }).status;
|
|
1086
|
+
return typeof status === "number" ? status : null;
|
|
1087
|
+
}
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1063
1091
|
function truncate(value: string, max: number): string {
|
|
1064
1092
|
return value.length > max ? value.slice(0, max - 1) + "…" : value;
|
|
1065
1093
|
}
|
|
@@ -1361,7 +1389,7 @@ function Hero() {
|
|
|
1361
1389
|
</div>
|
|
1362
1390
|
) : null}
|
|
1363
1391
|
{teammates.slice(0, 4).map((user) => (
|
|
1364
|
-
<div className="avatar" key={user.uuid} title={user.name + (user.
|
|
1392
|
+
<div className="avatar" key={user.uuid} title={user.name + (user.is_admin ? " · admin" : "")}>
|
|
1365
1393
|
{initials(user.name)}
|
|
1366
1394
|
</div>
|
|
1367
1395
|
))}
|
|
@@ -1867,9 +1895,124 @@ function ConnectorsCard() {
|
|
|
1867
1895
|
);
|
|
1868
1896
|
}
|
|
1869
1897
|
|
|
1898
|
+
function emailFailureMessage(error: unknown): string {
|
|
1899
|
+
const status = httpStatus(error);
|
|
1900
|
+
if (status === 403) return "Email is not granted for this app or user yet.";
|
|
1901
|
+
if (status === 429) return "The daily email recipient limit has been reached.";
|
|
1902
|
+
if (status === 503) return "Email is not configured for this Railcode instance.";
|
|
1903
|
+
return message(error);
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function EmailCard() {
|
|
1907
|
+
const { identity } = useAppStore();
|
|
1908
|
+
const [to, setTo] = useState("");
|
|
1909
|
+
const [subject, setSubject] = useState("Hello from " + APP_SLUG);
|
|
1910
|
+
const [body, setBody] = useState(
|
|
1911
|
+
"This message was sent by the Railcode starter app through the platform email gateway.",
|
|
1912
|
+
);
|
|
1913
|
+
const [busy, setBusy] = useState(false);
|
|
1914
|
+
const [status, setStatus] = useState<{ kind: "sent" | "error"; text: string } | null>(null);
|
|
1915
|
+
|
|
1916
|
+
useEffect(() => {
|
|
1917
|
+
if (!to && identity?.user.email) setTo(identity.user.email);
|
|
1918
|
+
}, [identity, to]);
|
|
1919
|
+
|
|
1920
|
+
async function sendMessage(event: FormEvent<HTMLFormElement>) {
|
|
1921
|
+
event.preventDefault();
|
|
1922
|
+
const recipient = to.trim();
|
|
1923
|
+
const trimmedSubject = subject.trim() || "Hello from " + APP_SLUG;
|
|
1924
|
+
const text = body.trim() || "Sent from a Railcode starter app.";
|
|
1925
|
+
if (!recipient) {
|
|
1926
|
+
setStatus({ kind: "error", text: "Add a recipient email address first." });
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
setBusy(true);
|
|
1931
|
+
setStatus(null);
|
|
1932
|
+
try {
|
|
1933
|
+
const result = await email.send({ to: recipient, subject: trimmedSubject, text });
|
|
1934
|
+
setStatus({
|
|
1935
|
+
kind: "sent",
|
|
1936
|
+
text: "Sent with status " + result.status + " · " + result.requestId,
|
|
1937
|
+
});
|
|
1938
|
+
} catch (error) {
|
|
1939
|
+
setStatus({ kind: "error", text: emailFailureMessage(error) });
|
|
1940
|
+
} finally {
|
|
1941
|
+
setBusy(false);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
return (
|
|
1946
|
+
<article className="card">
|
|
1947
|
+
<div className="card-head">
|
|
1948
|
+
<div>
|
|
1949
|
+
<div className="card-title-row">
|
|
1950
|
+
<Icon name="mail" />
|
|
1951
|
+
<span className="card-title">
|
|
1952
|
+
<Reveal code="await email.send(message)">Email</Reveal>
|
|
1953
|
+
</span>
|
|
1954
|
+
</div>
|
|
1955
|
+
</div>
|
|
1956
|
+
</div>
|
|
1957
|
+
|
|
1958
|
+
<form className="email-form" onSubmit={sendMessage}>
|
|
1959
|
+
<label className="control-field">
|
|
1960
|
+
<span>Recipient</span>
|
|
1961
|
+
<input
|
|
1962
|
+
className="field"
|
|
1963
|
+
type="email"
|
|
1964
|
+
autoComplete="email"
|
|
1965
|
+
value={to}
|
|
1966
|
+
onChange={(event) => setTo(event.target.value)}
|
|
1967
|
+
/>
|
|
1968
|
+
</label>
|
|
1969
|
+
<label className="control-field">
|
|
1970
|
+
<span>Subject</span>
|
|
1971
|
+
<input
|
|
1972
|
+
className="field"
|
|
1973
|
+
type="text"
|
|
1974
|
+
value={subject}
|
|
1975
|
+
onChange={(event) => setSubject(event.target.value)}
|
|
1976
|
+
/>
|
|
1977
|
+
</label>
|
|
1978
|
+
<label className="json-editor">
|
|
1979
|
+
<span>Message</span>
|
|
1980
|
+
<textarea
|
|
1981
|
+
className="field"
|
|
1982
|
+
rows={4}
|
|
1983
|
+
value={body}
|
|
1984
|
+
onChange={(event) => setBody(event.target.value)}
|
|
1985
|
+
/>
|
|
1986
|
+
</label>
|
|
1987
|
+
<ActionTip code={\`await email.send({
|
|
1988
|
+
to,
|
|
1989
|
+
subject,
|
|
1990
|
+
text
|
|
1991
|
+
})\`} className="upload-tip">
|
|
1992
|
+
<button className="btn btn-primary" type="submit" disabled={busy}>
|
|
1993
|
+
{busy ? <span className="spinner" /> : <Icon name="mail" />}
|
|
1994
|
+
{busy ? "Sending..." : "Send test email"}
|
|
1995
|
+
</button>
|
|
1996
|
+
</ActionTip>
|
|
1997
|
+
</form>
|
|
1998
|
+
|
|
1999
|
+
<div className="email-state">
|
|
2000
|
+
<div className="result-status">
|
|
2001
|
+
<span>Delivery</span>
|
|
2002
|
+
<span>{status?.kind === "sent" ? "Sent" : status?.kind === "error" ? "Needs setup" : "Ready"}</span>
|
|
2003
|
+
</div>
|
|
2004
|
+
<p className={status?.kind === "error" ? "email-note is-error" : "email-note"}>
|
|
2005
|
+
{status?.text ||
|
|
2006
|
+
"Email uses the platform sender and requires an email grant or app manifest approval."}
|
|
2007
|
+
</p>
|
|
2008
|
+
</div>
|
|
2009
|
+
</article>
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
1870
2013
|
const AI_SAMPLE_PROMPT = "Summarize what this Railcode template demonstrates in one sentence.";
|
|
1871
2014
|
const AI_SAMPLE_RESPONSE =
|
|
1872
|
-
"This template shows a live Railcode app reading identity, storing notes in KV, uploading files, querying Postgres, invoking a saved query, calling Stripe through a service connector, and generating text — all without handling a single credential in the browser.";
|
|
2015
|
+
"This template shows a live Railcode app reading identity, storing notes in KV, uploading files, querying Postgres, invoking a saved query, calling Stripe through a service connector, sending email, and generating text — all without handling a single credential in the browser.";
|
|
1873
2016
|
|
|
1874
2017
|
function AiCard() {
|
|
1875
2018
|
const [prompt, setPrompt] = useState(AI_SAMPLE_PROMPT);
|
|
@@ -2010,6 +2153,10 @@ function IconSprite() {
|
|
|
2010
2153
|
<symbol id="i-chat" viewBox="0 0 24 24">
|
|
2011
2154
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
2012
2155
|
</symbol>
|
|
2156
|
+
<symbol id="i-mail" viewBox="0 0 24 24">
|
|
2157
|
+
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
2158
|
+
<path d="m3 7 9 6 9-6" />
|
|
2159
|
+
</symbol>
|
|
2013
2160
|
<symbol id="i-code" viewBox="0 0 24 24">
|
|
2014
2161
|
<polyline points="16 18 22 12 16 6" />
|
|
2015
2162
|
<polyline points="8 6 2 12 8 18" />
|
|
@@ -2076,11 +2223,18 @@ export function App() {
|
|
|
2076
2223
|
|
|
2077
2224
|
<GuideSection
|
|
2078
2225
|
title="They can connect to external services"
|
|
2079
|
-
body="Railcode apps can securely connect to your SaaS services so you can manage refunds,
|
|
2226
|
+
body="Railcode apps can securely connect to your SaaS services so you can manage refunds, sync tickets, enrich records, and anything else you need."
|
|
2080
2227
|
>
|
|
2081
2228
|
<ConnectorsCard />
|
|
2082
2229
|
</GuideSection>
|
|
2083
2230
|
|
|
2231
|
+
<GuideSection
|
|
2232
|
+
title="They can send email"
|
|
2233
|
+
body="Send transactional email through the Railcode gateway without putting provider credentials in browser code."
|
|
2234
|
+
>
|
|
2235
|
+
<EmailCard />
|
|
2236
|
+
</GuideSection>
|
|
2237
|
+
|
|
2084
2238
|
<GuideSection
|
|
2085
2239
|
title="They can use AI"
|
|
2086
2240
|
body="Build AI-native apps that use LLMs to enable powerful workflows."
|
|
@@ -3101,6 +3255,41 @@ th:first-child {
|
|
|
3101
3255
|
}
|
|
3102
3256
|
}
|
|
3103
3257
|
|
|
3258
|
+
/* ---------- email card ---------- */
|
|
3259
|
+
|
|
3260
|
+
.email-form {
|
|
3261
|
+
display: flex;
|
|
3262
|
+
flex-direction: column;
|
|
3263
|
+
gap: var(--space-3);
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
.email-form .field {
|
|
3267
|
+
width: 100%;
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
.email-state {
|
|
3271
|
+
margin-top: var(--space-4);
|
|
3272
|
+
padding: 12px 14px;
|
|
3273
|
+
border: 1px solid var(--line);
|
|
3274
|
+
border-radius: var(--radius-md);
|
|
3275
|
+
background: var(--bg-inset);
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
.email-state .result-status {
|
|
3279
|
+
margin-bottom: 6px;
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
.email-note {
|
|
3283
|
+
color: var(--ink-3);
|
|
3284
|
+
font-size: 12.5px;
|
|
3285
|
+
line-height: 1.55;
|
|
3286
|
+
overflow-wrap: anywhere;
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
.email-note.is-error {
|
|
3290
|
+
color: #b42318;
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3104
3293
|
/* ---------- reveal + action tooltips ---------- */
|
|
3105
3294
|
|
|
3106
3295
|
.reveal {
|
|
@@ -4102,7 +4291,7 @@ List options:
|
|
|
4102
4291
|
|
|
4103
4292
|
Query options:
|
|
4104
4293
|
--connection <name> Connector to query (default: default)
|
|
4105
|
-
--engine <postgres|
|
|
4294
|
+
--engine <postgres|bigquery|turso> Engine override (inferred from --connection otherwise)
|
|
4106
4295
|
--params '<json-array>' Values for $1, $2, … placeholders
|
|
4107
4296
|
--file <path> Read SQL from a file instead of the argument
|
|
4108
4297
|
--json Print the raw { columns, rows, rowcount, truncated } envelope
|
|
@@ -5453,6 +5642,10 @@ async function handleLocalApi(ctx, req, res, url) {
|
|
|
5453
5642
|
sendJson(res, 200, [appUserEnvelope()]);
|
|
5454
5643
|
return;
|
|
5455
5644
|
}
|
|
5645
|
+
if (path === "/roles") {
|
|
5646
|
+
sendJson(res, 200, []);
|
|
5647
|
+
return;
|
|
5648
|
+
}
|
|
5456
5649
|
if (path === "/config/design-system") {
|
|
5457
5650
|
await handleDevDesignSystem(ctx, res);
|
|
5458
5651
|
return;
|
|
@@ -5803,13 +5996,19 @@ async function handleDevDesignSystem(ctx, res) {
|
|
|
5803
5996
|
// ── envelopes + http helpers ──────────────────────────────────────────────────
|
|
5804
5997
|
function meEnvelope(ctx) {
|
|
5805
5998
|
return {
|
|
5806
|
-
user: {
|
|
5999
|
+
user: {
|
|
6000
|
+
uuid: DEV_USER_UUID,
|
|
6001
|
+
name: "Local Dev",
|
|
6002
|
+
email: "dev@localhost",
|
|
6003
|
+
is_admin: true,
|
|
6004
|
+
roles: [],
|
|
6005
|
+
},
|
|
5807
6006
|
app: { uuid: DEV_APP_UUID, slug: ctx.app, name: titleCase(ctx.app) },
|
|
5808
6007
|
org: { uuid: DEV_ORG_UUID, slug: "local", name: "Local" },
|
|
5809
6008
|
};
|
|
5810
6009
|
}
|
|
5811
6010
|
function appUserEnvelope() {
|
|
5812
|
-
return { uuid: DEV_USER_UUID, name: "Local Dev", email: "dev@localhost",
|
|
6011
|
+
return { uuid: DEV_USER_UUID, name: "Local Dev", email: "dev@localhost", is_admin: true };
|
|
5813
6012
|
}
|
|
5814
6013
|
function isLoopbackHost(host) {
|
|
5815
6014
|
if (!host)
|
package/package.json
CHANGED
package/static/sdk.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
padding:2px 6px; border-radius:4px; white-space:nowrap; align-self:start; margin-top:2px; }
|
|
36
36
|
.k-sql { background:#1e3a5f; color:#7cc4ff; } .k-db { background:#3a2752; color:#c79bff; }
|
|
37
37
|
.k-files { background:#13413c; color:#5fd6c4; } .k-llm { background:#4a2b16; color:#f6b26b; }
|
|
38
|
-
.k-me, .k-connections, .k-app-users { background:#2a2f38; color:#9aa4b2; }
|
|
38
|
+
.k-me, .k-connections, .k-app-users, .k-roles { background:#2a2f38; color:#9aa4b2; }
|
|
39
39
|
.body { min-width:0; }
|
|
40
40
|
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size:11.5px; color:#e6e8eb;
|
|
41
41
|
word-break:break-word; display:block; line-height:1.45; }
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
<button id="collapse">Hide \u203A</button>
|
|
55
55
|
</header>
|
|
56
56
|
<div class="log" id="log"></div>
|
|
57
|
-
<footer>Calls made via <code>postgres()</code>, <code>llm</code>, <code>db</code>, <code>files</code>, <code>me()</code>, <code>appUsers()</code></footer>
|
|
57
|
+
<footer>Calls made via <code>postgres()</code>, <code>llm</code>, <code>db</code>, <code>files</code>, <code>me()</code>, <code>appUsers()</code>, <code>roles()</code></footer>
|
|
58
58
|
</div>`;
|
|
59
59
|
function mount() {
|
|
60
60
|
(document.body || document.documentElement).appendChild(host);
|
|
@@ -265,7 +265,7 @@
|
|
|
265
265
|
var data = namespace();
|
|
266
266
|
var postgres = namespace("postgres");
|
|
267
267
|
var bigquery = namespace("bigquery");
|
|
268
|
-
var
|
|
268
|
+
var turso = namespace("turso");
|
|
269
269
|
var dataConnectors = () => track("connections", "dataConnectors()", () => call("GET", "/connections"));
|
|
270
270
|
|
|
271
271
|
// ../sdk/src/util.ts
|
|
@@ -277,9 +277,18 @@
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
// ../sdk/src/db.ts
|
|
280
|
+
function scopeParams(s) {
|
|
281
|
+
return { scope: s.scope, role: s.role };
|
|
282
|
+
}
|
|
283
|
+
function scopeLabel(s) {
|
|
284
|
+
if (s.scope === "user") return "db.user";
|
|
285
|
+
if (s.scope === "role") return `db.role(${q(s.role ?? "")})`;
|
|
286
|
+
return "db.shared";
|
|
287
|
+
}
|
|
280
288
|
var Query = class {
|
|
281
|
-
constructor(collection) {
|
|
289
|
+
constructor(collection, scope) {
|
|
282
290
|
this.collection = collection;
|
|
291
|
+
this.scope = scope;
|
|
283
292
|
this._where = [];
|
|
284
293
|
}
|
|
285
294
|
where(field, op, value) {
|
|
@@ -309,11 +318,12 @@
|
|
|
309
318
|
prefix: this._prefix,
|
|
310
319
|
updated_since: this._since,
|
|
311
320
|
updated_before: this._before,
|
|
321
|
+
...scopeParams(this.scope),
|
|
312
322
|
...extra
|
|
313
323
|
};
|
|
314
324
|
}
|
|
315
325
|
label(method) {
|
|
316
|
-
return
|
|
326
|
+
return `${scopeLabel(this.scope)}.collection(${q(this.collection)}).${method}`;
|
|
317
327
|
}
|
|
318
328
|
async fetchPage(page, size) {
|
|
319
329
|
const resp = await call("GET", `/kv/${encPath(this.collection)}`, {
|
|
@@ -340,16 +350,20 @@
|
|
|
340
350
|
}
|
|
341
351
|
};
|
|
342
352
|
var Collection = class {
|
|
343
|
-
constructor(name) {
|
|
353
|
+
constructor(name, scope) {
|
|
344
354
|
this.name = name;
|
|
355
|
+
this.scope = scope;
|
|
345
356
|
}
|
|
346
357
|
label(method) {
|
|
347
|
-
return
|
|
358
|
+
return `${scopeLabel(this.scope)}.collection(${q(this.name)}).${method}`;
|
|
348
359
|
}
|
|
349
360
|
/** The stored value for ``key``, or null if absent. */
|
|
350
361
|
get(key) {
|
|
351
362
|
return track("db", this.label(`get(${q(key)})`), async () => {
|
|
352
|
-
const record = await getOrNull(
|
|
363
|
+
const record = await getOrNull(
|
|
364
|
+
`/kv/${encPath(this.name)}/${encPath(key)}`,
|
|
365
|
+
scopeParams(this.scope)
|
|
366
|
+
);
|
|
353
367
|
return record ? record.value : null;
|
|
354
368
|
});
|
|
355
369
|
}
|
|
@@ -357,7 +371,8 @@
|
|
|
357
371
|
put(key, value) {
|
|
358
372
|
return track("db", this.label(`put(${q(key)}, ${shorten(JSON.stringify(value), 40)})`), async () => {
|
|
359
373
|
const record = await call("PUT", `/kv/${encPath(this.name)}/${encPath(key)}`, {
|
|
360
|
-
body: { value }
|
|
374
|
+
body: { value },
|
|
375
|
+
query: scopeParams(this.scope)
|
|
361
376
|
});
|
|
362
377
|
return record.value;
|
|
363
378
|
});
|
|
@@ -366,18 +381,22 @@
|
|
|
366
381
|
return track(
|
|
367
382
|
"db",
|
|
368
383
|
this.label(`delete(${q(key)})`),
|
|
369
|
-
() => call("DELETE", `/kv/${encPath(this.name)}/${encPath(key)}
|
|
384
|
+
() => call("DELETE", `/kv/${encPath(this.name)}/${encPath(key)}`, {
|
|
385
|
+
query: scopeParams(this.scope)
|
|
386
|
+
})
|
|
370
387
|
);
|
|
371
388
|
}
|
|
372
389
|
/** All records in the collection (first page). */
|
|
373
390
|
list() {
|
|
374
391
|
return track("db", this.label("list()"), async () => {
|
|
375
|
-
const resp = await call("GET", `/kv/${encPath(this.name)}
|
|
392
|
+
const resp = await call("GET", `/kv/${encPath(this.name)}`, {
|
|
393
|
+
query: scopeParams(this.scope)
|
|
394
|
+
});
|
|
376
395
|
return resp.items;
|
|
377
396
|
});
|
|
378
397
|
}
|
|
379
398
|
query() {
|
|
380
|
-
return new Query(this.name);
|
|
399
|
+
return new Query(this.name, this.scope);
|
|
381
400
|
}
|
|
382
401
|
where(field, op, value) {
|
|
383
402
|
return this.query().where(field, op, value);
|
|
@@ -386,8 +405,15 @@
|
|
|
386
405
|
return this.query().prefix(value);
|
|
387
406
|
}
|
|
388
407
|
};
|
|
408
|
+
function namespace2(scope) {
|
|
409
|
+
return { collection: (name) => new Collection(name, scope) };
|
|
410
|
+
}
|
|
411
|
+
var shared = namespace2({});
|
|
389
412
|
var db = {
|
|
390
|
-
|
|
413
|
+
shared,
|
|
414
|
+
user: namespace2({ scope: "user" }),
|
|
415
|
+
role: (roleUuid) => namespace2({ scope: "role", role: roleUuid }),
|
|
416
|
+
collection: shared.collection
|
|
391
417
|
};
|
|
392
418
|
|
|
393
419
|
// ../sdk/src/design-system.ts
|
|
@@ -423,18 +449,41 @@
|
|
|
423
449
|
var email = { send: send2 };
|
|
424
450
|
|
|
425
451
|
// ../sdk/src/files.ts
|
|
452
|
+
var URL_EXPIRY_MARGIN_MS = 15e3;
|
|
453
|
+
var urlCache = /* @__PURE__ */ new Map();
|
|
454
|
+
function scopeParams2(s) {
|
|
455
|
+
return { scope: s.scope, role: s.role };
|
|
456
|
+
}
|
|
457
|
+
function scopeKey(s) {
|
|
458
|
+
if (s.scope === "user") return "user";
|
|
459
|
+
if (s.scope === "role") return `role:${s.role ?? ""}`;
|
|
460
|
+
return "shared";
|
|
461
|
+
}
|
|
462
|
+
function scopeLabel2(s) {
|
|
463
|
+
if (s.scope === "user") return "files.user";
|
|
464
|
+
if (s.scope === "role") return `files.role(${q(s.role ?? "")})`;
|
|
465
|
+
return "files.shared";
|
|
466
|
+
}
|
|
467
|
+
function cacheKey(s, name) {
|
|
468
|
+
return `${scopeKey(s)} ${name}`;
|
|
469
|
+
}
|
|
426
470
|
function contentTypeOf(data2, explicit) {
|
|
427
471
|
if (explicit) return explicit;
|
|
428
472
|
if (data2 instanceof Blob && data2.type) return data2.type;
|
|
429
473
|
return "application/octet-stream";
|
|
430
474
|
}
|
|
431
|
-
function upload(name, data2, contentType) {
|
|
432
|
-
return track("files",
|
|
475
|
+
function upload(scope, name, data2, contentType) {
|
|
476
|
+
return track("files", `${scopeLabel2(scope)}.upload(${q(name)}, <blob>)`, async () => {
|
|
477
|
+
const result = await doUpload(scope, name, data2, contentType);
|
|
478
|
+
urlCache.delete(cacheKey(scope, name));
|
|
479
|
+
return result;
|
|
480
|
+
});
|
|
433
481
|
}
|
|
434
|
-
async function doUpload(name, data2, contentType) {
|
|
482
|
+
async function doUpload(scope, name, data2, contentType) {
|
|
435
483
|
const ct = contentTypeOf(data2, contentType);
|
|
436
484
|
const target2 = await call("POST", "/files", {
|
|
437
|
-
body: { name, content_type: ct }
|
|
485
|
+
body: { name, content_type: ct },
|
|
486
|
+
query: scopeParams2(scope)
|
|
438
487
|
});
|
|
439
488
|
const headers = { "Content-Type": ct, ...target2.headers };
|
|
440
489
|
if (target2.mode === "proxy") {
|
|
@@ -449,21 +498,79 @@
|
|
|
449
498
|
}
|
|
450
499
|
const resp = await fetch(target2.url, { method: target2.method, body: data2, headers });
|
|
451
500
|
if (!resp.ok) throw new Error(`upload failed: ${resp.status}`);
|
|
452
|
-
return call("POST", "/files/finalize", { body: { name } });
|
|
501
|
+
return call("POST", "/files/finalize", { body: { name }, query: scopeParams2(scope) });
|
|
453
502
|
}
|
|
454
|
-
function url(name) {
|
|
455
|
-
return buildUrl(`/files/${encPath(name)}
|
|
503
|
+
function url(scope, name) {
|
|
504
|
+
return buildUrl(`/files/${encPath(name)}`, scopeParams2(scope));
|
|
456
505
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
506
|
+
async function urls(scope, names) {
|
|
507
|
+
return track("files", `${scopeLabel2(scope)}.urls(<${names.length} names>)`, async () => {
|
|
508
|
+
const unique = [...new Set(names)];
|
|
509
|
+
const now = Date.now();
|
|
510
|
+
const items = /* @__PURE__ */ new Map();
|
|
511
|
+
const unresolved = [];
|
|
512
|
+
for (const name of unique) {
|
|
513
|
+
const cached = urlCache.get(cacheKey(scope, name));
|
|
514
|
+
if (cached && cached.expiresAt > now + URL_EXPIRY_MARGIN_MS) items.set(name, cached.item);
|
|
515
|
+
else {
|
|
516
|
+
urlCache.delete(cacheKey(scope, name));
|
|
517
|
+
unresolved.push(name);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
let missing = [];
|
|
521
|
+
if (unresolved.length) {
|
|
522
|
+
const resolved = await call("POST", "/files/urls", {
|
|
523
|
+
body: { names: unresolved },
|
|
524
|
+
query: scopeParams2(scope)
|
|
525
|
+
});
|
|
526
|
+
missing = resolved.missing;
|
|
527
|
+
for (const item of resolved.items) {
|
|
528
|
+
const normalized = { ...item, url: new URL(item.url, location.origin).toString() };
|
|
529
|
+
const expiresAt = item.expires_in === null ? Number.POSITIVE_INFINITY : now + item.expires_in * 1e3;
|
|
530
|
+
urlCache.set(cacheKey(scope, item.name), { item: normalized, expiresAt });
|
|
531
|
+
items.set(item.name, normalized);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return { items: unique.flatMap((name) => items.has(name) ? [items.get(name)] : []), missing };
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
function list(scope) {
|
|
538
|
+
return track("files", `${scopeLabel2(scope)}.list()`, async () => {
|
|
539
|
+
const resp = await call("GET", "/files", { query: scopeParams2(scope) });
|
|
540
|
+
return resp.items;
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
function remove(scope, name) {
|
|
544
|
+
return track("files", `${scopeLabel2(scope)}.delete(${q(name)})`, async () => {
|
|
545
|
+
await call("DELETE", `/files/${encPath(name)}`, { query: scopeParams2(scope) });
|
|
546
|
+
urlCache.delete(cacheKey(scope, name));
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
function fileNamespace(scope) {
|
|
550
|
+
return {
|
|
551
|
+
upload: (name, data2, contentType) => upload(scope, name, data2, contentType),
|
|
552
|
+
url: (name) => url(scope, name),
|
|
553
|
+
urls: (names) => urls(scope, names),
|
|
554
|
+
list: () => list(scope),
|
|
555
|
+
delete: (name) => remove(scope, name)
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
var shared2 = fileNamespace({});
|
|
559
|
+
var files = {
|
|
560
|
+
shared: shared2,
|
|
561
|
+
user: fileNamespace({ scope: "user" }),
|
|
562
|
+
role: (roleUuid) => fileNamespace({ scope: "role", role: roleUuid }),
|
|
563
|
+
upload: shared2.upload,
|
|
564
|
+
url: shared2.url,
|
|
565
|
+
urls: shared2.urls,
|
|
566
|
+
list: shared2.list,
|
|
567
|
+
delete: shared2.delete
|
|
568
|
+
};
|
|
463
569
|
|
|
464
570
|
// ../sdk/src/identity.ts
|
|
465
571
|
var me = () => track("me", "me()", () => call("GET", "/me"));
|
|
466
572
|
var appUsers = () => track("app-users", "appUsers()", () => call("GET", "/app-users"));
|
|
573
|
+
var roles = () => track("roles", "roles()", () => call("GET", "/roles"));
|
|
467
574
|
|
|
468
575
|
// ../sdk/src/llm.ts
|
|
469
576
|
function label2(method, input, opts = {}) {
|
|
@@ -589,6 +696,7 @@
|
|
|
589
696
|
target.files = files;
|
|
590
697
|
target.me = me;
|
|
591
698
|
target.appUsers = appUsers;
|
|
699
|
+
target.roles = roles;
|
|
592
700
|
target.designSystem = designSystem;
|
|
593
701
|
target.email = email;
|
|
594
702
|
target.llm = llm;
|
|
@@ -596,7 +704,7 @@
|
|
|
596
704
|
target.data = data;
|
|
597
705
|
target.postgres = postgres;
|
|
598
706
|
target.bigquery = bigquery;
|
|
599
|
-
target.
|
|
707
|
+
target.turso = turso;
|
|
600
708
|
target.dataConnectors = dataConnectors;
|
|
601
709
|
target.connector = connector;
|
|
602
710
|
target.serviceConnectors = serviceConnectors;
|