@adrdp/pg-rest 0.1.3 → 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/README.md +89 -134
- package/dist/adapters/bun.d.ts +1 -1
- package/dist/adapters/node-postgres.d.ts +1 -1
- package/dist/adapters/pglite.d.ts +1 -1
- package/dist/adapters/postgres-js.d.ts +1 -1
- package/dist/index.d.ts +642 -130
- package/dist/index.js +380 -648
- package/dist/schema-BhF_TQdX.js +669 -0
- package/dist/types-DaNxiC7H.d.ts +146 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +202 -0
- package/package.json +7 -1
- /package/dist/{adapter-SqPs6SuJ.d.ts → adapter-C2udR5IT.d.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,157 +1,5 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
if (!
|
|
2767
|
-
|
|
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,
|
|
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
|
-
|
|
2194
|
+
caller.headers?.forEach((v, k) => {
|
|
2791
2195
|
headers[k] = v;
|
|
2792
2196
|
});
|
|
2793
|
-
entries.push(["request.method", method], ["request.path",
|
|
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
|
-
|
|
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,
|
|
2926
|
-
else plan = planRpc(env, method, schema, target.name,
|
|
2927
|
-
return execute(env, ctx,
|
|
2928
|
-
|
|
2929
|
-
|
|
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(
|
|
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,
|
|
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 =
|
|
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
|
|
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(
|
|
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,
|
|
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,
|
|
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:
|
|
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,
|
|
2744
|
+
return buildResponse(env, method, prefer, format, plan, result, honoredTx);
|
|
3334
2745
|
};
|
|
3335
|
-
const buildResponse = (env,
|
|
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
|
-
|
|
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 };
|