@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
package/dist/index.cjs
CHANGED
|
@@ -22,18 +22,40 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ApprovalRequiredError: () => ApprovalRequiredError,
|
|
24
24
|
PerSQL: () => PerSQL,
|
|
25
|
+
PerSQLApprovalRules: () => PerSQLApprovalRules,
|
|
25
26
|
PerSQLApprovals: () => PerSQLApprovals,
|
|
26
|
-
PerSQLBlob: () => PerSQLBlob,
|
|
27
27
|
PerSQLBranches: () => PerSQLBranches,
|
|
28
28
|
PerSQLDatabase: () => PerSQLDatabase,
|
|
29
29
|
PerSQLError: () => PerSQLError,
|
|
30
30
|
PerSQLProposals: () => PerSQLProposals,
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
RateLimitError: () => RateLimitError,
|
|
32
|
+
SupportClient: () => SupportClient
|
|
33
33
|
});
|
|
34
34
|
module.exports = __toCommonJS(index_exports);
|
|
35
35
|
|
|
36
36
|
// src/local.ts
|
|
37
|
+
function checkLocalExpect(e, r) {
|
|
38
|
+
const rows = r.rows.length;
|
|
39
|
+
if (e.rows != null && rows !== e.rows) {
|
|
40
|
+
return `expected rows=${e.rows}, got ${rows}`;
|
|
41
|
+
}
|
|
42
|
+
if (e.rowsAtLeast != null && rows < e.rowsAtLeast) {
|
|
43
|
+
return `expected rows>=${e.rowsAtLeast}, got ${rows}`;
|
|
44
|
+
}
|
|
45
|
+
if (e.rowsAtMost != null && rows > e.rowsAtMost) {
|
|
46
|
+
return `expected rows<=${e.rowsAtMost}, got ${rows}`;
|
|
47
|
+
}
|
|
48
|
+
if (e.rowsWritten != null && r.rowsWritten !== e.rowsWritten) {
|
|
49
|
+
return `expected rowsWritten=${e.rowsWritten}, got ${r.rowsWritten}`;
|
|
50
|
+
}
|
|
51
|
+
if (e.rowsWrittenAtLeast != null && r.rowsWritten < e.rowsWrittenAtLeast) {
|
|
52
|
+
return `expected rowsWritten>=${e.rowsWrittenAtLeast}, got ${r.rowsWritten}`;
|
|
53
|
+
}
|
|
54
|
+
if (e.rowsWrittenAtMost != null && r.rowsWritten > e.rowsWrittenAtMost) {
|
|
55
|
+
return `expected rowsWritten<=${e.rowsWrittenAtMost}, got ${r.rowsWritten}`;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
37
59
|
var PROPOSAL_TTL_DEFAULT_SEC = 600;
|
|
38
60
|
var PROPOSAL_TTL_MAX_SEC = 3600;
|
|
39
61
|
function newLocalProposalId() {
|
|
@@ -72,7 +94,16 @@ var LocalDriver = class {
|
|
|
72
94
|
}
|
|
73
95
|
async batch(statements, transaction) {
|
|
74
96
|
const db = await this.open();
|
|
75
|
-
const exec = () => statements.map((s) =>
|
|
97
|
+
const exec = () => statements.map((s, i) => {
|
|
98
|
+
const r = runOne(db, s.sql, s.params ?? []);
|
|
99
|
+
if (s.expect) {
|
|
100
|
+
const violation = checkLocalExpect(s.expect, r);
|
|
101
|
+
if (violation) {
|
|
102
|
+
throw new Error(`Assertion failed on statement ${i}: ${violation}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return r;
|
|
106
|
+
});
|
|
76
107
|
if (transaction) return db.transaction(exec)();
|
|
77
108
|
return exec();
|
|
78
109
|
}
|
|
@@ -234,6 +265,19 @@ var PerSQL = class _PerSQL {
|
|
|
234
265
|
close() {
|
|
235
266
|
this.local?.close();
|
|
236
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Resolve the bearer token's namespace + live usage. Use this once
|
|
270
|
+
* per agent run so you know the slug to put in URLs and how much
|
|
271
|
+
* monthly budget / prepaid balance remains before a 402.
|
|
272
|
+
*
|
|
273
|
+
* Throws in local mode — no server, nothing to ask.
|
|
274
|
+
*/
|
|
275
|
+
async me() {
|
|
276
|
+
if (this.local) {
|
|
277
|
+
throw new Error("PerSQL: me() is not available in local mode.");
|
|
278
|
+
}
|
|
279
|
+
return this.request("GET", "/v1/me");
|
|
280
|
+
}
|
|
237
281
|
/**
|
|
238
282
|
* Redeem a handoff token (`phand_…`) for a regular PerSQL client
|
|
239
283
|
* scoped to the handed-off (database, branch, role). Single use:
|
|
@@ -307,40 +351,74 @@ var PerSQL = class _PerSQL {
|
|
|
307
351
|
}
|
|
308
352
|
return envelope.data;
|
|
309
353
|
}
|
|
310
|
-
/** @internal — raw
|
|
311
|
-
async requestRaw(method, path, body, contentType) {
|
|
312
|
-
const res = await this._fetch(`${this.baseURL}${path}`, {
|
|
313
|
-
method,
|
|
314
|
-
headers: {
|
|
315
|
-
Authorization: `Bearer ${this.token}`,
|
|
316
|
-
"Content-Type": contentType ?? "application/octet-stream"
|
|
317
|
-
},
|
|
318
|
-
body,
|
|
319
|
-
// @ts-expect-error — duplex is required by Node fetch for streaming bodies
|
|
320
|
-
duplex: "half"
|
|
321
|
-
});
|
|
322
|
-
if (res.status === 429) {
|
|
323
|
-
const retryAfter = Number(res.headers.get("retry-after") ?? "60");
|
|
324
|
-
throw new RateLimitError(retryAfter);
|
|
325
|
-
}
|
|
326
|
-
let envelope = null;
|
|
327
|
-
try {
|
|
328
|
-
envelope = await res.json();
|
|
329
|
-
} catch {
|
|
330
|
-
}
|
|
331
|
-
if (!res.ok || !envelope?.success) {
|
|
332
|
-
const message = envelope?.error ?? `Request failed (${res.status})`;
|
|
333
|
-
throw new PerSQLError(res.status, message, envelope?.errorDetail);
|
|
334
|
-
}
|
|
335
|
-
return envelope.data;
|
|
336
|
-
}
|
|
337
|
-
/** @internal — raw fetch returning the underlying Response (used by blob GET). */
|
|
354
|
+
/** @internal — raw fetch returning the underlying Response. */
|
|
338
355
|
fetchRaw(method, path) {
|
|
339
356
|
return this._fetch(`${this.baseURL}${path}`, {
|
|
340
357
|
method,
|
|
341
358
|
headers: { Authorization: `Bearer ${this.token}` }
|
|
342
359
|
});
|
|
343
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* File a support ticket directly from your SDK code. Tickets land in
|
|
363
|
+
* the customer console at `/support`; staff reply from the admin
|
|
364
|
+
* app. Useful for agents and background jobs that hit a PerSQL
|
|
365
|
+
* error and want a human to see it.
|
|
366
|
+
*
|
|
367
|
+
* ```ts
|
|
368
|
+
* try {
|
|
369
|
+
* await db.query("UPDATE ...");
|
|
370
|
+
* } catch (err) {
|
|
371
|
+
* await persql.support.createTicket({
|
|
372
|
+
* subject: "Branch merge keeps 500ing",
|
|
373
|
+
* body: "Repro: ...",
|
|
374
|
+
* error: err,
|
|
375
|
+
* });
|
|
376
|
+
* }
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
get support() {
|
|
380
|
+
return new SupportClient(this);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
function errorContextFor(err) {
|
|
384
|
+
if (err instanceof PerSQLError) {
|
|
385
|
+
return {
|
|
386
|
+
errorName: err.name,
|
|
387
|
+
errorMessage: err.message,
|
|
388
|
+
status: err.status,
|
|
389
|
+
detail: err.detail ?? null
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (err instanceof Error) {
|
|
393
|
+
return { errorName: err.name, errorMessage: err.message };
|
|
394
|
+
}
|
|
395
|
+
if (err !== void 0 && err !== null) {
|
|
396
|
+
return { errorMessage: String(err) };
|
|
397
|
+
}
|
|
398
|
+
return {};
|
|
399
|
+
}
|
|
400
|
+
var SupportClient = class {
|
|
401
|
+
constructor(client) {
|
|
402
|
+
this.client = client;
|
|
403
|
+
}
|
|
404
|
+
async createTicket(opts) {
|
|
405
|
+
if (this.client.local) {
|
|
406
|
+
throw new Error("support.createTicket is not supported in local mode");
|
|
407
|
+
}
|
|
408
|
+
const errorContext = {
|
|
409
|
+
...errorContextFor(opts.error),
|
|
410
|
+
...opts.errorContext ?? {}
|
|
411
|
+
};
|
|
412
|
+
return this.client.request(
|
|
413
|
+
"POST",
|
|
414
|
+
"/v1/support/tickets",
|
|
415
|
+
{
|
|
416
|
+
subject: opts.subject,
|
|
417
|
+
body: opts.body,
|
|
418
|
+
errorContext: Object.keys(errorContext).length > 0 ? errorContext : void 0
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
}
|
|
344
422
|
};
|
|
345
423
|
var PerSQLDatabase = class {
|
|
346
424
|
constructor(client, namespace, slug) {
|
|
@@ -394,6 +472,25 @@ var PerSQLDatabase = class {
|
|
|
394
472
|
transaction(statements, options = {}) {
|
|
395
473
|
return this.batch(statements, { ...options, transaction: true });
|
|
396
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* Mint a short-lived session JWT for an end user. The server (which
|
|
477
|
+
* holds the bearer token) calls this and hands the resulting token
|
|
478
|
+
* to an untrusted client (browser, mobile app, agent run). The
|
|
479
|
+
* client uses the JWT to call this database's published `/p/*`
|
|
480
|
+
* endpoints on behalf of `userId`. TTL defaults to 1 hour; max 24h.
|
|
481
|
+
*/
|
|
482
|
+
async createSession(opts) {
|
|
483
|
+
if (this.client.local) {
|
|
484
|
+
throw new Error("createSession is not supported in local mode");
|
|
485
|
+
}
|
|
486
|
+
return this.client.request("POST", "/v1/sessions", {
|
|
487
|
+
namespaceSlug: this.namespace,
|
|
488
|
+
databaseSlug: this.slug,
|
|
489
|
+
userId: opts.userId,
|
|
490
|
+
email: opts.email,
|
|
491
|
+
expiresIn: opts.expiresIn
|
|
492
|
+
});
|
|
493
|
+
}
|
|
397
494
|
/**
|
|
398
495
|
* Engine telemetry — recent /v1/query and /v1/batch calls issued
|
|
399
496
|
* against this database. Backed by the in-DO `_persql_meta_query_log`
|
|
@@ -515,6 +612,61 @@ var PerSQLDatabase = class {
|
|
|
515
612
|
if (this.client.local) return this.client.local.explain(sql, params);
|
|
516
613
|
return this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/explain`, { sql, params });
|
|
517
614
|
}
|
|
615
|
+
/**
|
|
616
|
+
* Parse + bind-check a SQL statement without executing it. Faster
|
|
617
|
+
* and cheaper than running the query just to catch unknown columns
|
|
618
|
+
* or wrong param counts.
|
|
619
|
+
*
|
|
620
|
+
* Returns `{ ok: true }` on success or `{ ok: false, error, errorDetail }`
|
|
621
|
+
* with the same `errorDetail` envelope as `PerSQLError.detail`.
|
|
622
|
+
*/
|
|
623
|
+
async validate(sql, params = []) {
|
|
624
|
+
if (this.client.local) {
|
|
625
|
+
throw new Error("PerSQL: validate is not available in local mode.");
|
|
626
|
+
}
|
|
627
|
+
const data = await this.client.request(
|
|
628
|
+
"POST",
|
|
629
|
+
`/v1/db/${this.namespace}/${this.slug}/validate`,
|
|
630
|
+
{ sql, params }
|
|
631
|
+
);
|
|
632
|
+
return data;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Compact text rendering of the database's schema — one line per
|
|
636
|
+
* table with columns, types, PKs, and FK arrows inlined. Designed
|
|
637
|
+
* to stuff into an LLM prompt with minimum token overhead. See
|
|
638
|
+
* `db.describe()` for the structured form.
|
|
639
|
+
*/
|
|
640
|
+
async describeCompact() {
|
|
641
|
+
if (this.client.local) {
|
|
642
|
+
throw new Error("PerSQL: describeCompact is not available in local mode.");
|
|
643
|
+
}
|
|
644
|
+
const data = await this.client.request(
|
|
645
|
+
"GET",
|
|
646
|
+
`/v1/db/${this.namespace}/${this.slug}/describe?format=compact`
|
|
647
|
+
);
|
|
648
|
+
return data.text;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* First N rows of a table plus per-column stats (null count,
|
|
652
|
+
* distinct count, min, max). One call to size up an unfamiliar
|
|
653
|
+
* table — replaces a hand-rolled `SELECT *`, `COUNT(*)`, and
|
|
654
|
+
* `PRAGMA table_info` sequence.
|
|
655
|
+
*
|
|
656
|
+
* `n` defaults to 10 and is capped at 100.
|
|
657
|
+
*/
|
|
658
|
+
async sampleTable(table, opts = {}) {
|
|
659
|
+
if (this.client.local) {
|
|
660
|
+
throw new Error("PerSQL: sampleTable is not available in local mode.");
|
|
661
|
+
}
|
|
662
|
+
const qs = opts.n ? `?n=${Math.max(1, Math.min(100, opts.n | 0))}` : "";
|
|
663
|
+
return this.client.request(
|
|
664
|
+
"GET",
|
|
665
|
+
`/v1/db/${this.namespace}/${this.slug}/tables/${encodeURIComponent(
|
|
666
|
+
table
|
|
667
|
+
)}/sample${qs}`
|
|
668
|
+
);
|
|
669
|
+
}
|
|
518
670
|
/**
|
|
519
671
|
* Introspect the database schema. Returns one entry per user table
|
|
520
672
|
* with column definitions, suitable for codegen tools (the
|
|
@@ -538,20 +690,6 @@ var PerSQLDatabase = class {
|
|
|
538
690
|
}
|
|
539
691
|
return out;
|
|
540
692
|
}
|
|
541
|
-
/**
|
|
542
|
-
* Per-database semantic search via Cloudflare Vectorize. Use
|
|
543
|
-
* `vectors.upsert` to embed and store rows; `vectors.query` to
|
|
544
|
-
* retrieve the most similar by free text. Embeddings are computed
|
|
545
|
-
* server-side using bge-base-en-v1.5 (768 dim, cosine distance).
|
|
546
|
-
*/
|
|
547
|
-
get vectors() {
|
|
548
|
-
if (this.client.local) {
|
|
549
|
-
throw new Error(
|
|
550
|
-
"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."
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
return new PerSQLVectors(this.client, this.namespace, this.slug);
|
|
554
|
-
}
|
|
555
693
|
/**
|
|
556
694
|
* Manage preview/PR-style branches of this database. Each branch is
|
|
557
695
|
* its own DO with its own SQLite file; create-or-reset by ref is
|
|
@@ -580,6 +718,19 @@ var PerSQLDatabase = class {
|
|
|
580
718
|
}
|
|
581
719
|
return new PerSQLApprovals(this.client, this.namespace, this.slug);
|
|
582
720
|
}
|
|
721
|
+
/**
|
|
722
|
+
* Manage the require_approval / deny rules for this database. Reads
|
|
723
|
+
* are open to any bearer with read access; create + delete require
|
|
724
|
+
* an admin-role bearer token.
|
|
725
|
+
*/
|
|
726
|
+
get approvalRules() {
|
|
727
|
+
if (this.client.local) {
|
|
728
|
+
throw new Error(
|
|
729
|
+
"PerSQL: approval rules live on the server; local mode does not enforce them."
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
return new PerSQLApprovalRules(this.client, this.namespace, this.slug);
|
|
733
|
+
}
|
|
583
734
|
/**
|
|
584
735
|
* Pre-flight a write before running it. `propose()` validates the
|
|
585
736
|
* SQL via EXPLAIN, estimates affected rows, and returns a single-use
|
|
@@ -591,20 +742,6 @@ var PerSQLDatabase = class {
|
|
|
591
742
|
get proposals() {
|
|
592
743
|
return new PerSQLProposals(this.client, this.namespace, this.slug);
|
|
593
744
|
}
|
|
594
|
-
/**
|
|
595
|
-
* Per-database BLOB storage backed by R2. Use this for anything
|
|
596
|
-
* larger than a SQLite cell (images, PDFs, model weights). Each
|
|
597
|
-
* database has its own private namespace; keys may be hierarchical
|
|
598
|
-
* (`avatars/2025/foo.jpg`) but never start with `/`.
|
|
599
|
-
*/
|
|
600
|
-
get blob() {
|
|
601
|
-
if (this.client.local) {
|
|
602
|
-
throw new Error(
|
|
603
|
-
"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."
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
return new PerSQLBlob(this.client, this.namespace, this.slug);
|
|
607
|
-
}
|
|
608
745
|
/**
|
|
609
746
|
* Subscribe to row-changes via WebSocket — the SQL equivalent of
|
|
610
747
|
* Postgres `LISTEN`. The callback fires once per write that
|
|
@@ -665,6 +802,55 @@ var PerSQLDatabase = class {
|
|
|
665
802
|
}
|
|
666
803
|
};
|
|
667
804
|
}
|
|
805
|
+
/**
|
|
806
|
+
* Long-poll for row-changes on this database. Returns immediately
|
|
807
|
+
* if changes newer than `since` are already buffered; otherwise
|
|
808
|
+
* blocks server-side for up to `waitMs` (default 25s, max 25s).
|
|
809
|
+
*
|
|
810
|
+
* Most callers want `changes()` instead — an async iterator that
|
|
811
|
+
* loops `waitForChanges` forever, surfacing each batch as it
|
|
812
|
+
* arrives.
|
|
813
|
+
*/
|
|
814
|
+
async waitForChanges(opts = {}) {
|
|
815
|
+
if (this.client.local) {
|
|
816
|
+
throw new Error(
|
|
817
|
+
"PerSQL: waitForChanges requires the DO change ring \u2014 not available in local mode."
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
const qs = new URLSearchParams();
|
|
821
|
+
if (typeof opts.since === "number") qs.set("since", String(opts.since));
|
|
822
|
+
if (typeof opts.waitMs === "number") qs.set("waitMs", String(opts.waitMs));
|
|
823
|
+
if (opts.tables && opts.tables.length > 0) qs.set("tables", opts.tables.join(","));
|
|
824
|
+
const q = qs.toString();
|
|
825
|
+
return this.client.request(
|
|
826
|
+
"GET",
|
|
827
|
+
`/v1/db/${this.namespace}/${this.slug}/changes${q ? `?${q}` : ""}`
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Async-iterator change feed. Loops `waitForChanges` forever and
|
|
832
|
+
* yields one batch per server response. Pass an `AbortSignal` to
|
|
833
|
+
* stop the loop cleanly.
|
|
834
|
+
*
|
|
835
|
+
* ```ts
|
|
836
|
+
* const ctl = new AbortController();
|
|
837
|
+
* for await (const batch of db.changes({ signal: ctl.signal })) {
|
|
838
|
+
* for (const c of batch.changes) console.log(c.kind, c.table);
|
|
839
|
+
* }
|
|
840
|
+
* ```
|
|
841
|
+
*/
|
|
842
|
+
async *changes(opts = {}) {
|
|
843
|
+
let cursor = opts.since ?? 0;
|
|
844
|
+
while (!opts.signal?.aborted) {
|
|
845
|
+
const batch = await this.waitForChanges({
|
|
846
|
+
since: cursor,
|
|
847
|
+
waitMs: opts.waitMs,
|
|
848
|
+
tables: opts.tables
|
|
849
|
+
});
|
|
850
|
+
cursor = batch.cursor;
|
|
851
|
+
yield batch;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
668
854
|
/**
|
|
669
855
|
* Returns the callback shape `drizzle-orm/sqlite-proxy` expects.
|
|
670
856
|
* Pair with `drizzle()` from that module to get a typed,
|
|
@@ -692,42 +878,64 @@ var PerSQLDatabase = class {
|
|
|
692
878
|
return callback;
|
|
693
879
|
}
|
|
694
880
|
/**
|
|
695
|
-
* Returns a tool definition
|
|
696
|
-
*
|
|
881
|
+
* Returns a single SQL-query tool definition in every shape PerSQL
|
|
882
|
+
* supports (Anthropic, OpenAI Chat, Vercel AI SDK, Mastra, LangChain,
|
|
883
|
+
* OpenAI Agents SDK). Same input contract as `asTools()` — the only
|
|
884
|
+
* difference is one fat `query` tool instead of typed-per-table tools.
|
|
885
|
+
*
|
|
886
|
+
* Pair the format-specific fields with `runTool` (or use the bundled
|
|
887
|
+
* `execute`/`invoke` callbacks, which call `runTool` internally).
|
|
697
888
|
*/
|
|
698
889
|
asTool(name = "persql_query") {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
type: "array",
|
|
709
|
-
items: {},
|
|
710
|
-
description: "Positional parameters for the SQL statement"
|
|
711
|
-
}
|
|
712
|
-
},
|
|
713
|
-
required: ["sql"]
|
|
890
|
+
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.`;
|
|
891
|
+
const inputSchema = {
|
|
892
|
+
type: "object",
|
|
893
|
+
properties: {
|
|
894
|
+
sql: { type: "string", description: "SQLite SQL statement" },
|
|
895
|
+
params: {
|
|
896
|
+
type: "array",
|
|
897
|
+
items: {},
|
|
898
|
+
description: "Positional parameters for the SQL statement"
|
|
714
899
|
}
|
|
715
900
|
},
|
|
901
|
+
required: ["sql"],
|
|
902
|
+
// OpenAI strict function-calling rejects a schema without this; the
|
|
903
|
+
// per-table asTools() schemas already set it. Keep them consistent.
|
|
904
|
+
additionalProperties: false
|
|
905
|
+
};
|
|
906
|
+
const execute = (input) => this.runTool({
|
|
907
|
+
sql: String(input.sql ?? ""),
|
|
908
|
+
params: Array.isArray(input.params) ? input.params : []
|
|
909
|
+
});
|
|
910
|
+
return {
|
|
911
|
+
anthropic: { name, description, input_schema: inputSchema },
|
|
716
912
|
openai: {
|
|
717
913
|
type: "function",
|
|
718
|
-
function: {
|
|
914
|
+
function: { name, description, parameters: inputSchema }
|
|
915
|
+
},
|
|
916
|
+
aiSdk: () => ({
|
|
917
|
+
[name]: { description, inputSchema, execute }
|
|
918
|
+
}),
|
|
919
|
+
mastra: () => ({
|
|
920
|
+
[name]: {
|
|
921
|
+
id: name,
|
|
922
|
+
description,
|
|
923
|
+
inputSchema,
|
|
924
|
+
execute: ({ context }) => execute(context)
|
|
925
|
+
}
|
|
926
|
+
}),
|
|
927
|
+
langchain: () => [
|
|
928
|
+
{ name, description, schema: inputSchema, invoke: execute }
|
|
929
|
+
],
|
|
930
|
+
openaiAgents: () => [
|
|
931
|
+
{
|
|
932
|
+
type: "function",
|
|
719
933
|
name,
|
|
720
|
-
description
|
|
721
|
-
parameters:
|
|
722
|
-
|
|
723
|
-
properties: {
|
|
724
|
-
sql: { type: "string" },
|
|
725
|
-
params: { type: "array", items: {} }
|
|
726
|
-
},
|
|
727
|
-
required: ["sql"]
|
|
728
|
-
}
|
|
934
|
+
description,
|
|
935
|
+
parameters: inputSchema,
|
|
936
|
+
invoke: execute
|
|
729
937
|
}
|
|
730
|
-
|
|
938
|
+
]
|
|
731
939
|
};
|
|
732
940
|
}
|
|
733
941
|
/**
|
|
@@ -1499,106 +1707,148 @@ function sanitizeToolPart(s) {
|
|
|
1499
1707
|
function quoteIdent(s) {
|
|
1500
1708
|
return `"${s.replace(/"/g, '""')}"`;
|
|
1501
1709
|
}
|
|
1502
|
-
var
|
|
1710
|
+
var PerSQLApprovals = class {
|
|
1503
1711
|
constructor(client, namespace, slug) {
|
|
1504
1712
|
this.client = client;
|
|
1505
1713
|
this.namespace = namespace;
|
|
1506
1714
|
this.slug = slug;
|
|
1507
1715
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1716
|
+
// Tokens this client has seen via subscribe — used to dedupe poll
|
|
1717
|
+
// results so onApprovalRequired fires once per token regardless of
|
|
1718
|
+
// how many subscribe ticks observe it.
|
|
1719
|
+
seen = /* @__PURE__ */ new Map();
|
|
1720
|
+
/**
|
|
1721
|
+
* Look up the status of an approval token without consuming it. The
|
|
1722
|
+
* bearer must match the one that minted the original 403; cross-bearer
|
|
1723
|
+
* lookup returns 403.
|
|
1724
|
+
*/
|
|
1725
|
+
async get(approvalToken) {
|
|
1726
|
+
return this.client.request(
|
|
1727
|
+
"GET",
|
|
1728
|
+
`/v1/db/${this.namespace}/${this.slug}/approval/${encodeURIComponent(
|
|
1729
|
+
approvalToken
|
|
1730
|
+
)}`
|
|
1516
1731
|
);
|
|
1517
1732
|
}
|
|
1518
|
-
/**
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1733
|
+
/**
|
|
1734
|
+
* Poll until a reviewer decides (or the token expires). Resolves with
|
|
1735
|
+
* the final status — `approved` is your green light for `redeem()`.
|
|
1736
|
+
*/
|
|
1737
|
+
async poll(approvalToken, opts = {}) {
|
|
1738
|
+
const intervalMs = Math.max(250, opts.intervalMs ?? 2e3);
|
|
1739
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 10 * 60 * 1e3);
|
|
1740
|
+
while (true) {
|
|
1741
|
+
if (opts.signal?.aborted) {
|
|
1742
|
+
throw new Error("approval poll aborted");
|
|
1743
|
+
}
|
|
1744
|
+
const status = await this.get(approvalToken);
|
|
1745
|
+
if (status.status !== "pending") return status;
|
|
1746
|
+
if (Date.now() > deadline) return status;
|
|
1747
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1525
1748
|
}
|
|
1526
|
-
return res;
|
|
1527
1749
|
}
|
|
1528
|
-
/**
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1750
|
+
/**
|
|
1751
|
+
* Long-running poll subscription for new approval events on this
|
|
1752
|
+
* database. Returns a `stop()` handle. Webhook delivery is the push
|
|
1753
|
+
* path (events `approval_required` / `approval_resolved`); subscribe
|
|
1754
|
+
* is the pull fallback for clients that can't host a webhook.
|
|
1755
|
+
*
|
|
1756
|
+
* Today this iterates over a known set of tokens supplied by the
|
|
1757
|
+
* caller; an SDK-only client that wants discovery should pair this
|
|
1758
|
+
* with the webhook path. Pass tokens you already hold (e.g. from a
|
|
1759
|
+
* prior `ApprovalRequiredError`).
|
|
1760
|
+
*/
|
|
1761
|
+
subscribe(tokens, opts) {
|
|
1762
|
+
const intervalMs = Math.max(1e3, opts.intervalMs ?? 5e3);
|
|
1763
|
+
let cancelled = false;
|
|
1764
|
+
const onAbort = () => {
|
|
1765
|
+
cancelled = true;
|
|
1766
|
+
};
|
|
1767
|
+
opts.signal?.addEventListener("abort", onAbort);
|
|
1768
|
+
const tick = async () => {
|
|
1769
|
+
for (const token of tokens) {
|
|
1770
|
+
if (cancelled) return;
|
|
1771
|
+
try {
|
|
1772
|
+
const status = await this.get(token);
|
|
1773
|
+
const prev = this.seen.get(token);
|
|
1774
|
+
if (status.status !== prev) {
|
|
1775
|
+
this.seen.set(token, status.status);
|
|
1776
|
+
const event = {
|
|
1777
|
+
approvalToken: token,
|
|
1778
|
+
status: status.status,
|
|
1779
|
+
hits: status.hits
|
|
1780
|
+
};
|
|
1781
|
+
if (status.status === "pending") {
|
|
1782
|
+
opts.onApprovalRequired?.(event);
|
|
1783
|
+
} else {
|
|
1784
|
+
opts.onApprovalResolved?.(event);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
} catch (e) {
|
|
1788
|
+
opts.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
const id = setInterval(() => {
|
|
1793
|
+
void tick();
|
|
1794
|
+
}, intervalMs);
|
|
1795
|
+
void tick();
|
|
1796
|
+
return {
|
|
1797
|
+
stop: () => {
|
|
1798
|
+
cancelled = true;
|
|
1799
|
+
clearInterval(id);
|
|
1800
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1532
1803
|
}
|
|
1533
|
-
/**
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
const
|
|
1540
|
-
|
|
1541
|
-
|
|
1804
|
+
/**
|
|
1805
|
+
* Redeem an approved approvalToken. The bearer must be the same one
|
|
1806
|
+
* that minted it, and the request must target the same database.
|
|
1807
|
+
* Returns the result of the originally-blocked query or batch.
|
|
1808
|
+
*/
|
|
1809
|
+
async redeem(approvalToken) {
|
|
1810
|
+
const raw = await this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/redeem_approval`, {
|
|
1811
|
+
approvalToken
|
|
1812
|
+
});
|
|
1813
|
+
if (Array.isArray(raw)) {
|
|
1814
|
+
return raw.map((r) => ({
|
|
1815
|
+
...r,
|
|
1816
|
+
data: rowsToObjects(r.columns, r.rows)
|
|
1817
|
+
}));
|
|
1818
|
+
}
|
|
1819
|
+
return { ...raw, data: rowsToObjects(raw.columns, raw.rows) };
|
|
1542
1820
|
}
|
|
1543
1821
|
};
|
|
1544
|
-
|
|
1545
|
-
return key.split("/").map(encodeURIComponent).join("/");
|
|
1546
|
-
}
|
|
1547
|
-
var PerSQLVectors = class {
|
|
1822
|
+
var PerSQLApprovalRules = class {
|
|
1548
1823
|
constructor(client, namespace, slug) {
|
|
1549
1824
|
this.client = client;
|
|
1550
1825
|
this.namespace = namespace;
|
|
1551
1826
|
this.slug = slug;
|
|
1552
1827
|
}
|
|
1553
|
-
|
|
1554
|
-
async upsert(items) {
|
|
1828
|
+
async list() {
|
|
1555
1829
|
return this.client.request(
|
|
1556
|
-
"
|
|
1557
|
-
`/v1/db/${this.namespace}/${this.slug}/
|
|
1558
|
-
{ items }
|
|
1830
|
+
"GET",
|
|
1831
|
+
`/v1/db/${this.namespace}/${this.slug}/approval-rules`
|
|
1559
1832
|
);
|
|
1560
1833
|
}
|
|
1561
|
-
/**
|
|
1562
|
-
async
|
|
1834
|
+
/** Requires an admin-role bearer token. */
|
|
1835
|
+
async create(input) {
|
|
1563
1836
|
return this.client.request(
|
|
1564
1837
|
"POST",
|
|
1565
|
-
`/v1/db/${this.namespace}/${this.slug}/
|
|
1566
|
-
|
|
1838
|
+
`/v1/db/${this.namespace}/${this.slug}/approval-rules`,
|
|
1839
|
+
input
|
|
1567
1840
|
);
|
|
1568
1841
|
}
|
|
1569
|
-
/**
|
|
1570
|
-
async delete(
|
|
1842
|
+
/** Requires an admin-role bearer token. */
|
|
1843
|
+
async delete(ruleId) {
|
|
1571
1844
|
return this.client.request(
|
|
1572
1845
|
"DELETE",
|
|
1573
|
-
`/v1/db/${this.namespace}/${this.slug}/
|
|
1574
|
-
|
|
1846
|
+
`/v1/db/${this.namespace}/${this.slug}/approval-rules/${encodeURIComponent(
|
|
1847
|
+
ruleId
|
|
1848
|
+
)}`
|
|
1575
1849
|
);
|
|
1576
1850
|
}
|
|
1577
1851
|
};
|
|
1578
|
-
var PerSQLApprovals = class {
|
|
1579
|
-
constructor(client, namespace, slug) {
|
|
1580
|
-
this.client = client;
|
|
1581
|
-
this.namespace = namespace;
|
|
1582
|
-
this.slug = slug;
|
|
1583
|
-
}
|
|
1584
|
-
/**
|
|
1585
|
-
* Redeem an approved approvalToken. The bearer must be the same one
|
|
1586
|
-
* that minted it, and the request must target the same database.
|
|
1587
|
-
* Returns the result of the originally-blocked query or batch.
|
|
1588
|
-
*/
|
|
1589
|
-
async redeem(approvalToken) {
|
|
1590
|
-
const raw = await this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/redeem_approval`, {
|
|
1591
|
-
approvalToken
|
|
1592
|
-
});
|
|
1593
|
-
if (Array.isArray(raw)) {
|
|
1594
|
-
return raw.map((r) => ({
|
|
1595
|
-
...r,
|
|
1596
|
-
data: rowsToObjects(r.columns, r.rows)
|
|
1597
|
-
}));
|
|
1598
|
-
}
|
|
1599
|
-
return { ...raw, data: rowsToObjects(raw.columns, raw.rows) };
|
|
1600
|
-
}
|
|
1601
|
-
};
|
|
1602
1852
|
var PerSQLProposals = class {
|
|
1603
1853
|
constructor(client, namespace, slug) {
|
|
1604
1854
|
this.client = client;
|
|
@@ -1610,6 +1860,11 @@ var PerSQLProposals = class {
|
|
|
1610
1860
|
* rows, and returns a single-use `executionToken` valid for 10 min
|
|
1611
1861
|
* (override with `ttlSec`, max 1 hour). Nothing is mutated until
|
|
1612
1862
|
* `apply()` is called.
|
|
1863
|
+
*
|
|
1864
|
+
* Local mode keeps the token in-process: single-use still holds, but
|
|
1865
|
+
* `estimatedAffectedRows` is always `null` (no server estimator) and a
|
|
1866
|
+
* stale/unknown token throws a plain `Error` rather than the HTTP
|
|
1867
|
+
* 404/403 of the server path.
|
|
1613
1868
|
*/
|
|
1614
1869
|
async propose(sql, opts = {}) {
|
|
1615
1870
|
if (this.client.local) {
|
|
@@ -1760,13 +2015,13 @@ function rowsToObjects(columns, rows) {
|
|
|
1760
2015
|
0 && (module.exports = {
|
|
1761
2016
|
ApprovalRequiredError,
|
|
1762
2017
|
PerSQL,
|
|
2018
|
+
PerSQLApprovalRules,
|
|
1763
2019
|
PerSQLApprovals,
|
|
1764
|
-
PerSQLBlob,
|
|
1765
2020
|
PerSQLBranches,
|
|
1766
2021
|
PerSQLDatabase,
|
|
1767
2022
|
PerSQLError,
|
|
1768
2023
|
PerSQLProposals,
|
|
1769
|
-
|
|
1770
|
-
|
|
2024
|
+
RateLimitError,
|
|
2025
|
+
SupportClient
|
|
1771
2026
|
});
|
|
1772
2027
|
//# sourceMappingURL=index.cjs.map
|