@persql/sdk 0.1.0 → 1.1.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/README.md +3 -3
- package/dist/{chunk-CDNTQOBK.js → chunk-YZEQFTCX.js} +414 -159
- package/dist/chunk-YZEQFTCX.js.map +1 -0
- package/dist/cli.cjs +411 -156
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +417 -162
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +318 -128
- package/dist/index.d.ts +318 -128
- package/dist/index.js +7 -7
- package/package.json +1 -1
- package/dist/chunk-CDNTQOBK.js.map +0 -1
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
// src/local.ts
|
|
2
|
+
function checkLocalExpect(e, r) {
|
|
3
|
+
const rows = r.rows.length;
|
|
4
|
+
if (e.rows != null && rows !== e.rows) {
|
|
5
|
+
return `expected rows=${e.rows}, got ${rows}`;
|
|
6
|
+
}
|
|
7
|
+
if (e.rowsAtLeast != null && rows < e.rowsAtLeast) {
|
|
8
|
+
return `expected rows>=${e.rowsAtLeast}, got ${rows}`;
|
|
9
|
+
}
|
|
10
|
+
if (e.rowsAtMost != null && rows > e.rowsAtMost) {
|
|
11
|
+
return `expected rows<=${e.rowsAtMost}, got ${rows}`;
|
|
12
|
+
}
|
|
13
|
+
if (e.rowsWritten != null && r.rowsWritten !== e.rowsWritten) {
|
|
14
|
+
return `expected rowsWritten=${e.rowsWritten}, got ${r.rowsWritten}`;
|
|
15
|
+
}
|
|
16
|
+
if (e.rowsWrittenAtLeast != null && r.rowsWritten < e.rowsWrittenAtLeast) {
|
|
17
|
+
return `expected rowsWritten>=${e.rowsWrittenAtLeast}, got ${r.rowsWritten}`;
|
|
18
|
+
}
|
|
19
|
+
if (e.rowsWrittenAtMost != null && r.rowsWritten > e.rowsWrittenAtMost) {
|
|
20
|
+
return `expected rowsWritten<=${e.rowsWrittenAtMost}, got ${r.rowsWritten}`;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
2
24
|
var PROPOSAL_TTL_DEFAULT_SEC = 600;
|
|
3
25
|
var PROPOSAL_TTL_MAX_SEC = 3600;
|
|
4
26
|
function newLocalProposalId() {
|
|
@@ -37,7 +59,16 @@ var LocalDriver = class {
|
|
|
37
59
|
}
|
|
38
60
|
async batch(statements, transaction) {
|
|
39
61
|
const db = await this.open();
|
|
40
|
-
const exec = () => statements.map((s) =>
|
|
62
|
+
const exec = () => statements.map((s, i) => {
|
|
63
|
+
const r = runOne(db, s.sql, s.params ?? []);
|
|
64
|
+
if (s.expect) {
|
|
65
|
+
const violation = checkLocalExpect(s.expect, r);
|
|
66
|
+
if (violation) {
|
|
67
|
+
throw new Error(`Assertion failed on statement ${i}: ${violation}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return r;
|
|
71
|
+
});
|
|
41
72
|
if (transaction) return db.transaction(exec)();
|
|
42
73
|
return exec();
|
|
43
74
|
}
|
|
@@ -199,6 +230,19 @@ var PerSQL = class _PerSQL {
|
|
|
199
230
|
close() {
|
|
200
231
|
this.local?.close();
|
|
201
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Resolve the bearer token's namespace + live usage. Use this once
|
|
235
|
+
* per agent run so you know the slug to put in URLs and how much
|
|
236
|
+
* monthly budget / prepaid balance remains before a 402.
|
|
237
|
+
*
|
|
238
|
+
* Throws in local mode — no server, nothing to ask.
|
|
239
|
+
*/
|
|
240
|
+
async me() {
|
|
241
|
+
if (this.local) {
|
|
242
|
+
throw new Error("PerSQL: me() is not available in local mode.");
|
|
243
|
+
}
|
|
244
|
+
return this.request("GET", "/v1/me");
|
|
245
|
+
}
|
|
202
246
|
/**
|
|
203
247
|
* Redeem a handoff token (`phand_…`) for a regular PerSQL client
|
|
204
248
|
* scoped to the handed-off (database, branch, role). Single use:
|
|
@@ -272,40 +316,74 @@ var PerSQL = class _PerSQL {
|
|
|
272
316
|
}
|
|
273
317
|
return envelope.data;
|
|
274
318
|
}
|
|
275
|
-
/** @internal — raw
|
|
276
|
-
async requestRaw(method, path, body, contentType) {
|
|
277
|
-
const res = await this._fetch(`${this.baseURL}${path}`, {
|
|
278
|
-
method,
|
|
279
|
-
headers: {
|
|
280
|
-
Authorization: `Bearer ${this.token}`,
|
|
281
|
-
"Content-Type": contentType ?? "application/octet-stream"
|
|
282
|
-
},
|
|
283
|
-
body,
|
|
284
|
-
// @ts-expect-error — duplex is required by Node fetch for streaming bodies
|
|
285
|
-
duplex: "half"
|
|
286
|
-
});
|
|
287
|
-
if (res.status === 429) {
|
|
288
|
-
const retryAfter = Number(res.headers.get("retry-after") ?? "60");
|
|
289
|
-
throw new RateLimitError(retryAfter);
|
|
290
|
-
}
|
|
291
|
-
let envelope = null;
|
|
292
|
-
try {
|
|
293
|
-
envelope = await res.json();
|
|
294
|
-
} catch {
|
|
295
|
-
}
|
|
296
|
-
if (!res.ok || !envelope?.success) {
|
|
297
|
-
const message = envelope?.error ?? `Request failed (${res.status})`;
|
|
298
|
-
throw new PerSQLError(res.status, message, envelope?.errorDetail);
|
|
299
|
-
}
|
|
300
|
-
return envelope.data;
|
|
301
|
-
}
|
|
302
|
-
/** @internal — raw fetch returning the underlying Response (used by blob GET). */
|
|
319
|
+
/** @internal — raw fetch returning the underlying Response. */
|
|
303
320
|
fetchRaw(method, path) {
|
|
304
321
|
return this._fetch(`${this.baseURL}${path}`, {
|
|
305
322
|
method,
|
|
306
323
|
headers: { Authorization: `Bearer ${this.token}` }
|
|
307
324
|
});
|
|
308
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* File a support ticket directly from your SDK code. Tickets land in
|
|
328
|
+
* the customer console at `/support`; staff reply from the admin
|
|
329
|
+
* app. Useful for agents and background jobs that hit a PerSQL
|
|
330
|
+
* error and want a human to see it.
|
|
331
|
+
*
|
|
332
|
+
* ```ts
|
|
333
|
+
* try {
|
|
334
|
+
* await db.query("UPDATE ...");
|
|
335
|
+
* } catch (err) {
|
|
336
|
+
* await persql.support.createTicket({
|
|
337
|
+
* subject: "Branch merge keeps 500ing",
|
|
338
|
+
* body: "Repro: ...",
|
|
339
|
+
* error: err,
|
|
340
|
+
* });
|
|
341
|
+
* }
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
344
|
+
get support() {
|
|
345
|
+
return new SupportClient(this);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
function errorContextFor(err) {
|
|
349
|
+
if (err instanceof PerSQLError) {
|
|
350
|
+
return {
|
|
351
|
+
errorName: err.name,
|
|
352
|
+
errorMessage: err.message,
|
|
353
|
+
status: err.status,
|
|
354
|
+
detail: err.detail ?? null
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (err instanceof Error) {
|
|
358
|
+
return { errorName: err.name, errorMessage: err.message };
|
|
359
|
+
}
|
|
360
|
+
if (err !== void 0 && err !== null) {
|
|
361
|
+
return { errorMessage: String(err) };
|
|
362
|
+
}
|
|
363
|
+
return {};
|
|
364
|
+
}
|
|
365
|
+
var SupportClient = class {
|
|
366
|
+
constructor(client) {
|
|
367
|
+
this.client = client;
|
|
368
|
+
}
|
|
369
|
+
async createTicket(opts) {
|
|
370
|
+
if (this.client.local) {
|
|
371
|
+
throw new Error("support.createTicket is not supported in local mode");
|
|
372
|
+
}
|
|
373
|
+
const errorContext = {
|
|
374
|
+
...errorContextFor(opts.error),
|
|
375
|
+
...opts.errorContext ?? {}
|
|
376
|
+
};
|
|
377
|
+
return this.client.request(
|
|
378
|
+
"POST",
|
|
379
|
+
"/v1/support/tickets",
|
|
380
|
+
{
|
|
381
|
+
subject: opts.subject,
|
|
382
|
+
body: opts.body,
|
|
383
|
+
errorContext: Object.keys(errorContext).length > 0 ? errorContext : void 0
|
|
384
|
+
}
|
|
385
|
+
);
|
|
386
|
+
}
|
|
309
387
|
};
|
|
310
388
|
var PerSQLDatabase = class {
|
|
311
389
|
constructor(client, namespace, slug) {
|
|
@@ -359,6 +437,25 @@ var PerSQLDatabase = class {
|
|
|
359
437
|
transaction(statements, options = {}) {
|
|
360
438
|
return this.batch(statements, { ...options, transaction: true });
|
|
361
439
|
}
|
|
440
|
+
/**
|
|
441
|
+
* Mint a short-lived session JWT for an end user. The server (which
|
|
442
|
+
* holds the bearer token) calls this and hands the resulting token
|
|
443
|
+
* to an untrusted client (browser, mobile app, agent run). The
|
|
444
|
+
* client uses the JWT to call this database's published `/p/*`
|
|
445
|
+
* endpoints on behalf of `userId`. TTL defaults to 1 hour; max 24h.
|
|
446
|
+
*/
|
|
447
|
+
async createSession(opts) {
|
|
448
|
+
if (this.client.local) {
|
|
449
|
+
throw new Error("createSession is not supported in local mode");
|
|
450
|
+
}
|
|
451
|
+
return this.client.request("POST", "/v1/sessions", {
|
|
452
|
+
namespaceSlug: this.namespace,
|
|
453
|
+
databaseSlug: this.slug,
|
|
454
|
+
userId: opts.userId,
|
|
455
|
+
email: opts.email,
|
|
456
|
+
expiresIn: opts.expiresIn
|
|
457
|
+
});
|
|
458
|
+
}
|
|
362
459
|
/**
|
|
363
460
|
* Engine telemetry — recent /v1/query and /v1/batch calls issued
|
|
364
461
|
* against this database. Backed by the in-DO `_persql_meta_query_log`
|
|
@@ -480,6 +577,61 @@ var PerSQLDatabase = class {
|
|
|
480
577
|
if (this.client.local) return this.client.local.explain(sql, params);
|
|
481
578
|
return this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/explain`, { sql, params });
|
|
482
579
|
}
|
|
580
|
+
/**
|
|
581
|
+
* Parse + bind-check a SQL statement without executing it. Faster
|
|
582
|
+
* and cheaper than running the query just to catch unknown columns
|
|
583
|
+
* or wrong param counts.
|
|
584
|
+
*
|
|
585
|
+
* Returns `{ ok: true }` on success or `{ ok: false, error, errorDetail }`
|
|
586
|
+
* with the same `errorDetail` envelope as `PerSQLError.detail`.
|
|
587
|
+
*/
|
|
588
|
+
async validate(sql, params = []) {
|
|
589
|
+
if (this.client.local) {
|
|
590
|
+
throw new Error("PerSQL: validate is not available in local mode.");
|
|
591
|
+
}
|
|
592
|
+
const data = await this.client.request(
|
|
593
|
+
"POST",
|
|
594
|
+
`/v1/db/${this.namespace}/${this.slug}/validate`,
|
|
595
|
+
{ sql, params }
|
|
596
|
+
);
|
|
597
|
+
return data;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Compact text rendering of the database's schema — one line per
|
|
601
|
+
* table with columns, types, PKs, and FK arrows inlined. Designed
|
|
602
|
+
* to stuff into an LLM prompt with minimum token overhead. See
|
|
603
|
+
* `db.describe()` for the structured form.
|
|
604
|
+
*/
|
|
605
|
+
async describeCompact() {
|
|
606
|
+
if (this.client.local) {
|
|
607
|
+
throw new Error("PerSQL: describeCompact is not available in local mode.");
|
|
608
|
+
}
|
|
609
|
+
const data = await this.client.request(
|
|
610
|
+
"GET",
|
|
611
|
+
`/v1/db/${this.namespace}/${this.slug}/describe?format=compact`
|
|
612
|
+
);
|
|
613
|
+
return data.text;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* First N rows of a table plus per-column stats (null count,
|
|
617
|
+
* distinct count, min, max). One call to size up an unfamiliar
|
|
618
|
+
* table — replaces a hand-rolled `SELECT *`, `COUNT(*)`, and
|
|
619
|
+
* `PRAGMA table_info` sequence.
|
|
620
|
+
*
|
|
621
|
+
* `n` defaults to 10 and is capped at 100.
|
|
622
|
+
*/
|
|
623
|
+
async sampleTable(table, opts = {}) {
|
|
624
|
+
if (this.client.local) {
|
|
625
|
+
throw new Error("PerSQL: sampleTable is not available in local mode.");
|
|
626
|
+
}
|
|
627
|
+
const qs = opts.n ? `?n=${Math.max(1, Math.min(100, opts.n | 0))}` : "";
|
|
628
|
+
return this.client.request(
|
|
629
|
+
"GET",
|
|
630
|
+
`/v1/db/${this.namespace}/${this.slug}/tables/${encodeURIComponent(
|
|
631
|
+
table
|
|
632
|
+
)}/sample${qs}`
|
|
633
|
+
);
|
|
634
|
+
}
|
|
483
635
|
/**
|
|
484
636
|
* Introspect the database schema. Returns one entry per user table
|
|
485
637
|
* with column definitions, suitable for codegen tools (the
|
|
@@ -503,20 +655,6 @@ var PerSQLDatabase = class {
|
|
|
503
655
|
}
|
|
504
656
|
return out;
|
|
505
657
|
}
|
|
506
|
-
/**
|
|
507
|
-
* Per-database semantic search via Cloudflare Vectorize. Use
|
|
508
|
-
* `vectors.upsert` to embed and store rows; `vectors.query` to
|
|
509
|
-
* retrieve the most similar by free text. Embeddings are computed
|
|
510
|
-
* server-side using bge-base-en-v1.5 (768 dim, cosine distance).
|
|
511
|
-
*/
|
|
512
|
-
get vectors() {
|
|
513
|
-
if (this.client.local) {
|
|
514
|
-
throw new Error(
|
|
515
|
-
"PerSQL: vectors require Cloudflare Vectorize and Workers AI \u2014 not available in local mode. Use a server-mode token (psql_live_/psql_test_) to call vectors.upsert/query/delete."
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
return new PerSQLVectors(this.client, this.namespace, this.slug);
|
|
519
|
-
}
|
|
520
658
|
/**
|
|
521
659
|
* Manage preview/PR-style branches of this database. Each branch is
|
|
522
660
|
* its own DO with its own SQLite file; create-or-reset by ref is
|
|
@@ -545,6 +683,19 @@ var PerSQLDatabase = class {
|
|
|
545
683
|
}
|
|
546
684
|
return new PerSQLApprovals(this.client, this.namespace, this.slug);
|
|
547
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* Manage the require_approval / deny rules for this database. Reads
|
|
688
|
+
* are open to any bearer with read access; create + delete require
|
|
689
|
+
* an admin-role bearer token.
|
|
690
|
+
*/
|
|
691
|
+
get approvalRules() {
|
|
692
|
+
if (this.client.local) {
|
|
693
|
+
throw new Error(
|
|
694
|
+
"PerSQL: approval rules live on the server; local mode does not enforce them."
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
return new PerSQLApprovalRules(this.client, this.namespace, this.slug);
|
|
698
|
+
}
|
|
548
699
|
/**
|
|
549
700
|
* Pre-flight a write before running it. `propose()` validates the
|
|
550
701
|
* SQL via EXPLAIN, estimates affected rows, and returns a single-use
|
|
@@ -556,20 +707,6 @@ var PerSQLDatabase = class {
|
|
|
556
707
|
get proposals() {
|
|
557
708
|
return new PerSQLProposals(this.client, this.namespace, this.slug);
|
|
558
709
|
}
|
|
559
|
-
/**
|
|
560
|
-
* Per-database BLOB storage backed by R2. Use this for anything
|
|
561
|
-
* larger than a SQLite cell (images, PDFs, model weights). Each
|
|
562
|
-
* database has its own private namespace; keys may be hierarchical
|
|
563
|
-
* (`avatars/2025/foo.jpg`) but never start with `/`.
|
|
564
|
-
*/
|
|
565
|
-
get blob() {
|
|
566
|
-
if (this.client.local) {
|
|
567
|
-
throw new Error(
|
|
568
|
-
"PerSQL: blob storage is backed by R2 \u2014 not available in local mode. Use a server-mode token, or store blobs in your test fixtures directly."
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
|
-
return new PerSQLBlob(this.client, this.namespace, this.slug);
|
|
572
|
-
}
|
|
573
710
|
/**
|
|
574
711
|
* Subscribe to row-changes via WebSocket — the SQL equivalent of
|
|
575
712
|
* Postgres `LISTEN`. The callback fires once per write that
|
|
@@ -630,6 +767,55 @@ var PerSQLDatabase = class {
|
|
|
630
767
|
}
|
|
631
768
|
};
|
|
632
769
|
}
|
|
770
|
+
/**
|
|
771
|
+
* Long-poll for row-changes on this database. Returns immediately
|
|
772
|
+
* if changes newer than `since` are already buffered; otherwise
|
|
773
|
+
* blocks server-side for up to `waitMs` (default 25s, max 25s).
|
|
774
|
+
*
|
|
775
|
+
* Most callers want `changes()` instead — an async iterator that
|
|
776
|
+
* loops `waitForChanges` forever, surfacing each batch as it
|
|
777
|
+
* arrives.
|
|
778
|
+
*/
|
|
779
|
+
async waitForChanges(opts = {}) {
|
|
780
|
+
if (this.client.local) {
|
|
781
|
+
throw new Error(
|
|
782
|
+
"PerSQL: waitForChanges requires the DO change ring \u2014 not available in local mode."
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
const qs = new URLSearchParams();
|
|
786
|
+
if (typeof opts.since === "number") qs.set("since", String(opts.since));
|
|
787
|
+
if (typeof opts.waitMs === "number") qs.set("waitMs", String(opts.waitMs));
|
|
788
|
+
if (opts.tables && opts.tables.length > 0) qs.set("tables", opts.tables.join(","));
|
|
789
|
+
const q = qs.toString();
|
|
790
|
+
return this.client.request(
|
|
791
|
+
"GET",
|
|
792
|
+
`/v1/db/${this.namespace}/${this.slug}/changes${q ? `?${q}` : ""}`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Async-iterator change feed. Loops `waitForChanges` forever and
|
|
797
|
+
* yields one batch per server response. Pass an `AbortSignal` to
|
|
798
|
+
* stop the loop cleanly.
|
|
799
|
+
*
|
|
800
|
+
* ```ts
|
|
801
|
+
* const ctl = new AbortController();
|
|
802
|
+
* for await (const batch of db.changes({ signal: ctl.signal })) {
|
|
803
|
+
* for (const c of batch.changes) console.log(c.kind, c.table);
|
|
804
|
+
* }
|
|
805
|
+
* ```
|
|
806
|
+
*/
|
|
807
|
+
async *changes(opts = {}) {
|
|
808
|
+
let cursor = opts.since ?? 0;
|
|
809
|
+
while (!opts.signal?.aborted) {
|
|
810
|
+
const batch = await this.waitForChanges({
|
|
811
|
+
since: cursor,
|
|
812
|
+
waitMs: opts.waitMs,
|
|
813
|
+
tables: opts.tables
|
|
814
|
+
});
|
|
815
|
+
cursor = batch.cursor;
|
|
816
|
+
yield batch;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
633
819
|
/**
|
|
634
820
|
* Returns the callback shape `drizzle-orm/sqlite-proxy` expects.
|
|
635
821
|
* Pair with `drizzle()` from that module to get a typed,
|
|
@@ -657,42 +843,64 @@ var PerSQLDatabase = class {
|
|
|
657
843
|
return callback;
|
|
658
844
|
}
|
|
659
845
|
/**
|
|
660
|
-
* Returns a tool definition
|
|
661
|
-
*
|
|
846
|
+
* Returns a single SQL-query tool definition in every shape PerSQL
|
|
847
|
+
* supports (Anthropic, OpenAI Chat, Vercel AI SDK, Mastra, LangChain,
|
|
848
|
+
* OpenAI Agents SDK). Same input contract as `asTools()` — the only
|
|
849
|
+
* difference is one fat `query` tool instead of typed-per-table tools.
|
|
850
|
+
*
|
|
851
|
+
* Pair the format-specific fields with `runTool` (or use the bundled
|
|
852
|
+
* `execute`/`invoke` callbacks, which call `runTool` internally).
|
|
662
853
|
*/
|
|
663
854
|
asTool(name = "persql_query") {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
type: "array",
|
|
674
|
-
items: {},
|
|
675
|
-
description: "Positional parameters for the SQL statement"
|
|
676
|
-
}
|
|
677
|
-
},
|
|
678
|
-
required: ["sql"]
|
|
855
|
+
const description = `Run a SQL query against the PerSQL database "${this.namespace}/${this.slug}". A single result is capped at 100,000 rows and the query errors above that \u2014 add a LIMIT for large tables. Use parameter binding (?) to avoid injection.`;
|
|
856
|
+
const inputSchema = {
|
|
857
|
+
type: "object",
|
|
858
|
+
properties: {
|
|
859
|
+
sql: { type: "string", description: "SQLite SQL statement" },
|
|
860
|
+
params: {
|
|
861
|
+
type: "array",
|
|
862
|
+
items: {},
|
|
863
|
+
description: "Positional parameters for the SQL statement"
|
|
679
864
|
}
|
|
680
865
|
},
|
|
866
|
+
required: ["sql"],
|
|
867
|
+
// OpenAI strict function-calling rejects a schema without this; the
|
|
868
|
+
// per-table asTools() schemas already set it. Keep them consistent.
|
|
869
|
+
additionalProperties: false
|
|
870
|
+
};
|
|
871
|
+
const execute = (input) => this.runTool({
|
|
872
|
+
sql: String(input.sql ?? ""),
|
|
873
|
+
params: Array.isArray(input.params) ? input.params : []
|
|
874
|
+
});
|
|
875
|
+
return {
|
|
876
|
+
anthropic: { name, description, input_schema: inputSchema },
|
|
681
877
|
openai: {
|
|
682
878
|
type: "function",
|
|
683
|
-
function: {
|
|
879
|
+
function: { name, description, parameters: inputSchema }
|
|
880
|
+
},
|
|
881
|
+
aiSdk: () => ({
|
|
882
|
+
[name]: { description, inputSchema, execute }
|
|
883
|
+
}),
|
|
884
|
+
mastra: () => ({
|
|
885
|
+
[name]: {
|
|
886
|
+
id: name,
|
|
887
|
+
description,
|
|
888
|
+
inputSchema,
|
|
889
|
+
execute: ({ context }) => execute(context)
|
|
890
|
+
}
|
|
891
|
+
}),
|
|
892
|
+
langchain: () => [
|
|
893
|
+
{ name, description, schema: inputSchema, invoke: execute }
|
|
894
|
+
],
|
|
895
|
+
openaiAgents: () => [
|
|
896
|
+
{
|
|
897
|
+
type: "function",
|
|
684
898
|
name,
|
|
685
|
-
description
|
|
686
|
-
parameters:
|
|
687
|
-
|
|
688
|
-
properties: {
|
|
689
|
-
sql: { type: "string" },
|
|
690
|
-
params: { type: "array", items: {} }
|
|
691
|
-
},
|
|
692
|
-
required: ["sql"]
|
|
693
|
-
}
|
|
899
|
+
description,
|
|
900
|
+
parameters: inputSchema,
|
|
901
|
+
invoke: execute
|
|
694
902
|
}
|
|
695
|
-
|
|
903
|
+
]
|
|
696
904
|
};
|
|
697
905
|
}
|
|
698
906
|
/**
|
|
@@ -1464,106 +1672,148 @@ function sanitizeToolPart(s) {
|
|
|
1464
1672
|
function quoteIdent(s) {
|
|
1465
1673
|
return `"${s.replace(/"/g, '""')}"`;
|
|
1466
1674
|
}
|
|
1467
|
-
var
|
|
1675
|
+
var PerSQLApprovals = class {
|
|
1468
1676
|
constructor(client, namespace, slug) {
|
|
1469
1677
|
this.client = client;
|
|
1470
1678
|
this.namespace = namespace;
|
|
1471
1679
|
this.slug = slug;
|
|
1472
1680
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1681
|
+
// Tokens this client has seen via subscribe — used to dedupe poll
|
|
1682
|
+
// results so onApprovalRequired fires once per token regardless of
|
|
1683
|
+
// how many subscribe ticks observe it.
|
|
1684
|
+
seen = /* @__PURE__ */ new Map();
|
|
1685
|
+
/**
|
|
1686
|
+
* Look up the status of an approval token without consuming it. The
|
|
1687
|
+
* bearer must match the one that minted the original 403; cross-bearer
|
|
1688
|
+
* lookup returns 403.
|
|
1689
|
+
*/
|
|
1690
|
+
async get(approvalToken) {
|
|
1691
|
+
return this.client.request(
|
|
1692
|
+
"GET",
|
|
1693
|
+
`/v1/db/${this.namespace}/${this.slug}/approval/${encodeURIComponent(
|
|
1694
|
+
approvalToken
|
|
1695
|
+
)}`
|
|
1481
1696
|
);
|
|
1482
1697
|
}
|
|
1483
|
-
/**
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1698
|
+
/**
|
|
1699
|
+
* Poll until a reviewer decides (or the token expires). Resolves with
|
|
1700
|
+
* the final status — `approved` is your green light for `redeem()`.
|
|
1701
|
+
*/
|
|
1702
|
+
async poll(approvalToken, opts = {}) {
|
|
1703
|
+
const intervalMs = Math.max(250, opts.intervalMs ?? 2e3);
|
|
1704
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 10 * 60 * 1e3);
|
|
1705
|
+
while (true) {
|
|
1706
|
+
if (opts.signal?.aborted) {
|
|
1707
|
+
throw new Error("approval poll aborted");
|
|
1708
|
+
}
|
|
1709
|
+
const status = await this.get(approvalToken);
|
|
1710
|
+
if (status.status !== "pending") return status;
|
|
1711
|
+
if (Date.now() > deadline) return status;
|
|
1712
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1490
1713
|
}
|
|
1491
|
-
return res;
|
|
1492
1714
|
}
|
|
1493
|
-
/**
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1715
|
+
/**
|
|
1716
|
+
* Long-running poll subscription for new approval events on this
|
|
1717
|
+
* database. Returns a `stop()` handle. Webhook delivery is the push
|
|
1718
|
+
* path (events `approval_required` / `approval_resolved`); subscribe
|
|
1719
|
+
* is the pull fallback for clients that can't host a webhook.
|
|
1720
|
+
*
|
|
1721
|
+
* Today this iterates over a known set of tokens supplied by the
|
|
1722
|
+
* caller; an SDK-only client that wants discovery should pair this
|
|
1723
|
+
* with the webhook path. Pass tokens you already hold (e.g. from a
|
|
1724
|
+
* prior `ApprovalRequiredError`).
|
|
1725
|
+
*/
|
|
1726
|
+
subscribe(tokens, opts) {
|
|
1727
|
+
const intervalMs = Math.max(1e3, opts.intervalMs ?? 5e3);
|
|
1728
|
+
let cancelled = false;
|
|
1729
|
+
const onAbort = () => {
|
|
1730
|
+
cancelled = true;
|
|
1731
|
+
};
|
|
1732
|
+
opts.signal?.addEventListener("abort", onAbort);
|
|
1733
|
+
const tick = async () => {
|
|
1734
|
+
for (const token of tokens) {
|
|
1735
|
+
if (cancelled) return;
|
|
1736
|
+
try {
|
|
1737
|
+
const status = await this.get(token);
|
|
1738
|
+
const prev = this.seen.get(token);
|
|
1739
|
+
if (status.status !== prev) {
|
|
1740
|
+
this.seen.set(token, status.status);
|
|
1741
|
+
const event = {
|
|
1742
|
+
approvalToken: token,
|
|
1743
|
+
status: status.status,
|
|
1744
|
+
hits: status.hits
|
|
1745
|
+
};
|
|
1746
|
+
if (status.status === "pending") {
|
|
1747
|
+
opts.onApprovalRequired?.(event);
|
|
1748
|
+
} else {
|
|
1749
|
+
opts.onApprovalResolved?.(event);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
} catch (e) {
|
|
1753
|
+
opts.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
const id = setInterval(() => {
|
|
1758
|
+
void tick();
|
|
1759
|
+
}, intervalMs);
|
|
1760
|
+
void tick();
|
|
1761
|
+
return {
|
|
1762
|
+
stop: () => {
|
|
1763
|
+
cancelled = true;
|
|
1764
|
+
clearInterval(id);
|
|
1765
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1497
1768
|
}
|
|
1498
|
-
/**
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
const
|
|
1505
|
-
|
|
1506
|
-
|
|
1769
|
+
/**
|
|
1770
|
+
* Redeem an approved approvalToken. The bearer must be the same one
|
|
1771
|
+
* that minted it, and the request must target the same database.
|
|
1772
|
+
* Returns the result of the originally-blocked query or batch.
|
|
1773
|
+
*/
|
|
1774
|
+
async redeem(approvalToken) {
|
|
1775
|
+
const raw = await this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/redeem_approval`, {
|
|
1776
|
+
approvalToken
|
|
1777
|
+
});
|
|
1778
|
+
if (Array.isArray(raw)) {
|
|
1779
|
+
return raw.map((r) => ({
|
|
1780
|
+
...r,
|
|
1781
|
+
data: rowsToObjects(r.columns, r.rows)
|
|
1782
|
+
}));
|
|
1783
|
+
}
|
|
1784
|
+
return { ...raw, data: rowsToObjects(raw.columns, raw.rows) };
|
|
1507
1785
|
}
|
|
1508
1786
|
};
|
|
1509
|
-
|
|
1510
|
-
return key.split("/").map(encodeURIComponent).join("/");
|
|
1511
|
-
}
|
|
1512
|
-
var PerSQLVectors = class {
|
|
1787
|
+
var PerSQLApprovalRules = class {
|
|
1513
1788
|
constructor(client, namespace, slug) {
|
|
1514
1789
|
this.client = client;
|
|
1515
1790
|
this.namespace = namespace;
|
|
1516
1791
|
this.slug = slug;
|
|
1517
1792
|
}
|
|
1518
|
-
|
|
1519
|
-
async upsert(items) {
|
|
1793
|
+
async list() {
|
|
1520
1794
|
return this.client.request(
|
|
1521
|
-
"
|
|
1522
|
-
`/v1/db/${this.namespace}/${this.slug}/
|
|
1523
|
-
{ items }
|
|
1795
|
+
"GET",
|
|
1796
|
+
`/v1/db/${this.namespace}/${this.slug}/approval-rules`
|
|
1524
1797
|
);
|
|
1525
1798
|
}
|
|
1526
|
-
/**
|
|
1527
|
-
async
|
|
1799
|
+
/** Requires an admin-role bearer token. */
|
|
1800
|
+
async create(input) {
|
|
1528
1801
|
return this.client.request(
|
|
1529
1802
|
"POST",
|
|
1530
|
-
`/v1/db/${this.namespace}/${this.slug}/
|
|
1531
|
-
|
|
1803
|
+
`/v1/db/${this.namespace}/${this.slug}/approval-rules`,
|
|
1804
|
+
input
|
|
1532
1805
|
);
|
|
1533
1806
|
}
|
|
1534
|
-
/**
|
|
1535
|
-
async delete(
|
|
1807
|
+
/** Requires an admin-role bearer token. */
|
|
1808
|
+
async delete(ruleId) {
|
|
1536
1809
|
return this.client.request(
|
|
1537
1810
|
"DELETE",
|
|
1538
|
-
`/v1/db/${this.namespace}/${this.slug}/
|
|
1539
|
-
|
|
1811
|
+
`/v1/db/${this.namespace}/${this.slug}/approval-rules/${encodeURIComponent(
|
|
1812
|
+
ruleId
|
|
1813
|
+
)}`
|
|
1540
1814
|
);
|
|
1541
1815
|
}
|
|
1542
1816
|
};
|
|
1543
|
-
var PerSQLApprovals = class {
|
|
1544
|
-
constructor(client, namespace, slug) {
|
|
1545
|
-
this.client = client;
|
|
1546
|
-
this.namespace = namespace;
|
|
1547
|
-
this.slug = slug;
|
|
1548
|
-
}
|
|
1549
|
-
/**
|
|
1550
|
-
* Redeem an approved approvalToken. The bearer must be the same one
|
|
1551
|
-
* that minted it, and the request must target the same database.
|
|
1552
|
-
* Returns the result of the originally-blocked query or batch.
|
|
1553
|
-
*/
|
|
1554
|
-
async redeem(approvalToken) {
|
|
1555
|
-
const raw = await this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/redeem_approval`, {
|
|
1556
|
-
approvalToken
|
|
1557
|
-
});
|
|
1558
|
-
if (Array.isArray(raw)) {
|
|
1559
|
-
return raw.map((r) => ({
|
|
1560
|
-
...r,
|
|
1561
|
-
data: rowsToObjects(r.columns, r.rows)
|
|
1562
|
-
}));
|
|
1563
|
-
}
|
|
1564
|
-
return { ...raw, data: rowsToObjects(raw.columns, raw.rows) };
|
|
1565
|
-
}
|
|
1566
|
-
};
|
|
1567
1817
|
var PerSQLProposals = class {
|
|
1568
1818
|
constructor(client, namespace, slug) {
|
|
1569
1819
|
this.client = client;
|
|
@@ -1575,6 +1825,11 @@ var PerSQLProposals = class {
|
|
|
1575
1825
|
* rows, and returns a single-use `executionToken` valid for 10 min
|
|
1576
1826
|
* (override with `ttlSec`, max 1 hour). Nothing is mutated until
|
|
1577
1827
|
* `apply()` is called.
|
|
1828
|
+
*
|
|
1829
|
+
* Local mode keeps the token in-process: single-use still holds, but
|
|
1830
|
+
* `estimatedAffectedRows` is always `null` (no server estimator) and a
|
|
1831
|
+
* stale/unknown token throws a plain `Error` rather than the HTTP
|
|
1832
|
+
* 404/403 of the server path.
|
|
1578
1833
|
*/
|
|
1579
1834
|
async propose(sql, opts = {}) {
|
|
1580
1835
|
if (this.client.local) {
|
|
@@ -1727,11 +1982,11 @@ export {
|
|
|
1727
1982
|
RateLimitError,
|
|
1728
1983
|
ApprovalRequiredError,
|
|
1729
1984
|
PerSQL,
|
|
1985
|
+
SupportClient,
|
|
1730
1986
|
PerSQLDatabase,
|
|
1731
|
-
PerSQLBlob,
|
|
1732
|
-
PerSQLVectors,
|
|
1733
1987
|
PerSQLApprovals,
|
|
1988
|
+
PerSQLApprovalRules,
|
|
1734
1989
|
PerSQLProposals,
|
|
1735
1990
|
PerSQLBranches
|
|
1736
1991
|
};
|
|
1737
|
-
//# sourceMappingURL=chunk-
|
|
1992
|
+
//# sourceMappingURL=chunk-YZEQFTCX.js.map
|