@adrdp/pg-rest 0.1.2 → 0.2.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/dist/index.js CHANGED
@@ -1,157 +1,5 @@
1
- //#region src/errors.ts
2
- /**
3
- * Thrown by the pure parts of the library (parsers, SQL builders). It is
4
- * converted into an {@link ApiError} at the request boundary.
5
- */
6
- var PgRestError = class extends Error {
7
- constructor(status, code, message, details = null, hint = null, headers = {}) {
8
- super(message);
9
- this.status = status;
10
- this.code = code;
11
- this.details = details;
12
- this.hint = hint;
13
- this.headers = headers;
14
- this.name = "PgRestError";
15
- }
16
- get body() {
17
- return {
18
- code: this.code,
19
- message: this.message,
20
- details: this.details,
21
- hint: this.hint
22
- };
23
- }
24
- };
25
- /** An error that has already been mapped to an HTTP response. */
26
- var ApiError = class extends Error {
27
- _tag = "ApiError";
28
- status;
29
- body;
30
- headers;
31
- constructor(fields) {
32
- super(fields.body.message);
33
- this.name = "ApiError";
34
- this.status = fields.status;
35
- this.body = fields.body;
36
- this.headers = fields.headers;
37
- }
38
- };
39
- /** Normalized error thrown by a {@link DbAdapter}. */
40
- var DbQueryError = class extends Error {
41
- constructor(sqlState, message, detail, hint) {
42
- super(message);
43
- this.sqlState = sqlState;
44
- this.detail = detail;
45
- this.hint = hint;
46
- this.name = "DbQueryError";
47
- }
48
- };
49
- const SQLSTATE = /^[0-9A-Z]{5}$/;
50
- /**
51
- * Recognise a database error thrown by any driver and turn it into a {@link DbQueryError}.
52
- * Drivers put the SQLSTATE in different places: `code` (node-postgres, postgres.js, PGlite),
53
- * `errno` (Bun.sql) or `sqlState`. Anything else (connection errors, bugs) is returned unchanged.
54
- */
55
- const normalizeDbError = (e) => {
56
- if (e instanceof DbQueryError || !e || typeof e !== "object") return e;
57
- const x = e;
58
- for (const key of [
59
- "sqlState",
60
- "code",
61
- "errno"
62
- ]) {
63
- const v = x[key];
64
- if (typeof v === "string" && SQLSTATE.test(v)) return new DbQueryError(v, String(x["message"] ?? ""), typeof x["detail"] === "string" ? x["detail"] : void 0, typeof x["hint"] === "string" ? x["hint"] : void 0);
65
- }
66
- return e;
67
- };
68
- /**
69
- * SQLSTATE → HTTP status, following the PostgREST 16 table
70
- * (https://docs.postgrest.org/en/v16/references/errors.html).
71
- */
72
- const statusFromSqlState = (state, role, anon) => {
73
- if (/^PT\d{3}$/.test(state)) return Number(state.slice(2));
74
- switch (state) {
75
- case "23503":
76
- case "23505": return 409;
77
- case "25006": return 405;
78
- case "42883":
79
- case "42P01": return 404;
80
- case "42P17": return 500;
81
- case "42501": return role === anon ? 401 : 403;
82
- case "P0001": return 400;
83
- case "53400": return 500;
84
- }
85
- switch (state.slice(0, 2)) {
86
- case "08":
87
- case "53": return 503;
88
- case "0L":
89
- case "0P":
90
- case "28": return 403;
91
- case "09":
92
- case "25":
93
- case "2D":
94
- case "38":
95
- case "39":
96
- case "3B":
97
- case "40":
98
- case "54":
99
- case "55":
100
- case "57":
101
- case "58":
102
- case "F0":
103
- case "HV":
104
- case "P0":
105
- case "XX": return 500;
106
- default: return 400;
107
- }
108
- };
109
- /**
110
- * Map a Postgres error to a PostgREST-style API error.
111
- *
112
- * Supports PostgREST's custom error conventions:
113
- * - `RAISE ... USING ERRCODE = 'PTxyz'` → HTTP status xyz
114
- * - `RAISE SQLSTATE 'PGRST'` with a JSON message/detail
115
- */
116
- const dbErrorToApiError = (err, role, anonymousRole) => {
117
- if (err.sqlState === "PGRST") try {
118
- const msg = JSON.parse(err.message);
119
- const det = err.detail ? JSON.parse(err.detail) : {};
120
- return new ApiError({
121
- status: det.status ?? 500,
122
- headers: det.headers ?? {},
123
- body: {
124
- code: msg.code ?? "PGRST",
125
- message: msg.message ?? "",
126
- details: msg.details ?? null,
127
- hint: msg.hint ?? null
128
- }
129
- });
130
- } catch {
131
- return new ApiError({
132
- status: 500,
133
- headers: {},
134
- body: {
135
- code: "PGRST121",
136
- message: "Could not parse JSON in the RAISE error",
137
- details: err.message,
138
- hint: null
139
- }
140
- });
141
- }
142
- return new ApiError({
143
- status: statusFromSqlState(err.sqlState, role, anonymousRole),
144
- headers: {},
145
- body: {
146
- code: err.sqlState,
147
- message: err.message,
148
- details: err.detail ?? null,
149
- hint: err.hint ?? null
150
- }
151
- });
152
- };
1
+ import { a as relKey, c as quoteIdent, d as ApiError, f as DbQueryError, h as normalizeDbError, i as levenshtein, l as quoteQualified, m as dbErrorToApiError, n as getRelation, o as resolveRelationship, p as PgRestError, r as introspect, s as literal, u as safeType } from "./schema-BhF_TQdX.js";
153
2
 
154
- //#endregion
155
3
  //#region src/parse/field.ts
156
4
  const parseError = (message, details) => new PgRestError(400, "PGRST100", message, details ?? null);
157
5
  /**
@@ -246,37 +94,6 @@ const parseField = (input) => {
246
94
  };
247
95
  };
248
96
 
249
- //#endregion
250
- //#region src/quote.ts
251
- /** Quote an SQL identifier: `a"b` → `"a""b"`. */
252
- const quoteIdent = (name) => {
253
- if (name.includes("\0")) throw new PgRestError(400, "PGRST100", "Invalid identifier", "contains a NUL byte");
254
- return `"${name.replace(/"/g, "\"\"")}"`;
255
- };
256
- const quoteQualified = (schema, name) => `${quoteIdent(schema)}.${quoteIdent(name)}`;
257
- /**
258
- * Escape a string as an SQL literal of *unknown* type, so Postgres infers the
259
- * type from context (`int_col = '5'` works). Mirrors `pg.escapeLiteral`.
260
- *
261
- * We inline literals (like PostgREST does) instead of binding parameters
262
- * because some drivers (Bun.sql) re-encode string parameters as JSON when the
263
- * inferred type is json/jsonb.
264
- */
265
- const literal = (value) => {
266
- if (value.includes("\0")) throw new PgRestError(400, "PGRST100", "Invalid value", "contains a NUL byte");
267
- let out = `'${value.replace(/'/g, "''")}'`;
268
- if (out.includes("\\")) out = `E${out.replace(/\\/g, "\\\\")}`;
269
- return out;
270
- };
271
- const ID = "[A-Za-z_][A-Za-z0-9_]*";
272
- const TYPE_RE = new RegExp(`^(?:(?:double precision|character varying|national character varying|national character|bit varying|timestamp(?:\\(\\d+\\))? with(?:out)? time zone|time(?:\\(\\d+\\))? with(?:out)? time zone)|${ID}(?:\\.${ID})?)(?:\\(\\s*\\d+(?:\\s*,\\s*\\d+)?\\s*\\))?(?:\\[\\])*$`, "i");
273
- /** Validate a user-supplied cast target (`::type`) and return it verbatim. */
274
- const safeType = (type) => {
275
- const t = type.trim();
276
- if (t.length > 100 || !TYPE_RE.test(t)) throw new PgRestError(400, "PGRST100", "Invalid type in cast", `"${type}" is not an allowed cast target`);
277
- return t;
278
- };
279
-
280
97
  //#endregion
