@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.js
CHANGED
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
|
|
@@ -692,42 +829,61 @@ var PerSQLDatabase = class {
|
|
|
692
829
|
return callback;
|
|
693
830
|
}
|
|
694
831
|
/**
|
|
695
|
-
* Returns a tool definition
|
|
696
|
-
*
|
|
832
|
+
* Returns a single SQL-query tool definition in every shape PerSQL
|
|
833
|
+
* supports (Anthropic, OpenAI Chat, Vercel AI SDK, Mastra, LangChain,
|
|
834
|
+
* OpenAI Agents SDK). Same input contract as `asTools()` — the only
|
|
835
|
+
* difference is one fat `query` tool instead of typed-per-table tools.
|
|
836
|
+
*
|
|
837
|
+
* Pair the format-specific fields with `runTool` (or use the bundled
|
|
838
|
+
* `execute`/`invoke` callbacks, which call `runTool` internally).
|
|
697
839
|
*/
|
|
698
840
|
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"]
|
|
841
|
+
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.`;
|
|
842
|
+
const inputSchema = {
|
|
843
|
+
type: "object",
|
|
844
|
+
properties: {
|
|
845
|
+
sql: { type: "string", description: "SQLite SQL statement" },
|
|
846
|
+
params: {
|
|
847
|
+
type: "array",
|
|
848
|
+
items: {},
|
|
849
|
+
description: "Positional parameters for the SQL statement"
|
|
714
850
|
}
|
|
715
851
|
},
|
|
852
|
+
required: ["sql"]
|
|
853
|
+
};
|
|
854
|
+
const execute = (input) => this.runTool({
|
|
855
|
+
sql: String(input.sql ?? ""),
|
|
856
|
+
params: Array.isArray(input.params) ? input.params : []
|
|
857
|
+
});
|
|
858
|
+
return {
|
|
859
|
+
anthropic: { name, description, input_schema: inputSchema },
|
|
716
860
|
openai: {
|
|
717
861
|
type: "function",
|
|
718
|
-
function: {
|
|
862
|
+
function: { name, description, parameters: inputSchema }
|
|
863
|
+
},
|
|
864
|
+
aiSdk: () => ({
|
|
865
|
+
[name]: { description, inputSchema, execute }
|
|
866
|
+
}),
|
|
867
|
+
mastra: () => ({
|
|
868
|
+
[name]: {
|
|
869
|
+
id: name,
|
|
870
|
+
description,
|
|
871
|
+
inputSchema,
|
|
872
|
+
execute: ({ context }) => execute(context)
|
|
873
|
+
}
|
|
874
|
+
}),
|
|
875
|
+
langchain: () => [
|
|
876
|
+
{ name, description, schema: inputSchema, invoke: execute }
|
|
877
|
+
],
|
|
878
|
+
openaiAgents: () => [
|
|
879
|
+
{
|
|
880
|
+
type: "function",
|
|
719
881
|
name,
|
|
720
|
-
description
|
|
721
|
-
parameters:
|
|
722
|
-
|
|
723
|
-
properties: {
|
|
724
|
-
sql: { type: "string" },
|
|
725
|
-
params: { type: "array", items: {} }
|
|
726
|
-
},
|
|
727
|
-
required: ["sql"]
|
|
728
|
-
}
|
|
882
|
+
description,
|
|
883
|
+
parameters: inputSchema,
|
|
884
|
+
invoke: execute
|
|
729
885
|
}
|
|
730
|
-
|
|
886
|
+
]
|
|
731
887
|
};
|
|
732
888
|
}
|
|
733
889
|
/**
|
|
@@ -1499,106 +1655,148 @@ function sanitizeToolPart(s) {
|
|
|
1499
1655
|
function quoteIdent(s) {
|
|
1500
1656
|
return `"${s.replace(/"/g, '""')}"`;
|
|
1501
1657
|
}
|
|
1502
|
-
var
|
|
1658
|
+
var PerSQLApprovals = class {
|
|
1503
1659
|
constructor(client, namespace, slug) {
|
|
1504
1660
|
this.client = client;
|
|
1505
1661
|
this.namespace = namespace;
|
|
1506
1662
|
this.slug = slug;
|
|
1507
1663
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1664
|
+
// Tokens this client has seen via subscribe — used to dedupe poll
|
|
1665
|
+
// results so onApprovalRequired fires once per token regardless of
|
|
1666
|
+
// how many subscribe ticks observe it.
|
|
1667
|
+
seen = /* @__PURE__ */ new Map();
|
|
1668
|
+
/**
|
|
1669
|
+
* Look up the status of an approval token without consuming it. The
|
|
1670
|
+
* bearer must match the one that minted the original 403; cross-bearer
|
|
1671
|
+
* lookup returns 403.
|
|
1672
|
+
*/
|
|
1673
|
+
async get(approvalToken) {
|
|
1674
|
+
return this.client.request(
|
|
1675
|
+
"GET",
|
|
1676
|
+
`/v1/db/${this.namespace}/${this.slug}/approval/${encodeURIComponent(
|
|
1677
|
+
approvalToken
|
|
1678
|
+
)}`
|
|
1516
1679
|
);
|
|
1517
1680
|
}
|
|
1518
|
-
/**
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1681
|
+
/**
|
|
1682
|
+
* Poll until a reviewer decides (or the token expires). Resolves with
|
|
1683
|
+
* the final status — `approved` is your green light for `redeem()`.
|
|
1684
|
+
*/
|
|
1685
|
+
async poll(approvalToken, opts = {}) {
|
|
1686
|
+
const intervalMs = Math.max(250, opts.intervalMs ?? 2e3);
|
|
1687
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 10 * 60 * 1e3);
|
|
1688
|
+
while (true) {
|
|
1689
|
+
if (opts.signal?.aborted) {
|
|
1690
|
+
throw new Error("approval poll aborted");
|
|
1691
|
+
}
|
|
1692
|
+
const status = await this.get(approvalToken);
|
|
1693
|
+
if (status.status !== "pending") return status;
|
|
1694
|
+
if (Date.now() > deadline) return status;
|
|
1695
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1525
1696
|
}
|
|
1526
|
-
return res;
|
|
1527
1697
|
}
|
|
1528
|
-
/**
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1698
|
+
/**
|
|
1699
|
+
* Long-running poll subscription for new approval events on this
|
|
1700
|
+
* database. Returns a `stop()` handle. Webhook delivery is the push
|
|
1701
|
+
* path (events `approval_required` / `approval_resolved`); subscribe
|
|
1702
|
+
* is the pull fallback for clients that can't host a webhook.
|
|
1703
|
+
*
|
|
1704
|
+
* Today this iterates over a known set of tokens supplied by the
|
|
1705
|
+
* caller; an SDK-only client that wants discovery should pair this
|
|
1706
|
+
* with the webhook path. Pass tokens you already hold (e.g. from a
|
|
1707
|
+
* prior `ApprovalRequiredError`).
|
|
1708
|
+
*/
|
|
1709
|
+
subscribe(tokens, opts) {
|
|
1710
|
+
const intervalMs = Math.max(1e3, opts.intervalMs ?? 5e3);
|
|
1711
|
+
let cancelled = false;
|
|
1712
|
+
const onAbort = () => {
|
|
1713
|
+
cancelled = true;
|
|
1714
|
+
};
|
|
1715
|
+
opts.signal?.addEventListener("abort", onAbort);
|
|
1716
|
+
const tick = async () => {
|
|
1717
|
+
for (const token of tokens) {
|
|
1718
|
+
if (cancelled) return;
|
|
1719
|
+
try {
|
|
1720
|
+
const status = await this.get(token);
|
|
1721
|
+
const prev = this.seen.get(token);
|
|
1722
|
+
if (status.status !== prev) {
|
|
1723
|
+
this.seen.set(token, status.status);
|
|
1724
|
+
const event = {
|
|
1725
|
+
approvalToken: token,
|
|
1726
|
+
status: status.status,
|
|
1727
|
+
hits: status.hits
|
|
1728
|
+
};
|
|
1729
|
+
if (status.status === "pending") {
|
|
1730
|
+
opts.onApprovalRequired?.(event);
|
|
1731
|
+
} else {
|
|
1732
|
+
opts.onApprovalResolved?.(event);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
} catch (e) {
|
|
1736
|
+
opts.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
const id = setInterval(() => {
|
|
1741
|
+
void tick();
|
|
1742
|
+
}, intervalMs);
|
|
1743
|
+
void tick();
|
|
1744
|
+
return {
|
|
1745
|
+
stop: () => {
|
|
1746
|
+
cancelled = true;
|
|
1747
|
+
clearInterval(id);
|
|
1748
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1532
1751
|
}
|
|
1533
|
-
/**
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
const
|
|
1540
|
-
|
|
1541
|
-
|
|
1752
|
+
/**
|
|
1753
|
+
* Redeem an approved approvalToken. The bearer must be the same one
|
|
1754
|
+
* that minted it, and the request must target the same database.
|
|
1755
|
+
* Returns the result of the originally-blocked query or batch.
|
|
1756
|
+
*/
|
|
1757
|
+
async redeem(approvalToken) {
|
|
1758
|
+
const raw = await this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/redeem_approval`, {
|
|
1759
|
+
approvalToken
|
|
1760
|
+
});
|
|
1761
|
+
if (Array.isArray(raw)) {
|
|
1762
|
+
return raw.map((r) => ({
|
|
1763
|
+
...r,
|
|
1764
|
+
data: rowsToObjects(r.columns, r.rows)
|
|
1765
|
+
}));
|
|
1766
|
+
}
|
|
1767
|
+
return { ...raw, data: rowsToObjects(raw.columns, raw.rows) };
|
|
1542
1768
|
}
|
|
1543
1769
|
};
|
|
1544
|
-
|
|
1545
|
-
return key.split("/").map(encodeURIComponent).join("/");
|
|
1546
|
-
}
|
|
1547
|
-
var PerSQLVectors = class {
|
|
1770
|
+
var PerSQLApprovalRules = class {
|
|
1548
1771
|
constructor(client, namespace, slug) {
|
|
1549
1772
|
this.client = client;
|
|
1550
1773
|
this.namespace = namespace;
|
|
1551
1774
|
this.slug = slug;
|
|
1552
1775
|
}
|
|
1553
|
-
|
|
1554
|
-
async upsert(items) {
|
|
1776
|
+
async list() {
|
|
1555
1777
|
return this.client.request(
|
|
1556
|
-
"
|
|
1557
|
-
`/v1/db/${this.namespace}/${this.slug}/
|
|
1558
|
-
{ items }
|
|
1778
|
+
"GET",
|
|
1779
|
+
`/v1/db/${this.namespace}/${this.slug}/approval-rules`
|
|
1559
1780
|
);
|
|
1560
1781
|
}
|
|
1561
|
-
/**
|
|
1562
|
-
async
|
|
1782
|
+
/** Requires an admin-role bearer token. */
|
|
1783
|
+
async create(input) {
|
|
1563
1784
|
return this.client.request(
|
|
1564
1785
|
"POST",
|
|
1565
|
-
`/v1/db/${this.namespace}/${this.slug}/
|
|
1566
|
-
|
|
1786
|
+
`/v1/db/${this.namespace}/${this.slug}/approval-rules`,
|
|
1787
|
+
input
|
|
1567
1788
|
);
|
|
1568
1789
|
}
|
|
1569
|
-
/**
|
|
1570
|
-
async delete(
|
|
1790
|
+
/** Requires an admin-role bearer token. */
|
|
1791
|
+
async delete(ruleId) {
|
|
1571
1792
|
return this.client.request(
|
|
1572
1793
|
"DELETE",
|
|
1573
|
-
`/v1/db/${this.namespace}/${this.slug}/
|
|
1574
|
-
|
|
1794
|
+
`/v1/db/${this.namespace}/${this.slug}/approval-rules/${encodeURIComponent(
|
|
1795
|
+
ruleId
|
|
1796
|
+
)}`
|
|
1575
1797
|
);
|
|
1576
1798
|
}
|
|
1577
1799
|
};
|
|
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
1800
|
var PerSQLProposals = class {
|
|
1603
1801
|
constructor(client, namespace, slug) {
|
|
1604
1802
|
this.client = client;
|
|
@@ -1760,13 +1958,13 @@ function rowsToObjects(columns, rows) {
|
|
|
1760
1958
|
0 && (module.exports = {
|
|
1761
1959
|
ApprovalRequiredError,
|
|
1762
1960
|
PerSQL,
|
|
1961
|
+
PerSQLApprovalRules,
|
|
1763
1962
|
PerSQLApprovals,
|
|
1764
|
-
PerSQLBlob,
|
|
1765
1963
|
PerSQLBranches,
|
|
1766
1964
|
PerSQLDatabase,
|
|
1767
1965
|
PerSQLError,
|
|
1768
1966
|
PerSQLProposals,
|
|
1769
|
-
|
|
1770
|
-
|
|
1967
|
+
RateLimitError,
|
|
1968
|
+
SupportClient
|
|
1771
1969
|
});
|
|
1772
1970
|
//# sourceMappingURL=index.cjs.map
|