@persql/sdk 0.1.0 → 1.0.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-ZRZANHPI.js} +357 -159
- package/dist/chunk-ZRZANHPI.js.map +1 -0
- package/dist/cli.cjs +354 -156
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +360 -162
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +263 -128
- package/dist/index.d.ts +263 -128
- package/dist/index.js +7 -7
- package/package.json +1 -1
- package/dist/chunk-CDNTQOBK.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -5,6 +5,28 @@
|
|
|
5
5
|
var import_promises = require("fs/promises");
|
|
6
6
|
|
|
7
7
|
// src/local.ts
|
|
8
|
+
function checkLocalExpect(e, r) {
|
|
9
|
+
const rows = r.rows.length;
|
|
10
|
+
if (e.rows != null && rows !== e.rows) {
|
|
11
|
+
return `expected rows=${e.rows}, got ${rows}`;
|
|
12
|
+
}
|
|
13
|
+
if (e.rowsAtLeast != null && rows < e.rowsAtLeast) {
|
|
14
|
+
return `expected rows>=${e.rowsAtLeast}, got ${rows}`;
|
|
15
|
+
}
|
|
16
|
+
if (e.rowsAtMost != null && rows > e.rowsAtMost) {
|
|
17
|
+
return `expected rows<=${e.rowsAtMost}, got ${rows}`;
|
|
18
|
+
}
|
|
19
|
+
if (e.rowsWritten != null && r.rowsWritten !== e.rowsWritten) {
|
|
20
|
+
return `expected rowsWritten=${e.rowsWritten}, got ${r.rowsWritten}`;
|
|
21
|
+
}
|
|
22
|
+
if (e.rowsWrittenAtLeast != null && r.rowsWritten < e.rowsWrittenAtLeast) {
|
|
23
|
+
return `expected rowsWritten>=${e.rowsWrittenAtLeast}, got ${r.rowsWritten}`;
|
|
24
|
+
}
|
|
25
|
+
if (e.rowsWrittenAtMost != null && r.rowsWritten > e.rowsWrittenAtMost) {
|
|
26
|
+
return `expected rowsWritten<=${e.rowsWrittenAtMost}, got ${r.rowsWritten}`;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
8
30
|
var PROPOSAL_TTL_DEFAULT_SEC = 600;
|
|
9
31
|
var PROPOSAL_TTL_MAX_SEC = 3600;
|
|
10
32
|
function newLocalProposalId() {
|
|
@@ -43,7 +65,16 @@ var LocalDriver = class {
|
|
|
43
65
|
}
|
|
44
66
|
async batch(statements, transaction) {
|
|
45
67
|
const db = await this.open();
|
|
46
|
-
const exec = () => statements.map((s) =>
|
|
68
|
+
const exec = () => statements.map((s, i) => {
|
|
69
|
+
const r = runOne(db, s.sql, s.params ?? []);
|
|
70
|
+
if (s.expect) {
|
|
71
|
+
const violation = checkLocalExpect(s.expect, r);
|
|
72
|
+
if (violation) {
|
|
73
|
+
throw new Error(`Assertion failed on statement ${i}: ${violation}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return r;
|
|
77
|
+
});
|
|
47
78
|
if (transaction) return db.transaction(exec)();
|
|
48
79
|
return exec();
|
|
49
80
|
}
|
|
@@ -205,6 +236,19 @@ var PerSQL = class _PerSQL {
|
|
|
205
236
|
close() {
|
|
206
237
|
this.local?.close();
|
|
207
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Resolve the bearer token's namespace + live usage. Use this once
|
|
241
|
+
* per agent run so you know the slug to put in URLs and how much
|
|
242
|
+
* monthly budget / prepaid balance remains before a 402.
|
|
243
|
+
*
|
|
244
|
+
* Throws in local mode — no server, nothing to ask.
|
|
245
|
+
*/
|
|
246
|
+
async me() {
|
|
247
|
+
if (this.local) {
|
|
248
|
+
throw new Error("PerSQL: me() is not available in local mode.");
|
|
249
|
+
}
|
|
250
|
+
return this.request("GET", "/v1/me");
|
|
251
|
+
}
|
|
208
252
|
/**
|
|
209
253
|
* Redeem a handoff token (`phand_…`) for a regular PerSQL client
|
|
210
254
|
* scoped to the handed-off (database, branch, role). Single use:
|
|
@@ -278,40 +322,74 @@ var PerSQL = class _PerSQL {
|
|
|
278
322
|
}
|
|
279
323
|
return envelope.data;
|
|
280
324
|
}
|
|
281
|
-
/** @internal — raw
|
|
282
|
-
async requestRaw(method, path, body, contentType) {
|
|
283
|
-
const res = await this._fetch(`${this.baseURL}${path}`, {
|
|
284
|
-
method,
|
|
285
|
-
headers: {
|
|
286
|
-
Authorization: `Bearer ${this.token}`,
|
|
287
|
-
"Content-Type": contentType ?? "application/octet-stream"
|
|
288
|
-
},
|
|
289
|
-
body,
|
|
290
|
-
// @ts-expect-error — duplex is required by Node fetch for streaming bodies
|
|
291
|
-
duplex: "half"
|
|
292
|
-
});
|
|
293
|
-
if (res.status === 429) {
|
|
294
|
-
const retryAfter = Number(res.headers.get("retry-after") ?? "60");
|
|
295
|
-
throw new RateLimitError(retryAfter);
|
|
296
|
-
}
|
|
297
|
-
let envelope = null;
|
|
298
|
-
try {
|
|
299
|
-
envelope = await res.json();
|
|
300
|
-
} catch {
|
|
301
|
-
}
|
|
302
|
-
if (!res.ok || !envelope?.success) {
|
|
303
|
-
const message = envelope?.error ?? `Request failed (${res.status})`;
|
|
304
|
-
throw new PerSQLError(res.status, message, envelope?.errorDetail);
|
|
305
|
-
}
|
|
306
|
-
return envelope.data;
|
|
307
|
-
}
|
|
308
|
-
/** @internal — raw fetch returning the underlying Response (used by blob GET). */
|
|
325
|
+
/** @internal — raw fetch returning the underlying Response. */
|
|
309
326
|
fetchRaw(method, path) {
|
|
310
327
|
return this._fetch(`${this.baseURL}${path}`, {
|
|
311
328
|
method,
|
|
312
329
|
headers: { Authorization: `Bearer ${this.token}` }
|
|
313
330
|
});
|
|
314
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* File a support ticket directly from your SDK code. Tickets land in
|
|
334
|
+
* the customer console at `/support`; staff reply from the admin
|
|
335
|
+
* app. Useful for agents and background jobs that hit a PerSQL
|
|
336
|
+
* error and want a human to see it.
|
|
337
|
+
*
|
|
338
|
+
* ```ts
|
|
339
|
+
* try {
|
|
340
|
+
* await db.query("UPDATE ...");
|
|
341
|
+
* } catch (err) {
|
|
342
|
+
* await persql.support.createTicket({
|
|
343
|
+
* subject: "Branch merge keeps 500ing",
|
|
344
|
+
* body: "Repro: ...",
|
|
345
|
+
* error: err,
|
|
346
|
+
* });
|
|
347
|
+
* }
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
get support() {
|
|
351
|
+
return new SupportClient(this);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
function errorContextFor(err) {
|
|
355
|
+
if (err instanceof PerSQLError) {
|
|
356
|
+
return {
|
|
357
|
+
errorName: err.name,
|
|
358
|
+
errorMessage: err.message,
|
|
359
|
+
status: err.status,
|
|
360
|
+
detail: err.detail ?? null
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (err instanceof Error) {
|
|
364
|
+
return { errorName: err.name, errorMessage: err.message };
|
|
365
|
+
}
|
|
366
|
+
if (err !== void 0 && err !== null) {
|
|
367
|
+
return { errorMessage: String(err) };
|
|
368
|
+
}
|
|
369
|
+
return {};
|
|
370
|
+
}
|
|
371
|
+
var SupportClient = class {
|
|
372
|
+
constructor(client) {
|
|
373
|
+
this.client = client;
|
|
374
|
+
}
|
|
375
|
+
async createTicket(opts) {
|
|
376
|
+
if (this.client.local) {
|
|
377
|
+
throw new Error("support.createTicket is not supported in local mode");
|
|
378
|
+
}
|
|
379
|
+
const errorContext = {
|
|
380
|
+
...errorContextFor(opts.error),
|
|
381
|
+
...opts.errorContext ?? {}
|
|
382
|
+
};
|
|
383
|
+
return this.client.request(
|
|
384
|
+
"POST",
|
|
385
|
+
"/v1/support/tickets",
|
|
386
|
+
{
|
|
387
|
+
subject: opts.subject,
|
|
388
|
+
body: opts.body,
|
|
389
|
+
errorContext: Object.keys(errorContext).length > 0 ? errorContext : void 0
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
}
|
|
315
393
|
};
|
|
316
394
|
var PerSQLDatabase = class {
|
|
317
395
|
constructor(client, namespace, slug) {
|
|
@@ -365,6 +443,25 @@ var PerSQLDatabase = class {
|
|
|
365
443
|
transaction(statements, options = {}) {
|
|
366
444
|
return this.batch(statements, { ...options, transaction: true });
|
|
367
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* Mint a short-lived session JWT for an end user. The server (which
|
|
448
|
+
* holds the bearer token) calls this and hands the resulting token
|
|
449
|
+
* to an untrusted client (browser, mobile app, agent run). The
|
|
450
|
+
* client uses the JWT to call this database's published `/p/*`
|
|
451
|
+
* endpoints on behalf of `userId`. TTL defaults to 1 hour; max 24h.
|
|
452
|
+
*/
|
|
453
|
+
async createSession(opts) {
|
|
454
|
+
if (this.client.local) {
|
|
455
|
+
throw new Error("createSession is not supported in local mode");
|
|
456
|
+
}
|
|
457
|
+
return this.client.request("POST", "/v1/sessions", {
|
|
458
|
+
namespaceSlug: this.namespace,
|
|
459
|
+
databaseSlug: this.slug,
|
|
460
|
+
userId: opts.userId,
|
|
461
|
+
email: opts.email,
|
|
462
|
+
expiresIn: opts.expiresIn
|
|
463
|
+
});
|
|
464
|
+
}
|
|
368
465
|
/**
|
|
369
466
|
* Engine telemetry — recent /v1/query and /v1/batch calls issued
|
|
370
467
|
* against this database. Backed by the in-DO `_persql_meta_query_log`
|
|
@@ -486,6 +583,61 @@ var PerSQLDatabase = class {
|
|
|
486
583
|
if (this.client.local) return this.client.local.explain(sql, params);
|
|
487
584
|
return this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/explain`, { sql, params });
|
|
488
585
|
}
|
|
586
|
+
/**
|
|
587
|
+
* Parse + bind-check a SQL statement without executing it. Faster
|
|
588
|
+
* and cheaper than running the query just to catch unknown columns
|
|
589
|
+
* or wrong param counts.
|
|
590
|
+
*
|
|
591
|
+
* Returns `{ ok: true }` on success or `{ ok: false, error, errorDetail }`
|
|
592
|
+
* with the same `errorDetail` envelope as `PerSQLError.detail`.
|
|
593
|
+
*/
|
|
594
|
+
async validate(sql, params = []) {
|
|
595
|
+
if (this.client.local) {
|
|
596
|
+
throw new Error("PerSQL: validate is not available in local mode.");
|
|
597
|
+
}
|
|
598
|
+
const data = await this.client.request(
|
|
599
|
+
"POST",
|
|
600
|
+
`/v1/db/${this.namespace}/${this.slug}/validate`,
|
|
601
|
+
{ sql, params }
|
|
602
|
+
);
|
|
603
|
+
return data;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Compact text rendering of the database's schema — one line per
|
|
607
|
+
* table with columns, types, PKs, and FK arrows inlined. Designed
|
|
608
|
+
* to stuff into an LLM prompt with minimum token overhead. See
|
|
609
|
+
* `db.describe()` for the structured form.
|
|
610
|
+
*/
|
|
611
|
+
async describeCompact() {
|
|
612
|
+
if (this.client.local) {
|
|
613
|
+
throw new Error("PerSQL: describeCompact is not available in local mode.");
|
|
614
|
+
}
|
|
615
|
+
const data = await this.client.request(
|
|
616
|
+
"GET",
|
|
617
|
+
`/v1/db/${this.namespace}/${this.slug}/describe?format=compact`
|
|
618
|
+
);
|
|
619
|
+
return data.text;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* First N rows of a table plus per-column stats (null count,
|
|
623
|
+
* distinct count, min, max). One call to size up an unfamiliar
|
|
624
|
+
* table — replaces a hand-rolled `SELECT *`, `COUNT(*)`, and
|
|
625
|
+
* `PRAGMA table_info` sequence.
|
|
626
|
+
*
|
|
627
|
+
* `n` defaults to 10 and is capped at 100.
|
|
628
|
+
*/
|
|
629
|
+
async sampleTable(table, opts = {}) {
|
|
630
|
+
if (this.client.local) {
|
|
631
|
+
throw new Error("PerSQL: sampleTable is not available in local mode.");
|
|
632
|
+
}
|
|
633
|
+
const qs = opts.n ? `?n=${Math.max(1, Math.min(100, opts.n | 0))}` : "";
|
|
634
|
+
return this.client.request(
|
|
635
|
+
"GET",
|
|
636
|
+
`/v1/db/${this.namespace}/${this.slug}/tables/${encodeURIComponent(
|
|
637
|
+
table
|
|
638
|
+
)}/sample${qs}`
|
|
639
|
+
);
|
|
640
|
+
}
|
|
489
641
|
/**
|
|
490
642
|
* Introspect the database schema. Returns one entry per user table
|
|
491
643
|
* with column definitions, suitable for codegen tools (the
|
|
@@ -509,20 +661,6 @@ var PerSQLDatabase = class {
|
|
|
509
661
|
}
|
|
510
662
|
return out;
|
|
511
663
|
}
|
|
512
|
-
/**
|
|
513
|
-
* Per-database semantic search via Cloudflare Vectorize. Use
|
|
514
|
-
* `vectors.upsert` to embed and store rows; `vectors.query` to
|
|
515
|
-
* retrieve the most similar by free text. Embeddings are computed
|
|
516
|
-
* server-side using bge-base-en-v1.5 (768 dim, cosine distance).
|
|
517
|
-
*/
|
|
518
|
-
get vectors() {
|
|
519
|
-
if (this.client.local) {
|
|
520
|
-
throw new Error(
|
|
521
|
-
"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."
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
return new PerSQLVectors(this.client, this.namespace, this.slug);
|
|
525
|
-
}
|
|
526
664
|
/**
|
|
527
665
|
* Manage preview/PR-style branches of this database. Each branch is
|
|
528
666
|
* its own DO with its own SQLite file; create-or-reset by ref is
|
|
@@ -551,6 +689,19 @@ var PerSQLDatabase = class {
|
|
|
551
689
|
}
|
|
552
690
|
return new PerSQLApprovals(this.client, this.namespace, this.slug);
|
|
553
691
|
}
|
|
692
|
+
/**
|
|
693
|
+
* Manage the require_approval / deny rules for this database. Reads
|
|
694
|
+
* are open to any bearer with read access; create + delete require
|
|
695
|
+
* an admin-role bearer token.
|
|
696
|
+
*/
|
|
697
|
+
get approvalRules() {
|
|
698
|
+
if (this.client.local) {
|
|
699
|
+
throw new Error(
|
|
700
|
+
"PerSQL: approval rules live on the server; local mode does not enforce them."
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
return new PerSQLApprovalRules(this.client, this.namespace, this.slug);
|
|
704
|
+
}
|
|
554
705
|
/**
|
|
555
706
|
* Pre-flight a write before running it. `propose()` validates the
|
|
556
707
|
* SQL via EXPLAIN, estimates affected rows, and returns a single-use
|
|
@@ -562,20 +713,6 @@ var PerSQLDatabase = class {
|
|
|
562
713
|
get proposals() {
|
|
563
714
|
return new PerSQLProposals(this.client, this.namespace, this.slug);
|
|
564
715
|
}
|
|
565
|
-
/**
|
|
566
|
-
* Per-database BLOB storage backed by R2. Use this for anything
|
|
567
|
-
* larger than a SQLite cell (images, PDFs, model weights). Each
|
|
568
|
-
* database has its own private namespace; keys may be hierarchical
|
|
569
|
-
* (`avatars/2025/foo.jpg`) but never start with `/`.
|
|
570
|
-
*/
|
|
571
|
-
get blob() {
|
|
572
|
-
if (this.client.local) {
|
|
573
|
-
throw new Error(
|
|
574
|
-
"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."
|
|
575
|
-
);
|
|
576
|
-
}
|
|
577
|
-
return new PerSQLBlob(this.client, this.namespace, this.slug);
|
|
578
|
-
}
|
|
579
716
|
/**
|
|
580
717
|
* Subscribe to row-changes via WebSocket — the SQL equivalent of
|
|
581
718
|
* Postgres `LISTEN`. The callback fires once per write that
|
|
@@ -663,42 +800,61 @@ var PerSQLDatabase = class {
|
|
|
663
800
|
return callback;
|
|
664
801
|
}
|
|
665
802
|
/**
|
|
666
|
-
* Returns a tool definition
|
|
667
|
-
*
|
|
803
|
+
* Returns a single SQL-query tool definition in every shape PerSQL
|
|
804
|
+
* supports (Anthropic, OpenAI Chat, Vercel AI SDK, Mastra, LangChain,
|
|
805
|
+
* OpenAI Agents SDK). Same input contract as `asTools()` — the only
|
|
806
|
+
* difference is one fat `query` tool instead of typed-per-table tools.
|
|
807
|
+
*
|
|
808
|
+
* Pair the format-specific fields with `runTool` (or use the bundled
|
|
809
|
+
* `execute`/`invoke` callbacks, which call `runTool` internally).
|
|
668
810
|
*/
|
|
669
811
|
asTool(name = "persql_query") {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
type: "array",
|
|
680
|
-
items: {},
|
|
681
|
-
description: "Positional parameters for the SQL statement"
|
|
682
|
-
}
|
|
683
|
-
},
|
|
684
|
-
required: ["sql"]
|
|
812
|
+
const description = `Run a SQL query against the PerSQL database "${this.namespace}/${this.slug}". Returns up to 1000 rows. Use parameter binding (?) to avoid injection.`;
|
|
813
|
+
const inputSchema = {
|
|
814
|
+
type: "object",
|
|
815
|
+
properties: {
|
|
816
|
+
sql: { type: "string", description: "SQLite SQL statement" },
|
|
817
|
+
params: {
|
|
818
|
+
type: "array",
|
|
819
|
+
items: {},
|
|
820
|
+
description: "Positional parameters for the SQL statement"
|
|
685
821
|
}
|
|
686
822
|
},
|
|
823
|
+
required: ["sql"]
|
|
824
|
+
};
|
|
825
|
+
const execute = (input) => this.runTool({
|
|
826
|
+
sql: String(input.sql ?? ""),
|
|
827
|
+
params: Array.isArray(input.params) ? input.params : []
|
|
828
|
+
});
|
|
829
|
+
return {
|
|
830
|
+
anthropic: { name, description, input_schema: inputSchema },
|
|
687
831
|
openai: {
|
|
688
832
|
type: "function",
|
|
689
|
-
function: {
|
|
833
|
+
function: { name, description, parameters: inputSchema }
|
|
834
|
+
},
|
|
835
|
+
aiSdk: () => ({
|
|
836
|
+
[name]: { description, inputSchema, execute }
|
|
837
|
+
}),
|
|
838
|
+
mastra: () => ({
|
|
839
|
+
[name]: {
|
|
840
|
+
id: name,
|
|
841
|
+
description,
|
|
842
|
+
inputSchema,
|
|
843
|
+
execute: ({ context }) => execute(context)
|
|
844
|
+
}
|
|
845
|
+
}),
|
|
846
|
+
langchain: () => [
|
|
847
|
+
{ name, description, schema: inputSchema, invoke: execute }
|
|
848
|
+
],
|
|
849
|
+
openaiAgents: () => [
|
|
850
|
+
{
|
|
851
|
+
type: "function",
|
|
690
852
|
name,
|
|
691
|
-
description
|
|
692
|
-
parameters:
|
|
693
|
-
|
|
694
|
-
properties: {
|
|
695
|
-
sql: { type: "string" },
|
|
696
|
-
params: { type: "array", items: {} }
|
|
697
|
-
},
|
|
698
|
-
required: ["sql"]
|
|
699
|
-
}
|
|
853
|
+
description,
|
|
854
|
+
parameters: inputSchema,
|
|
855
|
+
invoke: execute
|
|
700
856
|
}
|
|
701
|
-
|
|
857
|
+
]
|
|
702
858
|
};
|
|
703
859
|
}
|
|
704
860
|
/**
|
|
@@ -1470,106 +1626,148 @@ function sanitizeToolPart(s) {
|
|
|
1470
1626
|
function quoteIdent(s) {
|
|
1471
1627
|
return `"${s.replace(/"/g, '""')}"`;
|
|
1472
1628
|
}
|
|
1473
|
-
var
|
|
1629
|
+
var PerSQLApprovals = class {
|
|
1474
1630
|
constructor(client, namespace, slug) {
|
|
1475
1631
|
this.client = client;
|
|
1476
1632
|
this.namespace = namespace;
|
|
1477
1633
|
this.slug = slug;
|
|
1478
1634
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1635
|
+
// Tokens this client has seen via subscribe — used to dedupe poll
|
|
1636
|
+
// results so onApprovalRequired fires once per token regardless of
|
|
1637
|
+
// how many subscribe ticks observe it.
|
|
1638
|
+
seen = /* @__PURE__ */ new Map();
|
|
1639
|
+
/**
|
|
1640
|
+
* Look up the status of an approval token without consuming it. The
|
|
1641
|
+
* bearer must match the one that minted the original 403; cross-bearer
|
|
1642
|
+
* lookup returns 403.
|
|
1643
|
+
*/
|
|
1644
|
+
async get(approvalToken) {
|
|
1645
|
+
return this.client.request(
|
|
1646
|
+
"GET",
|
|
1647
|
+
`/v1/db/${this.namespace}/${this.slug}/approval/${encodeURIComponent(
|
|
1648
|
+
approvalToken
|
|
1649
|
+
)}`
|
|
1487
1650
|
);
|
|
1488
1651
|
}
|
|
1489
|
-
/**
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1652
|
+
/**
|
|
1653
|
+
* Poll until a reviewer decides (or the token expires). Resolves with
|
|
1654
|
+
* the final status — `approved` is your green light for `redeem()`.
|
|
1655
|
+
*/
|
|
1656
|
+
async poll(approvalToken, opts = {}) {
|
|
1657
|
+
const intervalMs = Math.max(250, opts.intervalMs ?? 2e3);
|
|
1658
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 10 * 60 * 1e3);
|
|
1659
|
+
while (true) {
|
|
1660
|
+
if (opts.signal?.aborted) {
|
|
1661
|
+
throw new Error("approval poll aborted");
|
|
1662
|
+
}
|
|
1663
|
+
const status = await this.get(approvalToken);
|
|
1664
|
+
if (status.status !== "pending") return status;
|
|
1665
|
+
if (Date.now() > deadline) return status;
|
|
1666
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1496
1667
|
}
|
|
1497
|
-
return res;
|
|
1498
1668
|
}
|
|
1499
|
-
/**
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1669
|
+
/**
|
|
1670
|
+
* Long-running poll subscription for new approval events on this
|
|
1671
|
+
* database. Returns a `stop()` handle. Webhook delivery is the push
|
|
1672
|
+
* path (events `approval_required` / `approval_resolved`); subscribe
|
|
1673
|
+
* is the pull fallback for clients that can't host a webhook.
|
|
1674
|
+
*
|
|
1675
|
+
* Today this iterates over a known set of tokens supplied by the
|
|
1676
|
+
* caller; an SDK-only client that wants discovery should pair this
|
|
1677
|
+
* with the webhook path. Pass tokens you already hold (e.g. from a
|
|
1678
|
+
* prior `ApprovalRequiredError`).
|
|
1679
|
+
*/
|
|
1680
|
+
subscribe(tokens, opts) {
|
|
1681
|
+
const intervalMs = Math.max(1e3, opts.intervalMs ?? 5e3);
|
|
1682
|
+
let cancelled = false;
|
|
1683
|
+
const onAbort = () => {
|
|
1684
|
+
cancelled = true;
|
|
1685
|
+
};
|
|
1686
|
+
opts.signal?.addEventListener("abort", onAbort);
|
|
1687
|
+
const tick = async () => {
|
|
1688
|
+
for (const token of tokens) {
|
|
1689
|
+
if (cancelled) return;
|
|
1690
|
+
try {
|
|
1691
|
+
const status = await this.get(token);
|
|
1692
|
+
const prev = this.seen.get(token);
|
|
1693
|
+
if (status.status !== prev) {
|
|
1694
|
+
this.seen.set(token, status.status);
|
|
1695
|
+
const event = {
|
|
1696
|
+
approvalToken: token,
|
|
1697
|
+
status: status.status,
|
|
1698
|
+
hits: status.hits
|
|
1699
|
+
};
|
|
1700
|
+
if (status.status === "pending") {
|
|
1701
|
+
opts.onApprovalRequired?.(event);
|
|
1702
|
+
} else {
|
|
1703
|
+
opts.onApprovalResolved?.(event);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
} catch (e) {
|
|
1707
|
+
opts.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
const id = setInterval(() => {
|
|
1712
|
+
void tick();
|
|
1713
|
+
}, intervalMs);
|
|
1714
|
+
void tick();
|
|
1715
|
+
return {
|
|
1716
|
+
stop: () => {
|
|
1717
|
+
cancelled = true;
|
|
1718
|
+
clearInterval(id);
|
|
1719
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1503
1722
|
}
|
|
1504
|
-
/**
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
1723
|
+
/**
|
|
1724
|
+
* Redeem an approved approvalToken. The bearer must be the same one
|
|
1725
|
+
* that minted it, and the request must target the same database.
|
|
1726
|
+
* Returns the result of the originally-blocked query or batch.
|
|
1727
|
+
*/
|
|
1728
|
+
async redeem(approvalToken) {
|
|
1729
|
+
const raw = await this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/redeem_approval`, {
|
|
1730
|
+
approvalToken
|
|
1731
|
+
});
|
|
1732
|
+
if (Array.isArray(raw)) {
|
|
1733
|
+
return raw.map((r) => ({
|
|
1734
|
+
...r,
|
|
1735
|
+
data: rowsToObjects(r.columns, r.rows)
|
|
1736
|
+
}));
|
|
1737
|
+
}
|
|
1738
|
+
return { ...raw, data: rowsToObjects(raw.columns, raw.rows) };
|
|
1513
1739
|
}
|
|
1514
1740
|
};
|
|
1515
|
-
|
|
1516
|
-
return key.split("/").map(encodeURIComponent).join("/");
|
|
1517
|
-
}
|
|
1518
|
-
var PerSQLVectors = class {
|
|
1741
|
+
var PerSQLApprovalRules = class {
|
|
1519
1742
|
constructor(client, namespace, slug) {
|
|
1520
1743
|
this.client = client;
|
|
1521
1744
|
this.namespace = namespace;
|
|
1522
1745
|
this.slug = slug;
|
|
1523
1746
|
}
|
|
1524
|
-
|
|
1525
|
-
async upsert(items) {
|
|
1747
|
+
async list() {
|
|
1526
1748
|
return this.client.request(
|
|
1527
|
-
"
|
|
1528
|
-
`/v1/db/${this.namespace}/${this.slug}/
|
|
1529
|
-
{ items }
|
|
1749
|
+
"GET",
|
|
1750
|
+
`/v1/db/${this.namespace}/${this.slug}/approval-rules`
|
|
1530
1751
|
);
|
|
1531
1752
|
}
|
|
1532
|
-
/**
|
|
1533
|
-
async
|
|
1753
|
+
/** Requires an admin-role bearer token. */
|
|
1754
|
+
async create(input) {
|
|
1534
1755
|
return this.client.request(
|
|
1535
1756
|
"POST",
|
|
1536
|
-
`/v1/db/${this.namespace}/${this.slug}/
|
|
1537
|
-
|
|
1757
|
+
`/v1/db/${this.namespace}/${this.slug}/approval-rules`,
|
|
1758
|
+
input
|
|
1538
1759
|
);
|
|
1539
1760
|
}
|
|
1540
|
-
/**
|
|
1541
|
-
async delete(
|
|
1761
|
+
/** Requires an admin-role bearer token. */
|
|
1762
|
+
async delete(ruleId) {
|
|
1542
1763
|
return this.client.request(
|
|
1543
1764
|
"DELETE",
|
|
1544
|
-
`/v1/db/${this.namespace}/${this.slug}/
|
|
1545
|
-
|
|
1765
|
+
`/v1/db/${this.namespace}/${this.slug}/approval-rules/${encodeURIComponent(
|
|
1766
|
+
ruleId
|
|
1767
|
+
)}`
|
|
1546
1768
|
);
|
|
1547
1769
|
}
|
|
1548
1770
|
};
|
|
1549
|
-
var PerSQLApprovals = class {
|
|
1550
|
-
constructor(client, namespace, slug) {
|
|
1551
|
-
this.client = client;
|
|
1552
|
-
this.namespace = namespace;
|
|
1553
|
-
this.slug = slug;
|
|
1554
|
-
}
|
|
1555
|
-
/**
|
|
1556
|
-
* Redeem an approved approvalToken. The bearer must be the same one
|
|
1557
|
-
* that minted it, and the request must target the same database.
|
|
1558
|
-
* Returns the result of the originally-blocked query or batch.
|
|
1559
|
-
*/
|
|
1560
|
-
async redeem(approvalToken) {
|
|
1561
|
-
const raw = await this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/redeem_approval`, {
|
|
1562
|
-
approvalToken
|
|
1563
|
-
});
|
|
1564
|
-
if (Array.isArray(raw)) {
|
|
1565
|
-
return raw.map((r) => ({
|
|
1566
|
-
...r,
|
|
1567
|
-
data: rowsToObjects(r.columns, r.rows)
|
|
1568
|
-
}));
|
|
1569
|
-
}
|
|
1570
|
-
return { ...raw, data: rowsToObjects(raw.columns, raw.rows) };
|
|
1571
|
-
}
|
|
1572
|
-
};
|
|
1573
1771
|
var PerSQLProposals = class {
|
|
1574
1772
|
constructor(client, namespace, slug) {
|
|
1575
1773
|
this.client = client;
|