281
98
  //#region src/parse/select.ts
282
99
  const AGGREGATES = new Set([
@@ -931,8 +748,8 @@ const parseList = (raw, where) => {
931
748
  */
932
749
  const parseOperatorExpr = (field$1, expr, unquoteValue = false) => {
933
750
  if (!unquoteValue) {
934
- const bad = checkFilter(expr);
935
- if (bad) throw bad;
751
+ const bad$1 = checkFilter(expr);
752
+ if (bad$1) throw bad$1;
936
753
  }
937
754
  let rest = expr;
938
755
  let negate = false;
@@ -1002,8 +819,8 @@ const parseLogic = (op, negate, value) => {
1002
819
  };
1003
820
  };
1004
821
  const parseOrder = (input) => {
1005
- const bad = checkOrder(input);
1006
- if (bad) throw bad;
822
+ const bad$1 = checkOrder(input);
823
+ if (bad$1) throw bad$1;
1007
824
  return parseOrderTerms(input);
1008
825
  };
1009
826
  const parseOrderTerms = (input) => splitTopLevel(input, ",").map((raw) => {
@@ -1388,431 +1205,6 @@ const parseCsv = (text) => {
1388
1205
  return data.filter((r) => !(r.length === 1 && r[0] === "")).map((r) => Object.fromEntries(header.map((h, i) => [h, r[i] === "" || r[i] === void 0 ? null : r[i]])));
1389
1206
  };
1390
1207
 
1391
- //#endregion
1392
- //#region src/schema.ts
1393
- const relKey = (schema, name) => `${schema}.${name}`;
1394
- const kindOf = (k) => k === "v" ? "view" : k === "m" ? "matview" : k === "f" ? "foreign" : k === "p" ? "partitioned" : "table";
1395
- const schemaList = (schemas) => `ARRAY[${schemas.map(literal).join(",")}]::text[]`;
1396
- const asJson = (inner) => `SELECT coalesce(json_agg(r), '[]'::json)::text AS j FROM (${inner}) r`;
1397
- const run = async (db, sql) => {
1398
- const j = (await db.query(asJson(sql)))[0]?.["j"];
1399
- return JSON.parse(typeof j === "string" ? j : "[]");
1400
- };
1401
- /**
1402
- * Foreign keys seen through views: a view that exposes the columns of a foreign key (directly, or
1403
- * through other views) gets the same relationships as the table, towards tables and views alike.
1404
- */
1405
- const viewForeignKeys = (fks, rows) => {
1406
- const viewCols = /* @__PURE__ */ new Map();
1407
- for (const r of rows) {
1408
- const key = relKey(r.vschema, r.vname);
1409
- const m = viewCols.get(key) ?? /* @__PURE__ */ new Map();
1410
- m.set(r.vcol, {
1411
- key: relKey(r.tschema, r.tname),
1412
- col: r.tcol
1413
- });
1414
- viewCols.set(key, m);
1415
- }
1416
- const base = (key, col, depth = 0) => {
1417
- const next = viewCols.get(key)?.get(col);
1418
- return next && depth < 16 ? base(next.key, next.col, depth + 1) : {
1419
- key,
1420
- col
1421
- };
1422
- };
1423
- /** Every view that exposes all of `cols` of table `tableKey`, with the view-side column names. */
1424
- const exposing = (tableKey, cols) => {
1425
- const out$1 = [];
1426
- for (const [vkey, m] of viewCols) {
1427
- const names = [];
1428
- for (const c of cols) {
1429
- let found;
1430
- for (const [vcol] of m) {
1431
- const b = base(vkey, vcol);
1432
- if (b.key === tableKey && b.col === c) {
1433
- found = vcol;
1434
- break;
1435
- }
1436
- }
1437
- if (found === void 0) break;
1438
- names.push(found);
1439
- }
1440
- if (names.length === cols.length) {
1441
- const [schema, ...rest] = vkey.split(".");
1442
- out$1.push({
1443
- schema,
1444
- name: rest.join("."),
1445
- cols: names
1446
- });
1447
- }
1448
- }
1449
- return out$1;
1450
- };
1451
- const out = [];
1452
- for (const fk of fks) {
1453
- const sources = [{
1454
- schema: fk.schema,
1455
- name: fk.table,
1456
- cols: [...fk.cols]
1457
- }, ...exposing(relKey(fk.schema, fk.table), fk.cols)];
1458
- const targets = [{
1459
- schema: fk.fschema,
1460
- name: fk.ftable,
1461
- cols: [...fk.fcols]
1462
- }, ...exposing(relKey(fk.fschema, fk.ftable), fk.fcols)];
1463
- for (const [i, s] of sources.entries()) for (const [j, t] of targets.entries()) {
1464
- if (i === 0 && j === 0) continue;
1465
- out.push({
1466
- name: fk.name,
1467
- schema: s.schema,
1468
- table: s.name,
1469
- cols: s.cols,
1470
- fschema: t.schema,
1471
- ftable: t.name,
1472
- fcols: t.cols
1473
- });
1474
- }
1475
- }
1476
- return out;
1477
- };
1478
- /** Read the catalog and build a fresh {@link SchemaCache}. */
1479
- const introspect = async (db, schemas) => {
1480
- const S = schemaList(schemas);
1481
- const relRows = await run(db, `SELECT n.nspname AS schema, c.relname AS name, c.relkind::text AS kind,
1482
- obj_description(c.oid, 'pg_class') AS comment,
1483
- pg_relation_is_updatable(c.oid, true) AS bits
1484
- FROM pg_class c
1485
- JOIN pg_namespace n ON n.oid = c.relnamespace
1486
- WHERE n.nspname = ANY(${S})
1487
- AND c.relkind IN ('r','v','m','f','p')
1488
- AND NOT c.relispartition
1489
- ORDER BY 1, 2`);
1490
- const colRows = await run(db, `SELECT n.nspname AS schema, c.relname AS "table", a.attname AS name,
1491
- format_type(a.atttypid, a.atttypmod) AS type,
1492
- NOT a.attnotnull AS nullable,
1493
- (a.atthasdef OR a.attidentity <> '') AS has_default,
1494
- pg_get_expr(d.adbin, d.adrelid) AS default_expr,
1495
- (a.attgenerated <> '') AS generated,
1496
- col_description(c.oid, a.attnum) AS comment,
1497
- a.attnum AS pos
1498
- FROM pg_attribute a
1499
- JOIN pg_class c ON c.oid = a.attrelid
1500
- JOIN pg_namespace n ON n.oid = c.relnamespace
1501
- LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
1502
- WHERE n.nspname = ANY(${S})
1503
- AND c.relkind IN ('r','v','m','f','p')
1504
- AND NOT c.relispartition
1505
- AND a.attnum > 0 AND NOT a.attisdropped
1506
- ORDER BY 1, 2, a.attnum`);
1507
- const conRows = await run(db, `SELECT con.conname AS name, con.contype::text AS type,
1508
- ns.nspname AS schema, rel.relname AS "table",
1509
- (SELECT json_agg(a.attname ORDER BY k.ord)
1510
- FROM unnest(con.conkey) WITH ORDINALITY k(attnum, ord)
1511
- JOIN pg_attribute a ON a.attrelid = con.conrelid AND a.attnum = k.attnum) AS cols,
1512
- fns.nspname AS fschema, frel.relname AS ftable,
1513
- (SELECT json_agg(a.attname ORDER BY k.ord)
1514
- FROM unnest(con.confkey) WITH ORDINALITY k(attnum, ord)
1515
- JOIN pg_attribute a ON a.attrelid = con.confrelid AND a.attnum = k.attnum) AS fcols
1516
- FROM pg_constraint con
1517
- JOIN pg_class rel ON rel.oid = con.conrelid
1518
- JOIN pg_namespace ns ON ns.oid = rel.relnamespace
1519
- LEFT JOIN pg_class frel ON frel.oid = con.confrelid
1520
- LEFT JOIN pg_namespace fns ON fns.oid = frel.relnamespace
1521
- WHERE con.contype IN ('p','u','f')
1522
- AND con.conparentid = 0
1523
- AND (ns.nspname = ANY(${S}) OR fns.nspname = ANY(${S}))`);
1524
- const viewRows = await run(db, `SELECT DISTINCT ON (vc.oid, m.x[1]::int)
1525
- vn.nspname AS vschema, vc.relname AS vname, va.attname AS vcol,
1526
- tn.nspname AS tschema, tc.relname AS tname, ta.attname AS tcol
1527
- FROM pg_rewrite rw
1528
- JOIN pg_class vc ON vc.oid = rw.ev_class AND vc.relkind IN ('v','m')
1529
- JOIN pg_namespace vn ON vn.oid = vc.relnamespace
1530
- CROSS JOIN LATERAL regexp_matches(rw.ev_action::text,
1531
- ':resno (\\d+) :resname [^:]* :ressortgroupref \\d+ :resorigtbl (\\d+) :resorigcol (\\d+) :resjunk false', 'g')
1532
- WITH ORDINALITY AS m(x, ord)
1533
- JOIN pg_attribute va ON va.attrelid = vc.oid AND va.attnum = m.x[1]::int AND NOT va.attisdropped
1534
- JOIN pg_class tc ON tc.oid = m.x[2]::oid
1535
- JOIN pg_namespace tn ON tn.oid = tc.relnamespace
1536
- JOIN pg_attribute ta ON ta.attrelid = tc.oid AND ta.attnum = m.x[3]::int
1537
- WHERE vn.nspname = ANY(${S}) AND rw.rulename = '_RETURN' AND m.x[2]::oid <> 0
1538
- ORDER BY vc.oid, m.x[1]::int, m.ord DESC`);
1539
- const fnRows = await run(db, `SELECT n.nspname AS schema, p.proname AS name,
1540
- p.proretset AS returns_set, p.provolatile::text AS volatility,
1541
- format_type(p.prorettype, NULL) AS return_type,
1542
- rt.typtype::text AS return_typtype,
1543
- coalesce(p.proargmodes && ARRAY['o','b','t']::"char"[], false) AS has_out,
1544
- p.pronargdefaults AS n_defaults,
1545
- obj_description(p.oid, 'pg_proc') AS comment,
1546
- p.prorows::float8 AS rows,
1547
- CASE WHEN rt.typtype = 'd' AND rt.typname LIKE '%/%'
1548
- THEN json_build_object('name', rt.typname, 'bytea', rt.typbasetype = 'bytea'::regtype) END AS ret_media,
1549
- (SELECT json_build_object('schema', n2.nspname, 'name', c2.relname)
1550
- FROM pg_type t2 JOIN pg_class c2 ON c2.oid = t2.typrelid JOIN pg_namespace n2 ON n2.oid = c2.relnamespace
1551
- WHERE p.pronargs = 1 AND t2.oid = p.proargtypes[0] AND c2.relkind IN ('r','v','m','f','p')) AS arg_rel,
1552
- (SELECT json_build_object('schema', n2.nspname, 'name', c2.relname)
1553
- FROM pg_type t2 JOIN pg_class c2 ON c2.oid = t2.typrelid JOIN pg_namespace n2 ON n2.oid = c2.relnamespace
1554
- WHERE t2.oid = p.prorettype AND c2.relkind IN ('r','v','m','f','p')) AS ret_rel,
1555
- (SELECT coalesce(json_agg(json_build_object(
1556
- 'name', coalesce(p.proargnames[t.i], ''),
1557
- 'type', format_type(t.typ, NULL),
1558
- 'mode', coalesce(p.proargmodes[t.i]::text, 'i')) ORDER BY t.i), '[]'::json)
1559
- FROM unnest(coalesce(p.proallargtypes, string_to_array(p.proargtypes::text, ' ')::oid[]))
1560
- WITH ORDINALITY AS t(typ, i)
1561
- WHERE coalesce(p.proargmodes[t.i]::text, 'i') IN ('i','b','v')) AS args
1562
- FROM pg_proc p
1563
- JOIN pg_namespace n ON n.oid = p.pronamespace
1564
- JOIN pg_type rt ON rt.oid = p.prorettype
1565
- WHERE n.nspname = ANY(${S})
1566
- AND p.prokind = 'f'
1567
- AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = p.oid AND d.deptype = 'e')
1568
- ORDER BY 1, 2`);
1569
- const mediaHandlers = (await run(db, `SELECT n.nspname AS schema, p.proname AS name,
1570
- (SELECT json_build_object('schema', n2.nspname, 'name', c2.relname)
1571
- FROM pg_type t2 JOIN pg_class c2 ON c2.oid = t2.typrelid JOIN pg_namespace n2 ON n2.oid = c2.relnamespace
1572
- WHERE t2.oid = p.proargtypes[0] AND c2.relkind IN ('r','v','m','f','p')) AS arg_rel,
1573
- rt.typbasetype = 'bytea'::regtype AS bytea
1574
- FROM pg_proc p
1575
- JOIN pg_namespace n ON n.oid = p.pronamespace
1576
- JOIN pg_type rt ON rt.oid = p.prorettype AND rt.typtype = 'd'
1577
- WHERE n.nspname = ANY(${S})
1578
- AND p.prokind = 'a' AND p.pronargs = 1 AND p.proname LIKE '%/%'
1579
- AND (p.proargtypes[0] = 'anyelement'::regtype
1580
- OR EXISTS (SELECT 1 FROM pg_type t3 JOIN pg_class c3 ON c3.oid = t3.typrelid WHERE t3.oid = p.proargtypes[0]))
1581
- ORDER BY 1, 2`)).map((h) => ({
1582
- schema: h.schema,
1583
- mediaType: h.name.toLowerCase(),
1584
- ...h.arg_rel ? { argRelation: h.arg_rel } : {},
1585
- bytea: h.bytea
1586
- }));
1587
- const pk = /* @__PURE__ */ new Map();
1588
- const uniques = /* @__PURE__ */ new Map();
1589
- const foreignKeys = [];
1590
- for (const c of conRows) {
1591
- const key = relKey(c.schema, c.table);
1592
- if (c.type === "p") pk.set(key, c.cols);
1593
- else if (c.type === "u") uniques.set(key, [...uniques.get(key) ?? [], c.cols]);
1594
- else if (c.type === "f" && c.fschema && c.ftable && c.fcols) foreignKeys.push({
1595
- name: c.name,
1596
- schema: c.schema,
1597
- table: c.table,
1598
- cols: c.cols,
1599
- fschema: c.fschema,
1600
- ftable: c.ftable,
1601
- fcols: c.fcols
1602
- });
1603
- }
1604
- foreignKeys.push(...viewForeignKeys(foreignKeys, viewRows));
1605
- const colsByRel = /* @__PURE__ */ new Map();
1606
- for (const c of colRows) {
1607
- const key = relKey(c.schema, c.table);
1608
- const list = colsByRel.get(key) ?? [];
1609
- list.push({
1610
- name: c.name,
1611
- type: c.type,
1612
- nullable: c.nullable,
1613
- hasDefault: c.has_default,
1614
- defaultExpr: c.default_expr,
1615
- generated: c.generated,
1616
- comment: c.comment,
1617
- position: c.pos
1618
- });
1619
- colsByRel.set(key, list);
1620
- }
1621
- const relations = /* @__PURE__ */ new Map();
1622
- for (const r of relRows) {
1623
- const key = relKey(r.schema, r.name);
1624
- relations.set(key, {
1625
- schema: r.schema,
1626
- name: r.name,
1627
- kind: kindOf(r.kind),
1628
- columns: colsByRel.get(key) ?? [],
1629
- pk: pk.get(key) ?? [],
1630
- uniques: uniques.get(key) ?? [],
1631
- insertable: (r.bits & 8) === 8,
1632
- updatable: (r.bits & 4) === 4,
1633
- deletable: (r.bits & 16) === 16,
1634
- comment: r.comment
1635
- });
1636
- }
1637
- const functions = /* @__PURE__ */ new Map();
1638
- for (const f of fnRows) {
1639
- const nDefaults = f.n_defaults;
1640
- const args = f.args.map((a, i) => ({
1641
- name: a.name,
1642
- type: a.type,
1643
- hasDefault: i >= f.args.length - nDefaults,
1644
- variadic: a.mode === "v"
1645
- }));
1646
- const def = {
1647
- schema: f.schema,
1648
- name: f.name,
1649
- args,
1650
- returnType: f.return_type,
1651
- returnsSet: f.returns_set,
1652
- returnsComposite: f.has_out || f.return_typtype === "c" || f.return_type === "record",
1653
- returnsVoid: f.return_type === "void",
1654
- volatility: f.volatility === "i" ? "immutable" : f.volatility === "s" ? "stable" : "volatile",
1655
- comment: f.comment,
1656
- rows: f.rows,
1657
- ...f.arg_rel ? { argRelation: f.arg_rel } : {},
1658
- ...f.ret_rel ? { returnRelation: f.ret_rel } : {},
1659
- ...f.ret_media && !f.returns_set ? { returnMediaType: f.ret_media } : {}
1660
- };
1661
- const key = relKey(f.schema, f.name);
1662
- functions.set(key, [...functions.get(key) ?? [], def]);
1663
- }
1664
- return {
1665
- schemas,
1666
- relations,
1667
- foreignKeys,
1668
- functions,
1669
- mediaHandlers
1670
- };
1671
- };
1672
- const sameCols = (a, b) => a.length === b.length && a.every((c) => b.includes(c));
1673
- const isUnique = (rel, cols) => rel.pk.length > 0 && sameCols(rel.pk, cols) || rel.uniques.some((u) => sameCols(u, cols));
1674
- const describe = (r) => {
1675
- const j = r.junction;
1676
- if (j) return `'${j.relation.name}' using '${j.fkFrom.name}' and '${j.fkTo.name}' (many-to-many)`;
1677
- if (r.computed) return `'${r.computed.name}' (computed, ${r.kind === "m2o" ? "many-to-one" : "one-to-many"})`;
1678
- return `'${r.fk.name}' (${r.kind === "m2o" ? "many-to-one" : r.kind === "o2o" ? "one-to-one" : "one-to-many"}) on ${r.fk.table}(${r.fk.cols.join(",")})`;
1679
- };
1680
- /** Find the relationships between `from` and the table/view named `target`. */
1681
- const findRelationships = (cache, from, target, hint) => {
1682
- const out = [];
1683
- const rel = (schema, name) => cache.relations.get(relKey(schema, name));
1684
- for (const fk of cache.foreignKeys) {
1685
- const selfReference = fk.schema === fk.fschema && fk.table === fk.ftable;
1686
- if (fk.schema === from.schema && fk.table === from.name && fk.ftable === target && !selfReference) {
1687
- const to = rel(fk.fschema, fk.ftable);
1688
- if (to) out.push({
1689
- kind: "m2o",
1690
- from,
1691
- to,
1692
- fromCols: fk.cols,
1693
- toCols: fk.fcols,
1694
- fk
1695
- });
1696
- }
1697
- if (fk.schema === from.schema && fk.table === from.name && fk.cols.length === 1 && fk.cols[0] === target) {
1698
- const to = rel(fk.fschema, fk.ftable);
1699
- if (to) out.push({
1700
- kind: "m2o",
1701
- from,
1702
- to,
1703
- fromCols: fk.cols,
1704
- toCols: fk.fcols,
1705
- fk
1706
- });
1707
- }
1708
- if (fk.fschema === from.schema && fk.ftable === from.name && fk.table === target) {
1709
- const to = rel(fk.schema, fk.table);
1710
- if (to) out.push({
1711
- kind: isUnique(to, fk.cols) ? "o2o" : "o2m",
1712
- from,
1713
- to,
1714
- fromCols: fk.fcols,
1715
- toCols: fk.cols,
1716
- fk
1717
- });
1718
- }
1719
- }
1720
- for (const f of cache.functions.get(relKey(from.schema, target)) ?? []) {
1721
- if (f.argRelation?.schema !== from.schema || f.argRelation.name !== from.name || !f.returnRelation) continue;
1722
- const to = rel(f.returnRelation.schema, f.returnRelation.name);
1723
- if (!to) continue;
1724
- const fk = {
1725
- name: f.name,
1726
- schema: from.schema,
1727
- table: from.name,
1728
- cols: [],
1729
- fschema: to.schema,
1730
- ftable: to.name,
1731
- fcols: []
1732
- };
1733
- out.push({
1734
- kind: !f.returnsSet || f.rows === 1 ? "m2o" : "o2m",
1735
- from,
1736
- to,
1737
- fromCols: [],
1738
- toCols: [],
1739
- fk,
1740
- computed: f
1741
- });
1742
- }
1743
- for (const fk1 of cache.foreignKeys) {
1744
- if (fk1.fschema !== from.schema || fk1.ftable !== from.name) continue;
1745
- const junction = rel(fk1.schema, fk1.table);
1746
- if (!junction) continue;
1747
- for (const fk2 of cache.foreignKeys) {
1748
- if (fk2 === fk1) continue;
1749
- if (fk2.schema !== fk1.schema || fk2.table !== fk1.table) continue;
1750
- if (fk2.ftable !== target) continue;
1751
- const to = rel(fk2.fschema, fk2.ftable);
1752
- if (!to) continue;
1753
- out.push({
1754
- kind: "m2m",
1755
- from,
1756
- to,
1757
- fromCols: fk1.fcols,
1758
- toCols: fk2.fcols,
1759
- fk: fk1,
1760
- junction: {
1761
- relation: junction,
1762
- fkFrom: fk1,
1763
- fkTo: fk2
1764
- }
1765
- });
1766
- }
1767
- }
1768
- if (!hint) return out;
1769
- return out.filter((r) => {
1770
- if (r.junction) return r.junction.relation.name === hint || r.junction.fkFrom.name === hint || r.junction.fkTo.name === hint;
1771
- return r.fk.name === hint || r.fk.cols.includes(hint) || r.fk.fcols.includes(hint);
1772
- });
1773
- };
1774
- /**
1775
- * Resolve exactly one relationship or throw PostgREST-style PGRST200/PGRST201.
1776
- * `target` may also be a table that is not related at all (→ PGRST200).
1777
- */
1778
- const resolveRelationship = (cache, from, target, hint) => {
1779
- const found = findRelationships(cache, from, target, hint);
1780
- if (found.length === 1) return found[0];
1781
- if (found.length === 0) throw new PgRestError(400, "PGRST200", `Could not find a relationship between '${from.name}' and '${target}' in the schema cache`, hint ? `Searched for a foreign key relationship between '${from.name}' and '${target}' using the hint '${hint}' in the schema '${from.schema}', but no matches were found.` : `Searched for a foreign key relationship between '${from.name}' and '${target}' in the schema '${from.schema}', but no matches were found.`, null);
1782
- throw new PgRestError(300, "PGRST201", `Could not embed because more than one relationship was found for '${from.name}' and '${target}'`, JSON.stringify(found.map((r) => describe(r))), `Try changing '${target}' to one of the following: ${found.map((r) => `'${target}!${r.junction ? r.junction.relation.name : r.fk.name}'`).join(", ")}. Find the desired relationship in the 'details' key.`);
1783
- };
1784
- /** Look up a relation by (optional) schema and name, or throw PGRST205. */
1785
- const getRelation = (cache, schema, name) => {
1786
- const rel = cache.relations.get(relKey(schema, name));
1787
- if (!rel) throw new PgRestError(404, "PGRST205", `Could not find the table '${schema}.${name}' in the schema cache`, null, suggest(cache, schema, name));
1788
- return rel;
1789
- };
1790
- const suggest = (cache, schema, name) => {
1791
- let best;
1792
- for (const r of cache.relations.values()) {
1793
- if (r.schema !== schema) continue;
1794
- const d = levenshtein(name.toLowerCase(), r.name.toLowerCase());
1795
- if (d <= Math.max(1, Math.floor(name.length / 4)) && (!best || d < best.d)) best = {
1796
- name: r.name,
1797
- d
1798
- };
1799
- }
1800
- return best ? `Perhaps you meant the table '${schema}.${best.name}'` : null;
1801
- };
1802
- const levenshtein = (a, b) => {
1803
- const dp = Array.from({ length: b.length + 1 }, (_, i) => i);
1804
- for (let i = 1; i <= a.length; i++) {
1805
- let prev = dp[0];
1806
- dp[0] = i;
1807
- for (let j = 1; j <= b.length; j++) {
1808
- const tmp = dp[j];
1809
- dp[j] = Math.min(dp[j] + 1, dp[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
1810
- prev = tmp;
1811
- }
1812
- }
1813
- return dp[b.length];
1814
- };
1815
-
1816
1208
  //#endregion
1817
1209
  //#region src/media.ts
1818
1210
  const choiceFor = (mediaType, bytea, extra) => ({
@@ -2753,6 +2145,12 @@ const buildDelete = (o) => {
2753
2145
 
2754
2146
  //#endregion
2755
2147
  //#region src/core.ts
2148
+ /** The bare keys of a query string: `?a&b=1` -> `{a}`. They are arguments, never filters. */
2149
+ const bareKeys = (search) => {
2150
+ const out = /* @__PURE__ */ new Set();
2151
+ for (const kv of search.slice(1).split("&")) if (kv !== "" && !kv.includes("=")) out.add(decodeURIComponent(kv));
2152
+ return out;
2153
+ };
2756
2154
  /** Statement options that depend on the negotiated output format. */
2757
2155
  const formatOptions = (format) => ({
2758
2156
  csv: format.kind === "csv",
@@ -2760,11 +2158,17 @@ const formatOptions = (format) => ({
2760
2158
  handler: format.kind === "handler" ? format.choice : void 0
2761
2159
  });
2762
2160
  const JSON_CT = "application/json; charset=utf-8";
2763
- const readSchema = (env, request, method) => {
2764
- const wanted = (method === "GET" || method === "HEAD" ? request.headers.get("accept-profile") : request.headers.get("content-profile"))?.trim();
2765
- if (!wanted) return env.config.schemas[0];
2766
- if (!env.config.schemas.includes(wanted)) throw new PgRestError(406, "PGRST106", `Invalid schema: ${wanted}`, null, `Only the following schemas are exposed: ${env.config.schemas.join(", ")}`);
2767
- return wanted;
2161
+ /** The schema a call runs against: the one it asks for, if exposed, else the default one. */
2162
+ const useSchema = (env, wanted) => {
2163
+ const name = wanted?.trim();
2164
+ if (!name) return env.config.schemas[0];
2165
+ if (!env.config.schemas.includes(name)) throw new PgRestError(406, "PGRST106", `Invalid schema: ${name}`, null, `Only the following schemas are exposed: ${env.config.schemas.join(", ")}`);
2166
+ return name;
2167
+ };
2168
+ const readSchema = (env, request, method) => useSchema(env, method === "GET" || method === "HEAD" ? request.headers.get("accept-profile") : request.headers.get("content-profile"));
2169
+ /** Refuse a call that has no role when anonymous access is disabled. */
2170
+ const assertRole = (env, ctx) => {
2171
+ if (!ctx.role && env.config.anonymousRole === void 0) throw new PgRestError(401, "PGRST302", "Anonymous access is disabled", null, null);
2768
2172
  };
2769
2173
  const parseCookies = (header) => {
2770
2174
  const out = {};
@@ -2783,14 +2187,14 @@ const parseCookies = (header) => {
2783
2187
  }
2784
2188
  return out;
2785
2189
  };
2786
- const applyCtxSql = (env, ctx, role, claims, request, method, pathname, schema, prefer) => {
2190
+ const applyCtxSql = (env, ctx, role, claims, caller, method, schema, prefer) => {
2787
2191
  const entries = [["role", role], ["request.jwt.claims", JSON.stringify(claims)]];
2788
2192
  if (env.config.requestSettings) {
2789
2193
  const headers = {};
2790
- request.headers.forEach((v, k) => {
2194
+ caller.headers?.forEach((v, k) => {
2791
2195
  headers[k] = v;
2792
2196
  });
2793
- entries.push(["request.method", method], ["request.path", pathname], ["request.headers", JSON.stringify(headers)], ["request.cookies", JSON.stringify(parseCookies(request.headers.get("cookie")))]);
2197
+ entries.push(["request.method", method], ["request.path", caller.path], ["request.headers", JSON.stringify(headers)], ["request.cookies", JSON.stringify(parseCookies(caller.headers?.get("cookie") ?? null))]);
2794
2198
  }
2795
2199
  const searchPath = [schema, ...env.config.extraSearchPath].map(quoteIdent).join(", ");
2796
2200
  entries.push(["search_path", searchPath]);
@@ -2807,21 +2211,21 @@ const applyCtxSql = (env, ctx, role, claims, request, method, pathname, schema,
2807
2211
  /** Parsed `response.headers` GUC: `[{"X-A": "b"}, ...]`. */
2808
2212
  const parseResponseHeaders = (raw) => {
2809
2213
  if (raw == null || raw === "") return {};
2810
- const bad = () => new PgRestError(500, "PGRST111", "response.headers guc must be a JSON array composed of objects with a single key and a string value");
2214
+ const bad$1 = () => new PgRestError(500, "PGRST111", "response.headers guc must be a JSON array composed of objects with a single key and a string value");
2811
2215
  let parsed;
2812
2216
  try {
2813
2217
  parsed = JSON.parse(String(raw));
2814
2218
  } catch {
2815
- throw bad();
2219
+ throw bad$1();
2816
2220
  }
2817
- if (!Array.isArray(parsed)) throw bad();
2221
+ if (!Array.isArray(parsed)) throw bad$1();
2818
2222
  const out = {};
2819
2223
  for (const h of parsed) {
2820
- if (!h || typeof h !== "object" || Array.isArray(h)) throw bad();
2224
+ if (!h || typeof h !== "object" || Array.isArray(h)) throw bad$1();
2821
2225
  const keys = Object.keys(h);
2822
- if (keys.length !== 1) throw bad();
2226
+ if (keys.length !== 1) throw bad$1();
2823
2227
  const v = h[keys[0]];
2824
- if (typeof v !== "string") throw bad();
2228
+ if (typeof v !== "string") throw bad$1();
2825
2229
  out[keys[0]] = v;
2826
2230
  }
2827
2231
  return out;
@@ -2870,7 +2274,7 @@ const effectivePaging = (env, query, range) => {
2870
2274
  };
2871
2275
  };
2872
2276
  const handleRequest = async (env, request, ctx) => {
2873
- if (!ctx.role && env.config.anonymousRole === void 0) throw new PgRestError(401, "PGRST302", "Anonymous access is disabled", null, null);
2277
+ assertRole(env, ctx);
2874
2278
  if (!isSupportedMethod(request.method)) return new Response(null, {
2875
2279
  status: 405,
2876
2280
  headers: { "Content-Type": JSON_CT }
@@ -2921,14 +2325,22 @@ const handleRequest = async (env, request, ctx) => {
2921
2325
  const wantsPlan = requested.kind === "plan" ? requested : void 0;
2922
2326
  const range = parseRange(request.headers.get("range"), request.headers.get("range-unit"));
2923
2327
  const body = await parseBody(request, method);
2328
+ const qp = {
2329
+ params: [...url.searchParams],
2330
+ bare: bareKeys(url.search)
2331
+ };
2924
2332
  let plan;
2925
- if (target.type === "table") plan = planTable(env, method, schema, target.name, url, prefer, format, range, body);
2926
- else plan = planRpc(env, method, schema, target.name, url, prefer, format, range, body);
2927
- return execute(env, ctx, request, method, url.pathname, schema, prefer, format, plan, honoredTx, wantsPlan);
2928
- };
2929
- const planTable = (env, method, schema, name, url, prefer, format, range, body) => {
2333
+ if (target.type === "table") plan = planTable(env, method, schema, target.name, qp, prefer, format, range, body);
2334
+ else plan = planRpc(env, method, schema, target.name, qp, prefer, format, range, body);
2335
+ return execute(env, ctx, {
2336
+ path: url.pathname,
2337
+ headers: request.headers,
2338
+ request
2339
+ }, method, schema, prefer, format, plan, honoredTx, wantsPlan);
2340
+ };
2341
+ const planTable = (env, method, schema, name, qp, prefer, format, range, body) => {
2930
2342
  const rel = getRelation(env.cache, schema, name);
2931
- const query = parseQuery([...url.searchParams]);
2343
+ const query = parseQuery(qp.params);
2932
2344
  const strip = format.kind === "json" || format.kind === "object" ? format.stripNulls : false;
2933
2345
  const singular = format.kind === "object";
2934
2346
  if (method === "GET" || method === "HEAD") {
@@ -3137,7 +2549,7 @@ const planTable = (env, method, schema, name, url, prefer, format, range, body)
3137
2549
  };
3138
2550
  /** A query-string value in the filter syntax (`eq.5`, `not.in.(1,2)`, `like(any).{a*}`), not a plain argument. */
3139
2551
  const looksLikeFilter = (v) => /^(not\.)?(eq|neq|gt|gte|lt|lte|like|ilike|match|imatch|in|is|isdistinct|cs|cd|ov|sl|sr|nxl|nxr|adj|fts|plfts|phfts|wfts)(\([a-z]+\))?\./.test(v);
3140
- const planRpc = (env, method, schema, name, url, prefer, format, range, body) => {
2552
+ const planRpc = (env, method, schema, name, qp, prefer, format, range, body) => {
3141
2553
  if (method !== "GET" && method !== "HEAD" && method !== "POST") throw new PgRestError(405, "PGRST101", `Cannot use the ${method} method on RPC`);
3142
2554
  const RESERVED = new Set([
3143
2555
  "select",
@@ -3153,7 +2565,7 @@ const planRpc = (env, method, schema, name, url, prefer, format, range, body) =>
3153
2565
  let nonObjectBody = false;
3154
2566
  const isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
3155
2567
  const getArgKeys = /* @__PURE__ */ new Set();
3156
- const bare = /* @__PURE__ */ new Set();
2568
+ const bare = qp.bare;
3157
2569
  if (method === "POST") {
3158
2570
  if (body) {
3159
2571
  const v = body.value;
@@ -3169,9 +2581,8 @@ const planRpc = (env, method, schema, name, url, prefer, format, range, body) =>
3169
2581
  }
3170
2582
  }
3171
2583
  } else {
3172
- for (const kv of url.search.slice(1).split("&")) if (kv !== "" && !kv.includes("=")) bare.add(decodeURIComponent(kv));
3173
2584
  const lists = /* @__PURE__ */ new Map();
3174
- for (const [k, v] of url.searchParams) {
2585
+ for (const [k, v] of qp.params) {
3175
2586
  if (RESERVED.has(k) || bare.has(k) || looksLikeFilter(v) || k.includes(".")) continue;
3176
2587
  lists.set(k, [...lists.get(k) ?? [], v]);
3177
2588
  }
@@ -3186,7 +2597,7 @@ const planRpc = (env, method, schema, name, url, prefer, format, range, body) =>
3186
2597
  const rawBody = nonObjectBody && isSingleUnnamedParam(fn);
3187
2598
  const call = buildRpc(env.cache, fn, provided, rawBody);
3188
2599
  const fnArgNames = new Set(fn.args.map((a) => a.name));
3189
- const query = parseQuery([...url.searchParams].filter(([k, v]) => !(v === "" && bare.has(k))), { allowFilterKey: (k) => method === "POST" || !fnArgNames.has(k) });
2600
+ const query = parseQuery(qp.params.filter(([k, v]) => !(v === "" && bare.has(k))), { allowFilterKey: (k) => method === "POST" || !fnArgNames.has(k) });
3190
2601
  const argsJson = rawBody ? JSON.stringify(body.value) : JSON.stringify(argValues);
3191
2602
  const params = call.usesBody ? [rawBody && body.kind === "json" && typeof body.value === "string" ? body.value : argsJson] : [];
3192
2603
  const strip = format.kind === "json" || format.kind === "object" ? format.stripNulls : false;
@@ -3243,13 +2654,13 @@ const planRpc = (env, method, schema, name, url, prefer, format, range, body) =>
3243
2654
  })
3244
2655
  };
3245
2656
  };
3246
- const execute = async (env, ctx, request, method, pathname, schema, prefer, format, plan, honoredTx, wantsPlan) => {
2657
+ const execute = async (env, ctx, caller, method, schema, prefer, format, plan, honoredTx, wantsPlan) => {
3247
2658
  const role = ctx.role ?? env.config.anonymousRole;
3248
2659
  const claims = {
3249
2660
  role,
3250
2661
  ...ctx.claims ?? {}
3251
2662
  };
3252
- const setup = applyCtxSql(env, ctx, role, claims, request, method, pathname, schema, prefer);
2663
+ const setup = applyCtxSql(env, ctx, role, claims, caller, method, schema, prefer);
3253
2664
  const strict = prefer.handling === "strict";
3254
2665
  const rollback = honoredTx && prefer.tx === "rollback" || wantsPlan !== void 0;
3255
2666
  const attempt = async () => {
@@ -3263,8 +2674,8 @@ const execute = async (env, ctx, request, method, pathname, schema, prefer, form
3263
2674
  role,
3264
2675
  claims,
3265
2676
  method,
3266
- path: pathname,
3267
- request
2677
+ path: caller.path,
2678
+ request: caller.request
3268
2679
  });
3269
2680
  let planned;
3270
2681
  if (plan.planned) {
@@ -3330,9 +2741,9 @@ const execute = async (env, ctx, request, method, pathname, schema, prefer, form
3330
2741
  }
3331
2742
  });
3332
2743
  }
3333
- return buildResponse(env, request, method, prefer, format, plan, result, honoredTx);
2744
+ return buildResponse(env, method, prefer, format, plan, result, honoredTx);
3334
2745
  };
3335
- const buildResponse = (env, request, method, prefer, format, plan, r, honoredTx) => {
2746
+ const buildResponse = (env, method, prefer, format, plan, r, honoredTx) => {
3336
2747
  const headers = { Vary: "Accept, Prefer, Range" };
3337
2748
  const applied = appliedHeader(prefer, honoredTx);
3338
2749
  if (applied) headers["Preference-Applied"] = applied;
@@ -3486,11 +2897,305 @@ const handleSafely = async (env, request, ctx) => {
3486
2897
  }
3487
2898
  };
3488
2899
 
2900
+ //#endregion
2901
+ //#region src/query.ts
2902
+ /** A query that failed, with the PostgREST error body and the input that produced it. */
2903
+ var QueryError = class extends ApiError {
2904
+ constructor(input, api, options) {
2905
+ super({
2906
+ status: api.status,
2907
+ body: api.body,
2908
+ headers: api.headers
2909
+ });
2910
+ this.input = input;
2911
+ this.name = "QueryError";
2912
+ if (options?.cause !== void 0) this.cause = options.cause;
2913
+ }
2914
+ };
2915
+ const OPS = new Set([
2916
+ "eq",
2917
+ "neq",
2918
+ "gt",
2919
+ "gte",
2920
+ "lt",
2921
+ "lte",
2922
+ "like",
2923
+ "ilike",
2924
+ "match",
2925
+ "imatch",
2926
+ "in",
2927
+ "is",
2928
+ "isdistinct",
2929
+ "fts",
2930
+ "plfts",
2931
+ "phfts",
2932
+ "wfts",
2933
+ "cs",
2934
+ "cd",
2935
+ "ov",
2936
+ "sl",
2937
+ "sr",
2938
+ "nxl",
2939
+ "nxr",
2940
+ "adj"
2941
+ ]);
2942
+ /** Inside a list or a logic tree these read as something other than text, so such a string is quoted. */
2943
+ const KEYWORDS = new Set([
2944
+ "null",
2945
+ "true",
2946
+ "false",
2947
+ "unknown"
2948
+ ]);
2949
+ const bad = (message, hint) => new PgRestError(400, "PGRST100", message, null, hint ?? null);
2950
+ /**
2951
+ * Render a value the way the query string carries it. A lone value is taken verbatim --
2952
+ * `name=eq.a,b` is the text `a,b` and `name=eq.null` is the text `null`, which is why
2953
+ * only a *delimited* context (a list or a logic tree, where `,` `)` and `.` separate)
2954
+ * needs quoting. `tests/compare` pins both against PostgREST.
2955
+ */
2956
+ const lit = (v, delimited) => {
2957
+ if (v === void 0 || v === null) return "null";
2958
+ if (typeof v !== "string") return String(v);
2959
+ if (!delimited) return v;
2960
+ return v === "" || KEYWORDS.has(v.toLowerCase()) || /[,()[\]{}".\\]/.test(v) || /^\s|\s$/.test(v) ? `"${v.replace(/(["\\])/g, "\\$1")}"` : v;
2961
+ };
2962
+ /** Array values are a list for `in` and a PostgreSQL array literal for the containment operators. */
2963
+ const listOf = (op, values) => {
2964
+ const items = values.map((v) => lit(v, true)).join(",");
2965
+ return op === "in" ? `(${items})` : `{${items}}`;
2966
+ };
2967
+ const toSpec = (column, f) => {
2968
+ if (f === null || typeof f !== "object") return {
2969
+ op: "eq",
2970
+ value: f
2971
+ };
2972
+ if (Array.isArray(f)) {
2973
+ const [op, value] = f;
2974
+ if (typeof op !== "string" || !OPS.has(op)) throw bad(`Unknown filter operator '${String(op)}' on '${column}'`, "A filter is a value, a [operator, value] tuple or { op, value }");
2975
+ return {
2976
+ op,
2977
+ value
2978
+ };
2979
+ }
2980
+ const spec = f;
2981
+ if (!OPS.has(spec.op)) throw bad(`Unknown filter operator '${String(spec.op)}' on '${column}'`);
2982
+ if (spec.quantifier && spec.config) throw bad(`'${column}' cannot have both a quantifier and a text-search configuration`);
2983
+ return spec;
2984
+ };
2985
+ /** The right-hand side of a filter: `[not.]op[(modifier)].value`. */
2986
+ const renderFilter = (column, f, delimited) => {
2987
+ const spec = toSpec(column, f);
2988
+ if (spec.value === null && (spec.op === "eq" || spec.op === "neq")) return `${Boolean(spec.not) !== (spec.op === "neq") ? "not." : ""}is.null`;
2989
+ const modifier = spec.quantifier ?? spec.config;
2990
+ return `${`${spec.not ? "not." : ""}${spec.op}${modifier ? `(${modifier})` : ""}`}.${Array.isArray(spec.value) ? listOf(spec.op, spec.value) : lit(spec.value, delimited)}`;
2991
+ };
2992
+ const isGroupKey = (key) => key === "and" || key === "or";
2993
+ /** Every condition of a `Where`, rendered as a logic tree would carry them (`total.gte.100`). */
2994
+ const renderConditions = (w) => Object.entries(w).flatMap(([key, value]) => {
2995
+ if (value === void 0) return [];
2996
+ if (isGroupKey(key)) {
2997
+ if (!Array.isArray(value)) throw bad(`'${key}' takes a list of groups`);
2998
+ return [`${key}(${value.map(renderGroup).join(",")})`];
2999
+ }
3000
+ return [`${key}.${renderFilter(key, value, true)}`];
3001
+ });
3002
+ /** One member of an `and`/`or` list. Several conditions in the same object are ANDed. */
3003
+ const renderGroup = (w) => {
3004
+ const parts = renderConditions(w);
3005
+ if (parts.length === 0) throw bad("An and/or group cannot be empty");
3006
+ return parts.length === 1 ? parts[0] : `and(${parts.join(",")})`;
3007
+ };
3008
+ const renderOrder = (o) => {
3009
+ if (typeof o === "string") return o;
3010
+ const nulls = o.nulls === "first" ? ".nullsfirst" : o.nulls === "last" ? ".nullslast" : "";
3011
+ return `${o.column}${o.desc ? ".desc" : ".asc"}${nulls}`;
3012
+ };
3013
+ /** `where`, `order`, `limit` and `offset` of one node, prefixed with its dotted path. */
3014
+ const pushNode = (node, path, out) => {
3015
+ const at$1 = path.length ? `${path.join(".")}.` : "";
3016
+ for (const [key, value] of Object.entries(node.where ?? {})) {
3017
+ if (value === void 0) continue;
3018
+ if (isGroupKey(key)) {
3019
+ if (!Array.isArray(value)) throw bad(`'${key}' takes a list of groups`);
3020
+ out.push([`${at$1}${key}`, `(${value.map(renderGroup).join(",")})`]);
3021
+ } else out.push([`${at$1}${key}`, renderFilter(key, value, false)]);
3022
+ }
3023
+ if (node.order?.length) out.push([`${at$1}order`, node.order.map(renderOrder).join(",")]);
3024
+ if (node.limit !== void 0) out.push([`${at$1}limit`, String(node.limit)]);
3025
+ if (node.offset !== void 0) out.push([`${at$1}offset`, String(node.offset)]);
3026
+ };
3027
+ const isEmbed = (v) => !Array.isArray(v);
3028
+ /** The `select` parameter. Embedded resources also push their own filters and paging into `out`. */
3029
+ const renderSelect = (items, path, out) => {
3030
+ return items.map((item) => {
3031
+ if (typeof item === "string") return item;
3032
+ return Object.entries(item).map(([relation, spec]) => {
3033
+ const embed = isEmbed(spec) ? spec : { select: spec };
3034
+ const name = embed.as ?? relation;
3035
+ const mods = `${embed.hint ? `!${embed.hint}` : ""}${embed.inner ? "!inner" : ""}`;
3036
+ const head = `${embed.spread ? "..." : ""}${embed.as ? `${embed.as}:` : ""}${relation}${mods}`;
3037
+ const inner = renderSelect(embed.select ?? ["*"], [...path, name], out);
3038
+ pushNode(embed, [...path, name], out);
3039
+ return `${head}(${inner})`;
3040
+ }).join(",");
3041
+ }).join(",");
3042
+ };
3043
+ /** Everything the query string would carry, built from the object instead of parsed from a URL. */
3044
+ const toParams = (input) => {
3045
+ const out = [];
3046
+ const embedded = [];
3047
+ if (input.select) out.push(["select", renderSelect(input.select, [], embedded)]);
3048
+ out.push(...embedded);
3049
+ pushNode(input, [], out);
3050
+ if ("action" in input && (input.action === "insert" || input.action === "upsert")) {
3051
+ if (input.onConflict?.length) out.push(["on_conflict", input.onConflict.join(",")]);
3052
+ if (input.columns?.length) out.push(["columns", input.columns.join(",")]);
3053
+ }
3054
+ return out;
3055
+ };
3056
+ const isRpc = (input) => "rpc" in input;
3057
+ const methodOf = (input) => {
3058
+ if (isRpc(input)) return "POST";
3059
+ switch (input.action ?? "select") {
3060
+ case "select": return "GET";
3061
+ case "insert":
3062
+ case "upsert": return "POST";
3063
+ case "update": return "PATCH";
3064
+ case "delete": return "DELETE";
3065
+ }
3066
+ };
3067
+ const bodyOf = (input) => {
3068
+ if (isRpc(input)) return {
3069
+ kind: "json",
3070
+ value: input.args ?? {}
3071
+ };
3072
+ if (!("action" in input)) return void 0;
3073
+ switch (input.action) {
3074
+ case "insert":
3075
+ case "upsert": return {
3076
+ kind: "json",
3077
+ value: input.rows
3078
+ };
3079
+ case "update": return {
3080
+ kind: "json",
3081
+ value: input.set
3082
+ };
3083
+ default: return;
3084
+ }
3085
+ };
3086
+ const writes = (input) => {
3087
+ return methodOf(input) !== "GET" && !isRpc(input);
3088
+ };
3089
+ /**
3090
+ * Unlike HTTP, a write defaults to returning its rows: a function that gives nothing back
3091
+ * is of little use in a program. `returning: "minimal"` is PostgREST's own default.
3092
+ */
3093
+ const returningOf = (input) => {
3094
+ if (!writes(input)) return void 0;
3095
+ return input.returning ?? "representation";
3096
+ };
3097
+ const preferencesOf = (input) => ({
3098
+ ...returningOf(input) ? { return: returningOf(input) } : {},
3099
+ ...input.count ? { count: input.count } : {},
3100
+ ..."action" in input && input.action === "upsert" ? { resolution: "merge-duplicates" } : {},
3101
+ ..."action" in input && input.action === "insert" && input.missing ? { missing: input.missing } : {},
3102
+ ...input.maxAffected !== void 0 ? {
3103
+ handling: "strict",
3104
+ maxAffected: input.maxAffected
3105
+ } : {},
3106
+ ...input.timezone ? { timezone: input.timezone } : {},
3107
+ invalid: []
3108
+ });
3109
+ /** The total in a `Content-Range` (`0-9/42`), when one was computed. */
3110
+ const totalOf = (range) => {
3111
+ const total = range?.split("/")[1];
3112
+ return total === void 0 || total === "*" ? void 0 : Number(total);
3113
+ };
3114
+ const run = async (env, input, ctx) => {
3115
+ assertRole(env, ctx);
3116
+ if (isRpc(input) && "table" in input) throw bad("A query names either a table or an rpc, not both");
3117
+ const schema = useSchema(env, input.schema);
3118
+ const method = methodOf(input);
3119
+ const prefer = preferencesOf(input);
3120
+ const format = input.single ? {
3121
+ kind: "object",
3122
+ stripNulls: Boolean(input.stripNulls)
3123
+ } : {
3124
+ kind: "json",
3125
+ stripNulls: Boolean(input.stripNulls)
3126
+ };
3127
+ const qp = {
3128
+ params: toParams(input),
3129
+ bare: /* @__PURE__ */ new Set()
3130
+ };
3131
+ const body = bodyOf(input);
3132
+ const plan = isRpc(input) ? planRpc(env, method, schema, input.rpc, qp, prefer, format, void 0, body) : planTable(env, method, schema, input.table, qp, prefer, format, void 0, body);
3133
+ const res = await execute(env, ctx, { path: isRpc(input) ? `/rpc/${input.rpc}` : `/${input.table}` }, method, schema, prefer, format, plan, false, void 0);
3134
+ const text = await res.text();
3135
+ return {
3136
+ data: text === "" ? null : JSON.parse(text),
3137
+ count: totalOf(res.headers.get("content-range")),
3138
+ status: res.status
3139
+ };
3140
+ };
3141
+ /**
3142
+ * Run a query described by an object, in-process. Same parser, same SQL and same errors as
3143
+ * {@link handleRequest}: only the HTTP envelope is gone. Throws a {@link QueryError}.
3144
+ */
3145
+ const runQuery = async (env, input, ctx = {}) => {
3146
+ try {
3147
+ return await run(env, input, ctx);
3148
+ } catch (e) {
3149
+ const api = toApiError(e, ctx.role ?? env.config.anonymousRole, env.config.anonymousRole);
3150
+ throw new QueryError(input, api, api.status >= 500 && !(e instanceof PgRestError) && !(e instanceof ApiError) ? { cause: e } : void 0);
3151
+ }
3152
+ };
3153
+ /**
3154
+ * Turn anything at all into the failed half of a {@link QuerySafeResult}. `runQuery` already maps
3155
+ * what it can; whatever it could not is still reported, as the body PostgREST answers a 500 with
3156
+ * and the original error under `cause`. Nothing is logged and nothing is dropped: `error` is the
3157
+ * only channel, so the caller decides what to do with it.
3158
+ */
3159
+ const toSafeFailure = (input, e) => {
3160
+ const error = e instanceof QueryError ? e : new QueryError(input, new ApiError({
3161
+ status: 500,
3162
+ headers: {},
3163
+ body: {
3164
+ code: "PGRSTX00",
3165
+ message: "Internal error",
3166
+ details: null,
3167
+ hint: null
3168
+ }
3169
+ }), { cause: e });
3170
+ return {
3171
+ data: null,
3172
+ error,
3173
+ status: error.status
3174
+ };
3175
+ };
3176
+ /**
3177
+ * {@link runQuery} with the failure as a value: `{ data, error }`, where exactly one is set.
3178
+ * The error is the same {@link QueryError} the throwing form raises -- this only chooses how it
3179
+ * reaches you, which is a matter of taste and of how much of a result you handle in one place.
3180
+ *
3181
+ * **This never rejects.** Every failure, including one the library did not foresee, comes back as
3182
+ * `error`, so a caller that checks it needs no `try`/`catch` at all.
3183
+ */
3184
+ const runQuerySafe = async (env, input, ctx = {}) => {
3185
+ try {
3186
+ return {
3187
+ ...await runQuery(env, input, ctx),
3188
+ error: null
3189
+ };
3190
+ } catch (e) {
3191
+ return toSafeFailure(input, e);
3192
+ }
3193
+ };
3194
+
3489
3195
  //#endregion
3490
3196
  //#region src/index.ts
3491
3197
  const UNEXPOSABLE = new Set(["pg_catalog", "information_schema"]);
3492
- /** Create a PostgREST-compatible handler over a Postgres database. */
3493
- const createPgRest = async (options) => {
3198
+ const create = async (options) => {
3494
3199
  const schemas = options.schemas?.length ? [...options.schemas] : ["public"];
3495
3200
  for (const s of schemas) if (UNEXPOSABLE.has(s)) throw new Error(`pg-rest: the schema "${s}" cannot be exposed`);
3496
3201
  const user = options.db;
@@ -3537,13 +3242,40 @@ const createPgRest = async (options) => {
3537
3242
  cache: store.current()
3538
3243
  }, request, context ?? {});
3539
3244
  };
3245
+ const query = async (input, ctx) => await runQuery({
3246
+ db: adapter,
3247
+ config,
3248
+ cache: store.current()
3249
+ }, input, ctx ?? {});
3250
+ const querySafe = async (input, ctx) => {
3251
+ try {
3252
+ return await runQuerySafe({
3253
+ db: adapter,
3254
+ config,
3255
+ cache: store.current()
3256
+ }, input, ctx ?? {});
3257
+ } catch (e) {
3258
+ return toSafeFailure(input, e);
3259
+ }
3260
+ };
3540
3261
  return {
3541
3262
  handle,
3263
+ query,
3264
+ querySafe,
3265
+ generateTypes: async (forTypes) => (await import("./types.js")).generateTypes(store.current(), forTypes),
3542
3266
  reloadSchema: async () => void await store.reload(),
3543
3267
  schemaCache: async () => store.current(),
3544
3268
  close: async () => {}
3545
3269
  };
3546
3270
  };
3271
+ /**
3272
+ * Create a PostgREST-compatible handler over a Postgres database.
3273
+ *
3274
+ * `createPgRest<Db>(options)` returns the same instance with `query` and `querySafe` typed by a
3275
+ * generated database type; the cast is the whole of that difference, since types never reach
3276
+ * runtime and the two instances are one object.
3277
+ */
3278
+ const createPgRest = create;
3547
3279
 
3548
3280
  //#endregion
3549
- export { ApiError, DbQueryError, PgRestError, SchemaLoadError, createPgRest };
3281
+ export { ApiError, DbQueryError, PgRestError, QueryError, SchemaLoadError, createPgRest };