@sovaria/sdk 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +179 -2
- package/dist/client.d.ts +146 -0
- package/dist/client.js +583 -0
- package/dist/collections.d.ts +189 -0
- package/dist/collections.js +467 -0
- package/dist/complete-api.d.ts +47 -0
- package/dist/complete-api.js +64 -0
- package/dist/crashes.d.ts +79 -0
- package/dist/crashes.js +176 -0
- package/dist/crypto.d.ts +37 -0
- package/dist/crypto.js +189 -0
- package/dist/errors.d.ts +40 -0
- package/dist/errors.js +246 -0
- package/dist/firestore.d.ts +176 -0
- package/dist/firestore.js +337 -0
- package/dist/generated.d.ts +1227 -0
- package/dist/generated.js +1054 -0
- package/dist/index.d.ts +2046 -0
- package/dist/index.js +2287 -0
- package/dist/offline.d.ts +90 -0
- package/dist/offline.js +528 -0
- package/dist/types.d.ts +102 -0
- package/dist/types.js +1 -0
- package/package.json +74 -4
package/dist/client.js
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { SovariaError } from "./types.js";
|
|
2
|
+
import { newTraceId, traceparentFor } from "./errors.js";
|
|
3
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
4
|
+
const MAX_RETRIES = 4;
|
|
5
|
+
const RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);
|
|
6
|
+
const SAFE_RETRY_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
7
|
+
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
8
|
+
const REFRESH_MARGIN_MS = 60_000;
|
|
9
|
+
function sleep(ms) {
|
|
10
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
}
|
|
12
|
+
function getHeaderCaseInsensitive(headers, headerName) {
|
|
13
|
+
const wanted = headerName.toLowerCase();
|
|
14
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
15
|
+
if (key.toLowerCase() === wanted) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
function generateIdempotencyKey() {
|
|
22
|
+
const cryptoObj = globalThis.crypto;
|
|
23
|
+
if (typeof cryptoObj?.randomUUID === "function") {
|
|
24
|
+
return cryptoObj.randomUUID();
|
|
25
|
+
}
|
|
26
|
+
if (typeof cryptoObj?.getRandomValues === "function") {
|
|
27
|
+
const bytes = new Uint8Array(16);
|
|
28
|
+
cryptoObj.getRandomValues(bytes);
|
|
29
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
30
|
+
}
|
|
31
|
+
throw SovariaError.config("No CSPRNG available for idempotency key generation", 500);
|
|
32
|
+
}
|
|
33
|
+
function existingIdempotencyKey(headers) {
|
|
34
|
+
const existing = getHeaderCaseInsensitive(headers, "Idempotency-Key")
|
|
35
|
+
?? getHeaderCaseInsensitive(headers, "X-Idempotency-Key");
|
|
36
|
+
return existing?.trim() || undefined;
|
|
37
|
+
}
|
|
38
|
+
function parseRetryAfterMs(retryAfterHeader) {
|
|
39
|
+
if (!retryAfterHeader) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const seconds = Number.parseFloat(retryAfterHeader);
|
|
43
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
44
|
+
return Math.floor(seconds * 1000);
|
|
45
|
+
}
|
|
46
|
+
const retryAtMs = Date.parse(retryAfterHeader);
|
|
47
|
+
if (Number.isNaN(retryAtMs)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return Math.max(0, retryAtMs - Date.now());
|
|
51
|
+
}
|
|
52
|
+
function computeRetryDelayMs(attempt, retryAfterMs = null) {
|
|
53
|
+
const jitterBackoffMs = Math.min(10_000, 100 * Math.pow(2, attempt) * Math.random());
|
|
54
|
+
if (retryAfterMs === null) {
|
|
55
|
+
return jitterBackoffMs;
|
|
56
|
+
}
|
|
57
|
+
return Math.max(jitterBackoffMs, Math.min(60_000, retryAfterMs));
|
|
58
|
+
}
|
|
59
|
+
// ── SovariaClient ────────────────────────────────────────────────────────────
|
|
60
|
+
/**
|
|
61
|
+
* Async TypeScript client for the Sovaria API Gateway.
|
|
62
|
+
*
|
|
63
|
+
* @deprecated Use {@link SovariaSDK} from `./index`. This legacy client is retained
|
|
64
|
+
* for in-repo tests only and is not a public package export.
|
|
65
|
+
*
|
|
66
|
+
* All requests flow through the gateway; engines are never called directly.
|
|
67
|
+
* Token refresh is automatic and transparent.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* const sov = new SovariaClient("https://api.sovaria.in");
|
|
72
|
+
* await sov.auth.login("user@example.com", "password"); // deprecated; posts to /console/login
|
|
73
|
+
* sov.setContext("org-123", "proj-456");
|
|
74
|
+
*
|
|
75
|
+
* const rows = await sov.data.sql.query("SELECT * FROM users LIMIT 10");
|
|
76
|
+
* await sov.auth.logout();
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export class SovariaClient {
|
|
80
|
+
baseUrl;
|
|
81
|
+
accessToken;
|
|
82
|
+
refreshToken;
|
|
83
|
+
expiresAt = 0;
|
|
84
|
+
orgId = null;
|
|
85
|
+
projectId = null;
|
|
86
|
+
envId = null;
|
|
87
|
+
refreshPromise = null;
|
|
88
|
+
onTokenRefresh;
|
|
89
|
+
auth;
|
|
90
|
+
org;
|
|
91
|
+
data;
|
|
92
|
+
storage;
|
|
93
|
+
functions;
|
|
94
|
+
realtime;
|
|
95
|
+
chrono;
|
|
96
|
+
constructor(baseUrl, options) {
|
|
97
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
98
|
+
this.accessToken = options?.accessToken ?? null;
|
|
99
|
+
this.refreshToken = options?.refreshToken ?? null;
|
|
100
|
+
this.onTokenRefresh = options?.onTokenRefresh;
|
|
101
|
+
this.auth = new AuthModule(this);
|
|
102
|
+
this.org = new OrgModule(this);
|
|
103
|
+
this.data = new DataModule(this);
|
|
104
|
+
this.storage = new StorageModule(this);
|
|
105
|
+
this.functions = new FunctionsModule(this);
|
|
106
|
+
this.realtime = new RealtimeModule(this);
|
|
107
|
+
this.chrono = new ChronoModule(this);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Bind org/project context for multi-tenant headers.
|
|
111
|
+
* These are injected as `X-Sovaria-Org-ID` and `X-Sovaria-Project-ID`
|
|
112
|
+
* on every subsequent request.
|
|
113
|
+
*/
|
|
114
|
+
setContext(orgId, projectId, envId) {
|
|
115
|
+
this.orgId = orgId;
|
|
116
|
+
this.projectId = projectId;
|
|
117
|
+
this.envId = envId?.trim() || null;
|
|
118
|
+
}
|
|
119
|
+
clearContext() {
|
|
120
|
+
this.orgId = null;
|
|
121
|
+
this.projectId = null;
|
|
122
|
+
this.envId = null;
|
|
123
|
+
}
|
|
124
|
+
// ── Token management (used internally by AuthModule) ───────────────────────
|
|
125
|
+
/** @internal */
|
|
126
|
+
_setTokens(tokens) {
|
|
127
|
+
this.accessToken = tokens.access_token;
|
|
128
|
+
this.refreshToken = tokens.refresh_token;
|
|
129
|
+
this.expiresAt = Date.now() + tokens.expires_in * 1000;
|
|
130
|
+
this.onTokenRefresh?.(tokens);
|
|
131
|
+
}
|
|
132
|
+
/** @internal */
|
|
133
|
+
_clearTokens() {
|
|
134
|
+
this.accessToken = null;
|
|
135
|
+
this.refreshToken = null;
|
|
136
|
+
this.expiresAt = 0;
|
|
137
|
+
}
|
|
138
|
+
/** @internal */
|
|
139
|
+
get _refreshToken() {
|
|
140
|
+
return this.refreshToken;
|
|
141
|
+
}
|
|
142
|
+
/** @internal */
|
|
143
|
+
get _isAuthenticated() {
|
|
144
|
+
return this.accessToken !== null;
|
|
145
|
+
}
|
|
146
|
+
// ── Core request method ────────────────────────────────────────────────────
|
|
147
|
+
/**
|
|
148
|
+
* Execute an HTTP request with automatic auth injection, token refresh, and
|
|
149
|
+
* exponential back-off with jitter on transient failures.
|
|
150
|
+
*
|
|
151
|
+
* @internal
|
|
152
|
+
*/
|
|
153
|
+
async _request(method, path, options) {
|
|
154
|
+
const normalizedMethod = method.toUpperCase();
|
|
155
|
+
const requiresAuth = options?.requiresAuth ?? true;
|
|
156
|
+
const baseHeaders = { ...(options?.headers ?? {}) };
|
|
157
|
+
// One Idempotency-Key for every attempt of a write (the caller's, or a fresh one): if the first
|
|
158
|
+
// try was applied but its response was lost, the retry gets the stored answer instead of
|
|
159
|
+
// running the change twice.
|
|
160
|
+
const idempotencyKey = existingIdempotencyKey(baseHeaders) ??
|
|
161
|
+
(MUTATING_METHODS.has(normalizedMethod) ? generateIdempotencyKey() : undefined);
|
|
162
|
+
if (idempotencyKey) {
|
|
163
|
+
for (const name of Object.keys(baseHeaders)) {
|
|
164
|
+
if (name.toLowerCase() === "idempotency-key")
|
|
165
|
+
delete baseHeaders[name];
|
|
166
|
+
}
|
|
167
|
+
baseHeaders["Idempotency-Key"] = idempotencyKey;
|
|
168
|
+
}
|
|
169
|
+
const canRetry = SAFE_RETRY_METHODS.has(normalizedMethod) || MUTATING_METHODS.has(normalizedMethod);
|
|
170
|
+
const traceId = getHeaderCaseInsensitive(baseHeaders, "traceparent")?.split("-")[1] ?? newTraceId();
|
|
171
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
172
|
+
if (requiresAuth) {
|
|
173
|
+
await this.ensureTokenFresh();
|
|
174
|
+
}
|
|
175
|
+
const headers = {
|
|
176
|
+
Accept: "application/json",
|
|
177
|
+
...(getHeaderCaseInsensitive(baseHeaders, "traceparent") ? {} : { traceparent: traceparentFor(traceId) }),
|
|
178
|
+
...baseHeaders,
|
|
179
|
+
};
|
|
180
|
+
const contentType = headers["Content-Type"] ?? headers["content-type"];
|
|
181
|
+
const rawBody = Boolean(contentType && !contentType.includes("application/json")) ||
|
|
182
|
+
(typeof FormData !== "undefined" && options?.body instanceof FormData);
|
|
183
|
+
if (!rawBody && !contentType) {
|
|
184
|
+
headers["Content-Type"] = "application/json";
|
|
185
|
+
}
|
|
186
|
+
if (typeof FormData !== "undefined" && options?.body instanceof FormData) {
|
|
187
|
+
delete headers["Content-Type"];
|
|
188
|
+
delete headers["content-type"];
|
|
189
|
+
}
|
|
190
|
+
if (requiresAuth && this.accessToken) {
|
|
191
|
+
headers["Authorization"] = `Bearer ${this.accessToken}`;
|
|
192
|
+
}
|
|
193
|
+
if (this.orgId) {
|
|
194
|
+
headers["X-Sovaria-Org-ID"] = this.orgId;
|
|
195
|
+
}
|
|
196
|
+
if (this.projectId) {
|
|
197
|
+
headers["X-Sovaria-Project-ID"] = this.projectId;
|
|
198
|
+
}
|
|
199
|
+
if (this.envId) {
|
|
200
|
+
headers["X-Sovaria-Env-ID"] = this.envId;
|
|
201
|
+
}
|
|
202
|
+
let url = `${this.baseUrl}${path}`;
|
|
203
|
+
if (options?.params) {
|
|
204
|
+
const qs = new URLSearchParams(options.params).toString();
|
|
205
|
+
if (qs)
|
|
206
|
+
url += `?${qs}`;
|
|
207
|
+
}
|
|
208
|
+
const fetchInit = { method: normalizedMethod, headers };
|
|
209
|
+
if (options?.body !== undefined) {
|
|
210
|
+
fetchInit.body = rawBody
|
|
211
|
+
? options.body
|
|
212
|
+
: JSON.stringify(options.body);
|
|
213
|
+
}
|
|
214
|
+
let response;
|
|
215
|
+
try {
|
|
216
|
+
response = await fetch(url, fetchInit);
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
if (!canRetry || attempt >= MAX_RETRIES - 1) {
|
|
220
|
+
throw new SovariaError(0, `Network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
221
|
+
}
|
|
222
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
223
|
+
await sleep(computeRetryDelayMs(attempt));
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (!response) {
|
|
228
|
+
throw new SovariaError(0, "Network error: request failed before receiving a response");
|
|
229
|
+
}
|
|
230
|
+
if (response.ok) {
|
|
231
|
+
const text = await response.text();
|
|
232
|
+
if (!text)
|
|
233
|
+
return {};
|
|
234
|
+
try {
|
|
235
|
+
return JSON.parse(text);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return {};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Auto-refresh on 401 and retry once
|
|
242
|
+
if (response.status === 401 && requiresAuth && this.refreshToken && attempt === 0) {
|
|
243
|
+
try {
|
|
244
|
+
await this.doRefresh();
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Refresh failed — fall through to error
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
let detail;
|
|
252
|
+
try {
|
|
253
|
+
detail = await response.json();
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
detail = response.statusText;
|
|
257
|
+
}
|
|
258
|
+
// "The same Idempotency-Key is still being processed": wait, then the retry gets the first
|
|
259
|
+
// try's stored answer instead of running the change twice.
|
|
260
|
+
const inProgress = response.status === 409 && detail?.code === "IDEMPOTENCY_IN_PROGRESS";
|
|
261
|
+
if (canRetry && (RETRYABLE_STATUSES.has(response.status) || inProgress) && attempt < MAX_RETRIES - 1) {
|
|
262
|
+
const retryAfterMs = response.status === 429 || inProgress
|
|
263
|
+
? parseRetryAfterMs(response.headers.get("Retry-After"))
|
|
264
|
+
: null;
|
|
265
|
+
await sleep(computeRetryDelayMs(attempt, retryAfterMs));
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
throw new SovariaError(response.status, detail).withTraceId(response.headers.get("X-Request-ID") ?? traceId);
|
|
269
|
+
}
|
|
270
|
+
throw new SovariaError(0, "Max retries exceeded");
|
|
271
|
+
}
|
|
272
|
+
// ── Token refresh internals ────────────────────────────────────────────────
|
|
273
|
+
async ensureTokenFresh() {
|
|
274
|
+
if (!this.accessToken)
|
|
275
|
+
return;
|
|
276
|
+
if (Date.now() < this.expiresAt - REFRESH_MARGIN_MS)
|
|
277
|
+
return;
|
|
278
|
+
if (!this.refreshToken)
|
|
279
|
+
return;
|
|
280
|
+
// Deduplicate concurrent refresh calls
|
|
281
|
+
if (!this.refreshPromise) {
|
|
282
|
+
this.refreshPromise = this.doRefresh().finally(() => {
|
|
283
|
+
this.refreshPromise = null;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
await this.refreshPromise;
|
|
287
|
+
}
|
|
288
|
+
async doRefresh() {
|
|
289
|
+
if (!this.refreshToken) {
|
|
290
|
+
throw new SovariaError(401, "No refresh token available — please log in again");
|
|
291
|
+
}
|
|
292
|
+
const response = await fetch(`${this.baseUrl}/api/v1/auth/refresh`, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
headers: { "Content-Type": "application/json" },
|
|
295
|
+
body: JSON.stringify({ refresh_token: this.refreshToken }),
|
|
296
|
+
});
|
|
297
|
+
if (!response.ok) {
|
|
298
|
+
this._clearTokens();
|
|
299
|
+
let detail;
|
|
300
|
+
try {
|
|
301
|
+
const errBody = await response.json();
|
|
302
|
+
detail = errBody.detail ?? "Refresh failed";
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
detail = "Refresh failed";
|
|
306
|
+
}
|
|
307
|
+
throw new SovariaError(response.status, detail);
|
|
308
|
+
}
|
|
309
|
+
const tokens = await response.json();
|
|
310
|
+
this._setTokens(tokens);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Build a WebSocket URL from the base URL, converting http(s) to ws(s).
|
|
314
|
+
* @internal
|
|
315
|
+
*/
|
|
316
|
+
_wsUrl(path) {
|
|
317
|
+
const base = this.baseUrl
|
|
318
|
+
.replace(/^https:\/\//, "wss://")
|
|
319
|
+
.replace(/^http:\/\//, "ws://");
|
|
320
|
+
return `${base}${path}`;
|
|
321
|
+
}
|
|
322
|
+
/** @internal */
|
|
323
|
+
async _issueRealtimeWsTicket(channel) {
|
|
324
|
+
if (!this.orgId || !this.projectId) {
|
|
325
|
+
throw SovariaError.config("Realtime websocket tickets require org/project context; call setContext() first");
|
|
326
|
+
}
|
|
327
|
+
const safeChannel = channel.trim();
|
|
328
|
+
if (!safeChannel) {
|
|
329
|
+
throw SovariaError.config("channel is required for realtime websocket tickets");
|
|
330
|
+
}
|
|
331
|
+
const response = await this._request("POST", "/api/v1/auth/ws-ticket", {
|
|
332
|
+
body: {
|
|
333
|
+
channel: safeChannel,
|
|
334
|
+
org_id: this.orgId,
|
|
335
|
+
project_id: this.projectId,
|
|
336
|
+
...(this.envId ? { env_id: this.envId } : {}),
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
const ticket = response.ticket?.trim();
|
|
340
|
+
if (!ticket) {
|
|
341
|
+
throw new SovariaError(502, "Gateway did not return a websocket ticket");
|
|
342
|
+
}
|
|
343
|
+
return ticket;
|
|
344
|
+
}
|
|
345
|
+
/** @internal */
|
|
346
|
+
get _accessToken() {
|
|
347
|
+
return this.accessToken;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// ── Auth Module ──────────────────────────────────────────────────────────────
|
|
351
|
+
class AuthModule {
|
|
352
|
+
client;
|
|
353
|
+
constructor(client) {
|
|
354
|
+
this.client = client;
|
|
355
|
+
}
|
|
356
|
+
async login(email, password) {
|
|
357
|
+
console.warn("SovariaClient.auth.login() is deprecated; use SovariaSDK.auth.consoleLogin() or auth.projectLogin(). This legacy client posts to /api/v1/auth/console/login (not /api/v1/auth/login).");
|
|
358
|
+
const tokens = await this.client._request("POST", "/api/v1/auth/console/login", { body: { email, password }, requiresAuth: false });
|
|
359
|
+
this.client._setTokens(tokens);
|
|
360
|
+
return tokens;
|
|
361
|
+
}
|
|
362
|
+
async register(email, password, name) {
|
|
363
|
+
const tokens = await this.client._request("POST", "/api/v1/auth/register", { body: { email, password, name }, requiresAuth: false });
|
|
364
|
+
this.client._setTokens(tokens);
|
|
365
|
+
return tokens;
|
|
366
|
+
}
|
|
367
|
+
async logout() {
|
|
368
|
+
if (this.client._isAuthenticated && this.client._refreshToken) {
|
|
369
|
+
try {
|
|
370
|
+
await this.client._request("POST", "/api/v1/auth/logout", {
|
|
371
|
+
body: { refresh_token: this.client._refreshToken },
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// Best-effort: local tokens are cleared regardless
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
this.client._clearTokens();
|
|
379
|
+
}
|
|
380
|
+
async me() {
|
|
381
|
+
return this.client._request("GET", "/api/v1/auth/me");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// ── Org Module ───────────────────────────────────────────────────────────────
|
|
385
|
+
class OrgModule {
|
|
386
|
+
client;
|
|
387
|
+
constructor(client) {
|
|
388
|
+
this.client = client;
|
|
389
|
+
}
|
|
390
|
+
async list() {
|
|
391
|
+
const result = await this.client._request("GET", "/api/v1/org");
|
|
392
|
+
return Array.isArray(result) ? result : result.nodes ?? [];
|
|
393
|
+
}
|
|
394
|
+
async get(id) {
|
|
395
|
+
return this.client._request("GET", `/api/v1/org/${encodeURIComponent(id)}`);
|
|
396
|
+
}
|
|
397
|
+
async create(type, name, metadata) {
|
|
398
|
+
return this.client._request("POST", "/api/v1/org", {
|
|
399
|
+
body: { type, name, metadata: metadata ?? {} },
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
async children(id) {
|
|
403
|
+
const result = await this.client._request("GET", `/api/v1/org/${encodeURIComponent(id)}/children`);
|
|
404
|
+
return Array.isArray(result) ? result : result.children ?? [];
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// ── Data Module ──────────────────────────────────────────────────────────────
|
|
408
|
+
class SqlSubmodule {
|
|
409
|
+
client;
|
|
410
|
+
constructor(client) {
|
|
411
|
+
this.client = client;
|
|
412
|
+
}
|
|
413
|
+
async createTable(tableName, columns) {
|
|
414
|
+
return this.client._request("POST", "/api/v1/data/rel/create", {
|
|
415
|
+
body: { table_name: tableName, columns },
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
async query(sql, params) {
|
|
419
|
+
const result = await this.client._request("POST", "/api/v1/sql/query", { body: { sql, params: params ?? {} } });
|
|
420
|
+
return Array.isArray(result) ? result : result.rows ?? [];
|
|
421
|
+
}
|
|
422
|
+
async dropTable(name) {
|
|
423
|
+
return this.client._request("POST", "/api/v1/data/rel/drop", {
|
|
424
|
+
body: { table_name: name },
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
class DocSubmodule {
|
|
429
|
+
client;
|
|
430
|
+
constructor(client) {
|
|
431
|
+
this.client = client;
|
|
432
|
+
}
|
|
433
|
+
async put(collection, key, data) {
|
|
434
|
+
return this.client._request("POST", `/api/v1/data/doc/${encodeURIComponent(collection)}/${encodeURIComponent(key)}`, { body: data });
|
|
435
|
+
}
|
|
436
|
+
async get(collection, key) {
|
|
437
|
+
return this.client._request("GET", `/api/v1/data/doc/${encodeURIComponent(collection)}/${encodeURIComponent(key)}`);
|
|
438
|
+
}
|
|
439
|
+
async scan(collection, filter) {
|
|
440
|
+
return this.client._request("POST", "/api/v1/data/query/scan", {
|
|
441
|
+
body: { prefix: collection, filter: filter ?? {} },
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
class DataModule {
|
|
446
|
+
sql;
|
|
447
|
+
doc;
|
|
448
|
+
constructor(client) {
|
|
449
|
+
this.sql = new SqlSubmodule(client);
|
|
450
|
+
this.doc = new DocSubmodule(client);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// ── Storage Module ───────────────────────────────────────────────────────────
|
|
454
|
+
class StorageModule {
|
|
455
|
+
client;
|
|
456
|
+
constructor(client) {
|
|
457
|
+
this.client = client;
|
|
458
|
+
}
|
|
459
|
+
async listBuckets() {
|
|
460
|
+
const result = await this.client._request("GET", "/api/v1/store/buckets");
|
|
461
|
+
return Array.isArray(result) ? result : result.buckets ?? [];
|
|
462
|
+
}
|
|
463
|
+
async createBucket(name, isPublic = false) {
|
|
464
|
+
return this.client._request("POST", "/api/v1/store/buckets", {
|
|
465
|
+
body: { name, public: isPublic },
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
async listObjects(bucket) {
|
|
469
|
+
const result = await this.client._request("GET", `/api/v1/store/objects/${encodeURIComponent(bucket)}`);
|
|
470
|
+
return Array.isArray(result) ? result : result.objects ?? [];
|
|
471
|
+
}
|
|
472
|
+
async uploadObject(bucket, path, body, contentType = "application/octet-stream") {
|
|
473
|
+
const encodedPath = path
|
|
474
|
+
.split("/")
|
|
475
|
+
.map((s) => encodeURIComponent(s))
|
|
476
|
+
.join("/");
|
|
477
|
+
return this.client._request("POST", `/api/v1/store/objects/${encodeURIComponent(bucket)}/${encodedPath}`, {
|
|
478
|
+
body,
|
|
479
|
+
headers: { "Content-Type": contentType },
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
async deleteObject(bucket, path) {
|
|
483
|
+
const encodedPath = path
|
|
484
|
+
.split("/")
|
|
485
|
+
.map((s) => encodeURIComponent(s))
|
|
486
|
+
.join("/");
|
|
487
|
+
await this.client._request("DELETE", `/api/v1/store/objects/${encodeURIComponent(bucket)}/${encodedPath}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// ── Functions Module ─────────────────────────────────────────────────────────
|
|
491
|
+
class FunctionsModule {
|
|
492
|
+
client;
|
|
493
|
+
constructor(client) {
|
|
494
|
+
this.client = client;
|
|
495
|
+
}
|
|
496
|
+
async deploy(name, code, runtime, entrypoint) {
|
|
497
|
+
return this.client._request("POST", "/api/v1/fn/deploy", { body: { name, code, runtime, entrypoint } });
|
|
498
|
+
}
|
|
499
|
+
async invoke(name, payload) {
|
|
500
|
+
return this.client._request("POST", `/api/v1/fn/${encodeURIComponent(name)}/invoke`, { body: payload ?? {} });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// ── Realtime Module ──────────────────────────────────────────────────────────
|
|
504
|
+
class RealtimeModule {
|
|
505
|
+
client;
|
|
506
|
+
constructor(client) {
|
|
507
|
+
this.client = client;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Subscribe to a realtime channel via WebSocket.
|
|
511
|
+
*
|
|
512
|
+
* Uses a short-lived ticket so JWTs are never placed in WebSocket URLs.
|
|
513
|
+
*
|
|
514
|
+
* Returns a subscription handle with an `unsubscribe()` method.
|
|
515
|
+
*/
|
|
516
|
+
subscribe(channel, callback) {
|
|
517
|
+
let ws = null;
|
|
518
|
+
let unsubscribed = false;
|
|
519
|
+
void (async () => {
|
|
520
|
+
try {
|
|
521
|
+
const ticket = await this.client._issueRealtimeWsTicket(channel);
|
|
522
|
+
if (unsubscribed) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const params = new URLSearchParams({ ticket });
|
|
526
|
+
const wsUrl = this.client._wsUrl(`/api/v1/rt/ws/${encodeURIComponent(channel)}?${params.toString()}`);
|
|
527
|
+
ws = new WebSocket(wsUrl);
|
|
528
|
+
ws.onmessage = (event) => {
|
|
529
|
+
try {
|
|
530
|
+
const parsed = JSON.parse(String(event.data));
|
|
531
|
+
callback(parsed);
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
callback({ type: "message", channel, payload: event.data });
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
ws.onerror = (event) => {
|
|
538
|
+
callback({
|
|
539
|
+
type: "error",
|
|
540
|
+
channel,
|
|
541
|
+
payload: event,
|
|
542
|
+
});
|
|
543
|
+
};
|
|
544
|
+
ws.onclose = (event) => {
|
|
545
|
+
callback({
|
|
546
|
+
type: "close",
|
|
547
|
+
channel,
|
|
548
|
+
payload: { code: event.code, reason: event.reason },
|
|
549
|
+
});
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
callback({
|
|
554
|
+
type: "error",
|
|
555
|
+
channel,
|
|
556
|
+
payload: error,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
})();
|
|
560
|
+
return {
|
|
561
|
+
unsubscribe: () => {
|
|
562
|
+
unsubscribed = true;
|
|
563
|
+
if (ws
|
|
564
|
+
&& (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
|
565
|
+
ws.close();
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// ── Chrono Module ────────────────────────────────────────────────────────────
|
|
572
|
+
class ChronoModule {
|
|
573
|
+
client;
|
|
574
|
+
constructor(client) {
|
|
575
|
+
this.client = client;
|
|
576
|
+
}
|
|
577
|
+
async log(timelineName) {
|
|
578
|
+
return this.client._request("GET", `/api/v1/chrono/timelines/${encodeURIComponent(timelineName)}/history`);
|
|
579
|
+
}
|
|
580
|
+
async jump(timelineName, targetHex) {
|
|
581
|
+
return this.client._request("POST", `/api/v1/chrono/timelines/${encodeURIComponent(timelineName)}/jump`, { body: { target_hex: targetHex } });
|
|
582
|
+
}
|
|
583
|
+
}
|